diff --git a/.gitignore b/.gitignore index e714b7b..43a75c3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,9 @@ build/ +src/version.h .idea/ cmake-build-debug/ +.codechecker/ .vscode/* *.code-workspace diff --git a/CMakeLists.txt b/CMakeLists.txt index c7ab9ce..24c5c81 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,6 +39,23 @@ ENDIF() set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Werror") set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +# ------------------------------------------------------------------------------ +# Git version +# ------------------------------------------------------------------------------ + +execute_process( + COMMAND git describe --tags --dirty=-modified + OUTPUT_VARIABLE GIT_VERSION + OUTPUT_STRIP_TRAILING_WHITESPACE +) + +# Configure a header file to pass the version number to the source code +configure_file( + "${PROJECT_SOURCE_DIR}/src/version.h.in" + "${PROJECT_SOURCE_DIR}/src/version.h" + @ONLY +) + # ------------------------------------------------------------------------------ # Clang format # ------------------------------------------------------------------------------ @@ -143,7 +160,7 @@ include (CTest) ## Simple test wether we can run the application (should basic hidapi functions, like enumerate, work) enable_testing() add_test(run_test headsetcontrol) -set_tests_properties(run_test PROPERTIES PASS_REGULAR_EXPRESSION "No supported headset found") +set_tests_properties(run_test PROPERTIES PASS_REGULAR_EXPRESSION "No supported device found;Found") # use make check to compile+test add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND} DEPENDS headsetcontrol) diff --git a/README.md b/README.md index 6b65501..b922303 100644 --- a/README.md +++ b/README.md @@ -9,50 +9,31 @@ talking. This differs from a simple loopback via PulseAudio as you won't have an ## Supported Headsets -- HyperX Cloud Alpha Wireless - - Battery, Inactive time, Sidetone, Voice Prompts (only tested on Linux) -- HyperX Cloud Flight Wireless - - Battery only (only tested on Linux) -- Corsair **Void** (Most void-versions*) - - Sidetone, Battery (for Wireless), LED on/off, Notification Sound -- Logitech G430 - - No support in current version (Last working on macOS in commit 41be99379f) -- Logitech G432 - - Sidetone (only tested on Linux) -- Logitech G433 - - Sidetone (only tested on Linux) -- Logitech G533 - - Sidetone, Battery (for Wireless) -- Logitech G535 - - Sidetone, Battery, Inactive time (only tested on Linux) -- Logitech G633 / G635 / G733 / G933 / G935 - - Sidetone, Battery (for Wireless), LED on/off -- Logitech G930 - - Sidetone, Battery -- SteelSeries Arctis 1, Arctis 1 for XBox - - Sidetone, Battery, Inactive time -- SteelSeries Arctis Nova 3 - - Sidetone, Equalizer Presets, Equalizer, Microphone Mute LED Brightness, Microphone Volume -- SteelSeries Arctis (7 and Pro) - - Sidetone, Battery, Inactive time, Chat-Mix level, LED on/off (allows to turn off the blinking LED on the base-station) -- SteelSeries Arctis 7+ - - Sidetone, Battery, Chat-Mix level, Inactive time, Equalizer Presets, Equalizer -- SteelSeries Arctis Nova 7 - - Sidetone, Battery, Chat-Mix level, Inactive time, Equalizer Presets, Equalizer -- SteelSeries Arctis 9 - - Sidetone, Battery, Inactive time, Chat-Mix level -- SteelSeries Arctis Pro Wireless - - Sidetone, Battery, Inactive time -- SteelSeries Arctis Nova Pro Wireless - - Sidetone, Battery, Inactive time -- Logitech G PRO - - Sidetone, Battery, Inactive time -- Logitech G PRO X 2 - - Sidetone, Inactive time -- Logitech Zone Wired/Zone 750 - - Sidetone, Voice prompts, Rotate to mute -- Roccat Elo 7.1 Air - - LED on/off, Inactive time (Note for Linux: Sidetone is handled by sound driver => use AlsaMixer) +| Device | sidetone | battery | notification sound | lights | inactive time | chatmix | voice prompts | rotate to mute | equalizer preset | equalizer | microphone mute led brightness | microphone volume | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| Corsair Headset Device | x | x | x | x | | | | | | | | | +| HyperX Cloud Alpha Wireless | x | x | | | x | | x | | | | | | +| HyperX Cloud Flight Wireless | | x | | | | | | | | | | | +| Logitech G430 | x | | | | | | | | | | | | +| Logitech G432/G433 | x | | | | | | | | | | | | +| Logitech G533 | x | x | | | x | | | | | | | | +| Logitech G535 | x | x | | | x | | | | | | | | +| Logitech G930 | x | x | | | | | | | | | | | +| Logitech G633/G635/G733/G933/G935 | x | x | | x | | | | | | | | | +| Logitech G PRO Series | x | x | | | x | | | | | | | | +| Logitech G PRO X 2 | x | | | | x | | | | | | | | +| Logitech Zone Wired/Zone 750 | x | | | | | | x | x | | | | | +| SteelSeries Arctis (1/7X) Wireless | x | x | | | x | | | | | | | | +| SteelSeries Arctis (7/Pro) | x | x | | x | x | x | | | | | | | +| SteelSeries Arctis 9 | x | x | | | x | x | | | | | | | +| SteelSeries Arctis Pro Wireless | x | x | | | x | | | | | | | | +| ROCCAT Elo 7.1 Air | | | | x | x | | | | | | | | +| ROCCAT Elo 7.1 USB | | | | x | | | | | | | | | +| SteelSeries Arctis Nova 3 | x | | | | | | | | x | x | x | x | +| SteelSeries Arctis Nova 7 | x | x | | | x | x | | | x | x | | | +| SteelSeries Arctis 7+ | x | x | | | x | x | | | x | x | | | +| SteelSeries Arctis Nova Pro Wireless | x | x | | x | x | | | | x | x | | | +| HeadsetControl Test device | x | x | x | x | x | x | x | x | x | x | x | x | For non-supported headsets on Linux: There is a chance that you can set the sidetone via AlsaMixer @@ -66,7 +47,7 @@ Some headsets expose sidetone as audio-channel volume and as such can be changed ### Prerequisites -You will need hidapi, c compilers and cmake. All usually installable via package managers. +Before building, ensure you have the necessary dependencies installed, including HIDAPI, C compilers, and CMake. These dependencies can usually be installed via your system's package manager. #### Debian / Ubuntu @@ -74,8 +55,9 @@ You will need hidapi, c compilers and cmake. All usually installable via package #### CentOS / RHEL (RedHat based) -RHEL and CentOS also require the epel-repository: `yum install epel-release`. Please inform yourself about the consequences of activating the epel-repository. +RHEL and CentOS also require the epel-repository. +`yum install epel-release` `yum groupinstall "Development tools"` `yum install git cmake hidapi-devel` @@ -106,18 +88,19 @@ RHEL and CentOS also require the epel-repository: `yum install epel-release`. Pl #### Mac OS X -I recommend using [Homebrew](https://brew.sh). +Recommendation: Use [Homebrew](https://brew.sh). -You can automatically compile and install the latest version of the software, by using -`brew install sapd/headsetcontrol/headsetcontrol --HEAD`. +* To automatically compile and install the latest version: +`brew install sapd/headsetcontrol/headsetcontrol --HEAD` +* To manually compile, first install the dependencies: +`brew install hidapi cmake` -If you wish to compile it manually, you can install the dependencies with `brew install hidapi cmake`. - -Also you have to download Xcode via the Mac App Store for the compilers. +Note: Xcode must be downloaded via the Mac App Store for the compilers. #### Windows -Windows support is a bit experimental and might not work in all cases. You can find binaries in the [releases](https://github.com/Sapd/HeadsetControl/releases) page, or compile instructions via MSYS2/MinGW in the [wiki](https://github.com/Sapd/HeadsetControl/wiki/Development#windows). +* Binaries are available on the [releases](https://github.com/Sapd/HeadsetControl/releases) page. +* For compilation instructions using MSYS2/MinGW refer to the [wiki](https://github.com/Sapd/HeadsetControl/wiki/Development#windows). ### Compiling @@ -128,58 +111,49 @@ cmake .. make ``` -If you want to be able to call HeadsetControl from every folder type: +To make `headsetcontrol` accessible globally, run: ```bash -make install +sudo make install ``` -This will copy the binary to a folder globally accessible via path. - -### Access without root - -Also in Linux, you need udev rules if you don't want to start the application with root. Those rules are generated via `headsetcontrol -u`. Typing `make install` on Linux generates and writes them automatically to /etc/udev/rules.d/. - -You can reload udev configuration without reboot via `sudo udevadm control --reload-rules && sudo udevadm trigger` - -## Usage - -Type `headsetcontrol -h` to get all available options.\ -(Don't forget to prefix it with `./` when the application resides in the current folder) - -Type `headsetcontrol -?` to get a list of supported capabilities for the currently detected headset. - -`headsetcontrol -s 128` sets the sidetone to 128 (REAL loud). You can silence it with `0`. I recommend a loudness of 16. - -The following options don't work on all devices yet: - -`headsetcontrol -b` check battery level. Returns a value from 0 to 100 or loading. +This command installs the binary in a location that is globally accessible via your system's PATH. -`headsetcontrol -n 0|1` sends a notification sound, made by the headset. 0 or 1 are currently supported as values. +### Access Without Root (Linux only) -`headsetcontrol -l 0|1` switches LED off/on (off almost doubles battery lifetime!). +To use the application without root privileges on Linux, udev rules are required. These can be generated with: -`headsetcontrol --short-output` cut unnecessary output, for reading by other scripts or applications. +```bash +headsetcontrol -u +``` -`headsetcontrol -i 0-90` sets inactive time in minutes, time must be between 0 and 90, 0 disables the feature. +Running `sudo make install` will automatically generate and write these rules to /etc/udev/rules.d/. To apply the changes without rebooting, reload udev configuration: -`headsetcontrol -m` retrieves the current chat-mix-dial level setting between 0 and 128. Below 64 is the game side and above is the chat side. +```bash +sudo udevadm control --reload-rules && sudo udevadm trigger +``` -`headsetcontrol -v 0|1` turn voice prompts on or off. +## Usage -`headsetcontrol -r 0|1` turn rotate to mute feature on or off. +To view available options for your device, use: -`headsetcontrol -u` Generates and outputs udev-rules for Linux. +```bash +headsetcontrol -h +``` -`headsetcontrol --dev` Advanced menu for developers, to send and/or receive custom data +For a complete list of all options, run: -`headsetcontrol -p 0-3` sets equalizer preset, must be between 0 and 3, 0 is the default preset. +```bash +headsetcontrol --help-all +``` -`headsetcontrol -e string` sets equalizer to specified curve, string must contain band values specific to the device (hex or decimal) delimited by spaces, or commas, or new-lines e.g "0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18". +To use headsetcontrol in scripts or other applications, explore: -##### Modifiers +```bash +headsetcontrol --output +``` -`--timeout 5000` Specifies a timeout for read-operations in milliseconds. Default is 5 seconds, 0 disables timeout. +Note: When running the application from the current directory, prefix commands with `./` ### Third Party diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0054167..7b6a434 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -8,6 +8,8 @@ set(SOURCE_FILES ${SOURCE_FILES} ${CMAKE_CURRENT_SOURCE_DIR}/device_registry.h ${CMAKE_CURRENT_SOURCE_DIR}/hid_utility.c ${CMAKE_CURRENT_SOURCE_DIR}/hid_utility.h + ${CMAKE_CURRENT_SOURCE_DIR}/output.c + ${CMAKE_CURRENT_SOURCE_DIR}/output.h ${CMAKE_CURRENT_SOURCE_DIR}/utility.c ${CMAKE_CURRENT_SOURCE_DIR}/utility.h PARENT_SCOPE) diff --git a/src/device.c b/src/device.c index 59c0d16..771964e 100644 --- a/src/device.c +++ b/src/device.c @@ -3,7 +3,7 @@ const char* const capabilities_str[NUM_CAPABILITIES] = { [CAP_SIDETONE] = "sidetone", - [CAP_BATTERY_STATUS] = "battery status", + [CAP_BATTERY_STATUS] = "battery", [CAP_NOTIFICATION_SOUND] = "notification sound", [CAP_LIGHTS] = "lights", [CAP_INACTIVE_TIME] = "inactive time", @@ -16,6 +16,22 @@ const char* const capabilities_str[NUM_CAPABILITIES] [CAP_MICROPHONE_VOLUME] = "microphone volume" }; +const char* const capabilities_str_enum[NUM_CAPABILITIES] + = { + [CAP_SIDETONE] = "CAP_SIDETONE", + [CAP_BATTERY_STATUS] = "CAP_BATTERY_STATUS", + [CAP_NOTIFICATION_SOUND] = "CAP_NOTIFICATION_SOUND", + [CAP_LIGHTS] = "CAP_LIGHTS", + [CAP_INACTIVE_TIME] = "CAP_INACTIVE_TIME", + [CAP_CHATMIX_STATUS] = "CAP_CHATMIX_STATUS", + [CAP_VOICE_PROMPTS] = "CAP_VOICE_PROMPTS", + [CAP_ROTATE_TO_MUTE] = "CAP_ROTATE_TO_MUTE", + [CAP_EQUALIZER_PRESET] = "CAP_EQUALIZER_PRESET", + [CAP_EQUALIZER] = "CAP_EQUALIZER", + [CAP_MICROPHONE_MUTE_LED_BRIGHTNESS] = "CAP_MICROPHONE_MUTE_LED_BRIGHTNESS", + [CAP_MICROPHONE_VOLUME] = "CAP_MICROPHONE_VOLUME" + }; + const char capabilities_str_short[NUM_CAPABILITIES] = { [CAP_SIDETONE] = 's', diff --git a/src/device.h b/src/device.h index bcddc3d..69f2fe8 100644 --- a/src/device.h +++ b/src/device.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #define VENDOR_CORSAIR 0x1b1c @@ -8,6 +9,9 @@ #define VENDOR_STEELSERIES 0x1038 #define VENDOR_ROCCAT 0x1e7d +#define VENDOR_TESTDEVICE 0xF00B +#define PRODUCT_TESTDEVICE 0xA00C + /// Convert given number to bitmask #define B(X) (1 << X) @@ -35,10 +39,22 @@ enum capabilities { NUM_CAPABILITIES }; +enum capabilitytype { + CAPABILITYTYPE_ACTION, + CAPABILITYTYPE_INFO +}; + /// Long name of every capability extern const char* const capabilities_str[NUM_CAPABILITIES]; -/// Short name of every capability +/// Short name of every capability (deprecated) extern const char capabilities_str_short[NUM_CAPABILITIES]; +/// Enum name of every capability +extern const char* const capabilities_str_enum[NUM_CAPABILITIES]; + +static inline bool has_capability(int device_capabilities, enum capabilities cap) +{ + return (device_capabilities & B(cap)) == B(cap); +} struct capability_detail { // Usage page, only used when usageid is not 0; HID Protocol specific @@ -53,7 +69,8 @@ struct capability_detail { */ enum battery_status { BATTERY_UNAVAILABLE = 65534, - BATTERY_CHARGING = 65535 + BATTERY_CHARGING = 65535, + BATTERY_AVAILABLE = 65500, }; enum headsetcontrol_errors { @@ -62,6 +79,31 @@ enum headsetcontrol_errors { HSC_OUT_OF_BOUNDS = -102, }; +typedef enum { + FEATURE_SUCCESS, + FEATURE_ERROR, + FEATURE_DEVICE_FAILED_OPEN, + FEATURE_INFO, // For non-error, informational states like "charging" + FEATURE_NOT_PROCESSED +} FeatureStatus; + +typedef struct { + FeatureStatus status; + /// Can hold battery level, error codes, or special status codes + int value; + /// For error messages, "Charging", "Unavailable", etc. Should be free()d after use + char* message; +} FeatureResult; + +typedef struct { + enum capabilities cap; + enum capabilitytype type; + void* param; + bool should_process; + FeatureResult result; + const char* name; +} FeatureRequest; + /** @brief Defines equalizer custom setings */ struct equalizer_settings { @@ -88,6 +130,9 @@ struct device { /// Name of device, used as information for the user char device_name[64]; + wchar_t device_hid_vendorname[64]; + wchar_t device_hid_productname[64]; + /// Bitmask of currently supported features the software can currently handle int capabilities; /// Details of all capabilities (e.g. to which interface to connect) diff --git a/src/device_registry.c b/src/device_registry.c index 7743c48..cddf229 100644 --- a/src/device_registry.c +++ b/src/device_registry.c @@ -1,6 +1,7 @@ #include "device_registry.h" #include "devices/corsair_void.h" +#include "devices/headsetcontrol_test.h" #include "devices/hyperx_calphaw.h" #include "devices/hyperx_cflight.h" #include "devices/logitech_g430.h" @@ -23,43 +24,71 @@ #include "devices/steelseries_arctis_nova_pro_wireless.h" #include "devices/steelseries_arctis_pro_wireless.h" +#include +#include #include -#define NUMDEVICES 22 +// Pointer to an array of pointers to device +static struct device** devicelist = NULL; +int num_devices = 0; -// array of pointers to device -static struct device*(devicelist[NUMDEVICES]); +void add_device(void (*init_func)(struct device**)); void init_devices() { - void_init(&devicelist[0]); - g430_init(&devicelist[1]); - g533_init(&devicelist[2]); - g930_init(&devicelist[3]); - g933_935_init(&devicelist[4]); - arctis_1_init(&devicelist[5]); - arctis_7_init(&devicelist[6]); - arctis_9_init(&devicelist[7]); - arctis_pro_wireless_init(&devicelist[8]); - gpro_init(&devicelist[9]); - zone_wired_init(&devicelist[10]); - elo71Air_init(&devicelist[11]); - g432_init(&devicelist[12]); - elo71USB_init(&devicelist[13]); - arctis_7_plus_init(&devicelist[14]); - cflight_init(&devicelist[15]); - g535_init(&devicelist[16]); - arctis_nova_3_init(&devicelist[17]); - arctis_nova_7_init(&devicelist[18]); - calphaw_init(&devicelist[19]); - arctis_nova_pro_wireless_init(&devicelist[20]); - gpro_x2_init(&devicelist[21]); + // Corsair + add_device(void_init); + // HyperX + add_device(calphaw_init); + add_device(cflight_init); + // Logitech + add_device(g430_init); + add_device(g432_init); + add_device(g533_init); + add_device(g535_init); + add_device(g930_init); + add_device(g933_935_init); + add_device(gpro_init); + add_device(gpro_x2_init); + add_device(zone_wired_init); + // SteelSeries + add_device(arctis_1_init); + add_device(arctis_7_init); + add_device(arctis_9_init); + add_device(arctis_pro_wireless_init); + // Roccat + add_device(elo71Air_init); + add_device(elo71USB_init); + // SteelSeries + add_device(arctis_nova_3_init); + add_device(arctis_nova_7_init); + add_device(arctis_7_plus_init); + add_device(arctis_nova_pro_wireless_init); + + add_device(headsetcontrol_test_init); +} + +void add_device(void (*init_func)(struct device**)) +{ + // Reallocate memory to accommodate the new device + struct device** temp = realloc(devicelist, (num_devices + 1) * sizeof(struct device*)); + if (temp == NULL) { + fprintf(stderr, "Failed to allocate memory for device list.\n"); + abort(); + } + devicelist = temp; + + // Initialize the new device + init_func(&devicelist[num_devices]); + num_devices++; } int get_device(struct device* device_found, uint16_t idVendor, uint16_t idProduct) { + assert(num_devices > 0); + // search for an implementation supporting one of the vendor+productid combination - for (int i = 0; i < NUMDEVICES; i++) { + for (int i = 0; i < num_devices; i++) { if (devicelist[i]->idVendor == idVendor) { // one device file can contain multiple product ids, iterate them for (int y = 0; y < devicelist[i]->numIdProducts; y++) { @@ -78,7 +107,9 @@ int get_device(struct device* device_found, uint16_t idVendor, uint16_t idProduc int iterate_devices(int index, struct device** device_found) { - if (index < NUMDEVICES) { + assert(num_devices > 0); + + if (index < num_devices) { *device_found = devicelist[index]; return 0; } else { diff --git a/src/devices/CMakeLists.txt b/src/devices/CMakeLists.txt index a722cb3..91227c9 100644 --- a/src/devices/CMakeLists.txt +++ b/src/devices/CMakeLists.txt @@ -43,4 +43,6 @@ set(SOURCE_FILES ${SOURCE_FILES} ${CMAKE_CURRENT_SOURCE_DIR}/logitech_zone_wired.h ${CMAKE_CURRENT_SOURCE_DIR}/logitech_gpro_x2.c ${CMAKE_CURRENT_SOURCE_DIR}/logitech_gpro_x2.h + ${CMAKE_CURRENT_SOURCE_DIR}/headsetcontrol_test.c + ${CMAKE_CURRENT_SOURCE_DIR}/headsetcontrol_test.h PARENT_SCOPE) diff --git a/src/devices/headsetcontrol_test.c b/src/devices/headsetcontrol_test.c new file mode 100644 index 0000000..58112bb --- /dev/null +++ b/src/devices/headsetcontrol_test.c @@ -0,0 +1,144 @@ +#include "../device.h" +#include "../utility.h" + +#include +#include + +static struct device device_headsetcontrol_test; + +static int headsetcontrol_test_send_sidetone(hid_device* device_handle, uint8_t num); +static int headsetcontrol_test_request_battery(hid_device* device_handle); +static int headsetcontrol_test_notification_sound(hid_device* device_handle, uint8_t soundid); +static int headsetcontrol_test_lights(hid_device* device_handle, uint8_t on); + +#define TESTBYTES_SEND 32 + +static const uint16_t PRODUCT_IDS[] = { PRODUCT_TESTDEVICE }; + +static int headsetcontrol_test_send_sidetone(hid_device* device_handle, uint8_t num); +static int headsetcontrol_test_request_battery(hid_device* device_handle); +static int headsetcontrol_test_notification_sound(hid_device* device_handle, uint8_t soundid); +static int headsetcontrol_test_lights(hid_device* device_handle, uint8_t on); +static int headsetcontrol_test_send_equalizer_preset(hid_device* device_handle, uint8_t num); +static int headsetcontrol_test_send_equalizer(hid_device* device_handle, struct equalizer_settings* settings); +static int headsetcontrol_test_send_microphone_mute_led_brightness(hid_device* device_handle, uint8_t num); +static int headsetcontrol_test_send_microphone_volume(hid_device* device_handle, uint8_t num); +static int headsetcontrol_test_switch_voice_prompts(hid_device* device_handle, uint8_t on); +static int headsetcontrol_test_switch_rotate_to_mute(hid_device* device_handle, uint8_t on); +static int headsetcontrol_test_request_chatmix(hid_device* device_handle); +static int headsetcontrol_test_set_inactive_time(hid_device* device_handle, uint8_t minutes); + +extern int test_profile; + +void headsetcontrol_test_init(struct device** device) +{ + if (test_profile < 0 || test_profile > 2) { + printf("test_profile must be between 0 and 2\n"); + abort(); + } + + device_headsetcontrol_test.idVendor = VENDOR_TESTDEVICE; + device_headsetcontrol_test.idProductsSupported = PRODUCT_IDS; + device_headsetcontrol_test.numIdProducts = 1; + + strncpy(device_headsetcontrol_test.device_name, "HeadsetControl Test device", sizeof(device_headsetcontrol_test.device_name)); + // normally filled by hid in main.c + wcsncpy(device_headsetcontrol_test.device_hid_vendorname, L"HeadsetControl", sizeof(device_headsetcontrol_test.device_hid_vendorname) / sizeof(device_headsetcontrol_test.device_hid_vendorname[0])); + wcsncpy(device_headsetcontrol_test.device_hid_productname, L"Test device", sizeof(device_headsetcontrol_test.device_hid_productname) / sizeof(device_headsetcontrol_test.device_hid_productname[0])); + + if (test_profile != 2) { + device_headsetcontrol_test.capabilities = B(CAP_SIDETONE) | B(CAP_BATTERY_STATUS) | B(CAP_NOTIFICATION_SOUND) | B(CAP_LIGHTS) | B(CAP_INACTIVE_TIME) | B(CAP_CHATMIX_STATUS) | B(CAP_VOICE_PROMPTS) | B(CAP_ROTATE_TO_MUTE) | B(CAP_EQUALIZER_PRESET) | B(CAP_EQUALIZER) | B(CAP_MICROPHONE_MUTE_LED_BRIGHTNESS) | B(CAP_MICROPHONE_VOLUME); + } else { + device_headsetcontrol_test.capabilities = B(CAP_SIDETONE) | B(CAP_LIGHTS) | B(CAP_BATTERY_STATUS); + } + + device_headsetcontrol_test.send_sidetone = &headsetcontrol_test_send_sidetone; + device_headsetcontrol_test.request_battery = &headsetcontrol_test_request_battery; + device_headsetcontrol_test.notifcation_sound = &headsetcontrol_test_notification_sound; + device_headsetcontrol_test.switch_lights = &headsetcontrol_test_lights; + device_headsetcontrol_test.send_inactive_time = &headsetcontrol_test_set_inactive_time; + device_headsetcontrol_test.request_chatmix = &headsetcontrol_test_request_chatmix; + device_headsetcontrol_test.switch_voice_prompts = &headsetcontrol_test_switch_voice_prompts; + device_headsetcontrol_test.switch_rotate_to_mute = &headsetcontrol_test_switch_rotate_to_mute; + device_headsetcontrol_test.send_equalizer_preset = &headsetcontrol_test_send_equalizer_preset; + device_headsetcontrol_test.send_equalizer = &headsetcontrol_test_send_equalizer; + device_headsetcontrol_test.send_microphone_mute_led_brightness = &headsetcontrol_test_send_microphone_mute_led_brightness; + device_headsetcontrol_test.send_microphone_volume = &headsetcontrol_test_send_microphone_volume; + + *device = &device_headsetcontrol_test; +} + +static int headsetcontrol_test_send_sidetone(hid_device* device_handle, uint8_t num) +{ + if (test_profile == 1) { + return -1; + } + + return TESTBYTES_SEND; +} + +static int headsetcontrol_test_request_battery(hid_device* device_handle) +{ + if (test_profile == 1) { + return -1; + } + + return 64; +} + +static int headsetcontrol_test_notification_sound(hid_device* device_handle, uint8_t soundid) +{ + return TESTBYTES_SEND; +} + +static int headsetcontrol_test_lights(hid_device* device_handle, uint8_t on) +{ + return TESTBYTES_SEND; +} + +static int headsetcontrol_test_send_equalizer_preset(hid_device* device_handle, uint8_t num) +{ + return TESTBYTES_SEND; +} + +static int headsetcontrol_test_send_equalizer(hid_device* device_handle, struct equalizer_settings* settings) +{ + return TESTBYTES_SEND; +} + +static int headsetcontrol_test_send_microphone_mute_led_brightness(hid_device* device_handle, uint8_t num) +{ + return TESTBYTES_SEND; +} + +static int headsetcontrol_test_send_microphone_volume(hid_device* device_handle, uint8_t num) +{ + return TESTBYTES_SEND; +} + +static int headsetcontrol_test_switch_voice_prompts(hid_device* device_handle, uint8_t on) +{ + if (test_profile == 1) { + return -1; + } + return TESTBYTES_SEND; +} + +static int headsetcontrol_test_switch_rotate_to_mute(hid_device* device_handle, uint8_t on) +{ + return TESTBYTES_SEND; +} + +static int headsetcontrol_test_request_chatmix(hid_device* device_handle) +{ + if (test_profile == 1) { + return -1; + } + + return 42; +} + +static int headsetcontrol_test_set_inactive_time(hid_device* device_handle, uint8_t minutes) +{ + return TESTBYTES_SEND; +} \ No newline at end of file diff --git a/src/devices/headsetcontrol_test.h b/src/devices/headsetcontrol_test.h new file mode 100644 index 0000000..17fb693 --- /dev/null +++ b/src/devices/headsetcontrol_test.h @@ -0,0 +1,3 @@ +#pragma once + +void headsetcontrol_test_init(struct device** device); \ No newline at end of file diff --git a/src/main.c b/src/main.c index ae64f2a..357dc92 100644 --- a/src/main.c +++ b/src/main.c @@ -1,5 +1,5 @@ /*** - Copyright (C) 2016-2018 Denis Arnst (Sapd) + Copyright (C) 2016-2024 Denis Arnst (Sapd) This file is part of HeadsetControl. @@ -21,38 +21,35 @@ #include "device.h" #include "device_registry.h" #include "hid_utility.h" +#include "output.h" #include "utility.h" +#include "version.h" #include #include #include +#include #include #include #include #include #include -int hsc_device_timeout = 5000; - -// 0=false; 1=true -static int short_output = 0; +int test_profile = 0; -/// printf only when short output not specified -#define PRINT_INFO(...) \ - { \ - if (!short_output) { \ - printf(__VA_ARGS__); \ - } \ - } +int hsc_device_timeout = 5000; /** * This function iterates through all HID devices. * * @return 0 when a supported device is found */ -static int find_device(struct device* device_found) +static int find_device(struct device* device_found, int test_device) { + if (test_device) + return get_device(device_found, VENDOR_TESTDEVICE, PRODUCT_TESTDEVICE); + struct hid_device_info* devs; struct hid_device_info* cur_dev; int found = -1; @@ -62,7 +59,6 @@ static int find_device(struct device* device_found) found = get_device(device_found, cur_dev->vendor_id, cur_dev->product_id); if (found == 0) { - PRINT_INFO("Found %s!\n", device_found->device_name); break; } @@ -99,6 +95,37 @@ static void print_udevrules() printf("LABEL=\"headset_end\"\n"); } +static void print_readmetable() +{ + int i = 0; + struct device* device_found; + + printf("| Device |"); + for (int j = 0; j < NUM_CAPABILITIES; j++) { + printf(" %s |", capabilities_str[j]); + } + printf("\n"); + + printf("| --- | "); + for (int j = 0; j < NUM_CAPABILITIES; j++) { + printf("--- | "); + } + printf("\n"); + + while (iterate_devices(i++, &device_found) == 0) { + printf("| %s |", device_found->device_name); + + for (int j = 0; j < NUM_CAPABILITIES; j++) { + if (has_capability(device_found->capabilities, j)) { + printf(" x |"); + } else { + printf(" |"); + } + } + printf("\n"); + } +} + /** * @brief Checks if an existing connection exists, and either uses it, or closes it and creates a new one * @@ -111,7 +138,7 @@ static void print_udevrules() * @param device_handle an existing device handle or NULL if none yet * @param device headsetcontrol struct, containing vendor and productid * @param cap which capability to use, to determine interfaceid and usageids - * @return 0 if successfull, or hid error code + * @return hid_device pointer if successfull, or NULL (error in hid_error) */ static hid_device* dynamic_connect(char** existing_hid_path, hid_device* device_handle, struct device* device, enum capabilities cap) @@ -121,8 +148,6 @@ static hid_device* dynamic_connect(char** existing_hid_path, hid_device* device_ device->capability_details[cap].interface, device->capability_details[cap].usagepage, device->capability_details[cap].usageid); if (!hid_path) { - fprintf(stderr, "Requested/supported HID device not found or system error.\n"); - fprintf(stderr, " HID Error: %S\n", hid_error(NULL)); return NULL; } @@ -140,13 +165,13 @@ static hid_device* dynamic_connect(char** existing_hid_path, hid_device* device_ device_handle = hid_open_path(hid_path); if (device_handle == NULL) { - fprintf(stderr, "Failed to open requested device.\n"); - fprintf(stderr, " HID Error: %S\n", hid_error(NULL)); - *existing_hid_path = NULL; return NULL; } + hid_get_manufacturer_string(device_handle, device->device_hid_vendorname, sizeof(device->device_hid_vendorname) / sizeof(device->device_hid_vendorname[0])); + hid_get_product_string(device_handle, device->device_hid_productname, sizeof(device->device_hid_productname) / sizeof(device->device_hid_productname[0])); + *existing_hid_path = hid_path; return device_handle; } @@ -159,21 +184,33 @@ static hid_device* dynamic_connect(char** existing_hid_path, hid_device* device_ * @param hid_path points to an already used path used to connect, or points to null * @param cap requested feature * @param param first parameter of the feature - * @return int 0 on success, some other number otherwise + * @return FeatureResult which saves the result or failure of the requested feature */ -static int handle_feature(struct device* device_found, hid_device** device_handle, char** hid_path, enum capabilities cap, void* param) +static FeatureResult handle_feature(struct device* device_found, hid_device** device_handle, char** hid_path, enum capabilities cap, void* param) { + FeatureResult result; + // Check if the headset implements the requested feature if ((device_found->capabilities & B(cap)) == 0) { - fprintf(stderr, "Error: This headset doesn't support %s\n", capabilities_str[cap]); - return 1; + result.status = FEATURE_ERROR; + result.value = -1; + asprintf(&result.message, "This headset doesn't support %s", capabilities_str[cap]); + return result; } - *device_handle = dynamic_connect(hid_path, *device_handle, - device_found, cap); + if (device_found->idProduct != PRODUCT_TESTDEVICE) { + *device_handle = dynamic_connect(hid_path, *device_handle, + device_found, cap); - if (!device_handle | !(*device_handle)) - return 1; + if (!device_handle | !(*device_handle)) { + result.status = FEATURE_DEVICE_FAILED_OPEN; + result.value = 0; + asprintf(&result.message, "Could not open device. Error: %ls", hid_error(*device_handle)); + return result; + } + } else { + *device_handle = NULL; + } int ret; @@ -185,16 +222,24 @@ static int handle_feature(struct device* device_found, hid_device** device_handl case CAP_BATTERY_STATUS: ret = device_found->request_battery(*device_handle); - if (ret < 0) - break; - else if (ret == BATTERY_CHARGING) - short_output ? printf("-1") : printf("Battery: Charging\n"); - else if (ret == BATTERY_UNAVAILABLE) - short_output ? printf("-2") : printf("Battery: Unavailable\n"); - else - short_output ? printf("%d", ret) : printf("Battery: %d%%\n", ret); - - break; + if (ret >= 0) { // Assuming 0 or positive values are valid battery levels + result.status = FEATURE_SUCCESS; + result.value = ret; + asprintf(&result.message, "Battery: %d%%", ret); + } else if (ret == BATTERY_CHARGING || ret == BATTERY_UNAVAILABLE) { + result.status = FEATURE_INFO; + result.value = ret; + if (ret == BATTERY_CHARGING) { + result.message = strdup("Charging"); + } else { + result.message = strdup("Unavailable"); + } + } else { // Handle errors + result.status = FEATURE_ERROR; + result.value = ret; + result.message = strdup("Error retrieving battery status"); + } + return result; case CAP_NOTIFICATION_SOUND: ret = device_found->notifcation_sound(*device_handle, *(int*)param); @@ -206,21 +251,22 @@ static int handle_feature(struct device* device_found, hid_device** device_handl case CAP_INACTIVE_TIME: ret = device_found->send_inactive_time(*device_handle, *(int*)param); - - if (ret < 0) - break; - - PRINT_INFO("Successfully set inactive time to %d minutes!\n", *(int*)param); break; case CAP_CHATMIX_STATUS: ret = device_found->request_chatmix(*device_handle); - if (ret < 0) - break; + if (ret >= 0) { + result.status = FEATURE_SUCCESS; + result.value = ret; + asprintf(&result.message, "Chat-Mix: %d", ret); + } else { + result.status = FEATURE_ERROR; + result.value = ret; + result.message = strdup("Error retrieving chatmix status"); + } - short_output ? printf("%d", ret) : printf("Chat-Mix: %d\n", ret); - break; + return result; case CAP_VOICE_PROMPTS: ret = device_found->switch_voice_prompts(*device_handle, *(int*)param); @@ -232,20 +278,10 @@ static int handle_feature(struct device* device_found, hid_device** device_handl case CAP_EQUALIZER_PRESET: ret = device_found->send_equalizer_preset(*device_handle, *(int*)param); - - if (ret < 0) - break; - - PRINT_INFO("Successfully set equalizer preset to %d!\n", *(int*)param); break; case CAP_EQUALIZER: ret = device_found->send_equalizer(*device_handle, (struct equalizer_settings*)param); - - if (ret < 0) - break; - - PRINT_INFO("Successfully set equalizer!\n"); break; case CAP_MICROPHONE_MUTE_LED_BRIGHTNESS: @@ -263,25 +299,167 @@ static int handle_feature(struct device* device_found, hid_device** device_handl break; } - if (ret == HSC_READ_TIMEOUT) { - fprintf(stderr, "Failed to set/request %s, because of timeout, try again.\n", capabilities_str[cap]); - return HSC_READ_TIMEOUT; - } else if (ret == HSC_ERROR) { - fprintf(stderr, "Failed to set/request %s. HeadsetControl Error. Error: %d: %ls\n", capabilities_str[cap], ret, hid_error(*device_handle)); - return HSC_ERROR; - } else if (ret == HSC_OUT_OF_BOUNDS) { - fprintf(stderr, "Failed to set/request %s. Provided parameter out of boundaries. Error: %d: %ls\n", capabilities_str[cap], ret, hid_error(*device_handle)); - return HSC_ERROR; - } else if (ret < 0) { - fprintf(stderr, "Failed to set/request %s. Error: %d: %ls\n", capabilities_str[cap], ret, hid_error(*device_handle)); - return 1; + // Handle success + if (ret >= 0) { + result.status = FEATURE_SUCCESS; + result.value = ret; + result.message = NULL; + return result; } - PRINT_INFO("Success!\n"); - return 0; + result.status = FEATURE_ERROR; + result.value = ret; + + switch (ret) { + case HSC_READ_TIMEOUT: + asprintf(&result.message, "Failed to set/request %s, because of timeout", capabilities_str[cap]); + break; + case HSC_ERROR: + asprintf(&result.message, "Failed to set/request %s. HeadsetControl Error", capabilities_str[cap]); + break; + case HSC_OUT_OF_BOUNDS: + asprintf(&result.message, "Failed to set/request %s. Provided parameter out of boundaries", capabilities_str[cap]); + break; + default: // Must be a HID error + if (device_found->idProduct != PRODUCT_TESTDEVICE) + asprintf(&result.message, "Failed to set/request %s. Error: %d: %ls", capabilities_str[cap], ret, hid_error(*device_handle)); + else // dont call hid_error on test device, it will confuse users/devs because it will show success + asprintf(&result.message, "Failed to set/request %s. Error: %d", capabilities_str[cap], ret); + + break; + } + + return result; +} + +void print_help(char* programname, struct device* device_found, bool _show_all) +{ + bool show_all = !device_found || _show_all; + + printf("HeadsetControl by Sapd (Denis Arnst)\n\thttps://github.com/Sapd/HeadsetControl\n\n"); + printf("Version: %s\n\n", VERSION); + // printf("Usage: %s [options]\n", programname); + // printf("Options:\n"); + + if (show_all || has_capability(device_found->capabilities, CAP_SIDETONE)) { + printf("Sidetone:\n"); + printf(" -s, --sidetone LEVEL\t\tSet sidetone level (0-128)\n"); + printf("\n"); + } + + if (show_all || has_capability(device_found->capabilities, CAP_BATTERY_STATUS)) { + printf("Battery:\n"); + printf(" -b, --battery\t\t\tCheck battery level\n"); + printf("\n"); + } + + // ------ Category: lights and notifications + bool show_lights = show_all || has_capability(device_found->capabilities, CAP_LIGHTS); + bool show_voice_prompts = show_all || has_capability(device_found->capabilities, CAP_VOICE_PROMPTS); + + if (show_lights || show_voice_prompts) { + printf("%s:\n", (show_lights && show_voice_prompts) ? "Lights and Voice Prompts" : (show_lights ? "Lights" : "Voice Prompts")); + if (show_lights) { + printf(" -l, --light [0|1]\t\tTurn lights off (0) or on (1)\n"); + } + if (show_voice_prompts) { + printf(" -v, --voice-prompt [0|1]\tTurn voice prompts off (0) or on (1)\n"); + } + printf("\n"); + } + // ------ + + // ------ Category: Features + bool show_inactive_time = show_all || has_capability(device_found->capabilities, CAP_INACTIVE_TIME); + bool show_chatmix_status = show_all || has_capability(device_found->capabilities, CAP_CHATMIX_STATUS); + bool show_notification_sound = show_all || has_capability(device_found->capabilities, CAP_NOTIFICATION_SOUND); + + if (show_inactive_time || show_chatmix_status || show_notification_sound) { + printf("Features:\n"); + if (show_inactive_time) { + printf(" -i, --inactive-time MINUTES\tSet inactive time (0-90 minutes, 0 disables)\n"); + } + if (show_chatmix_status) { + printf(" -m, --chatmix LEVEL\t\tGet chat-mix-dial level (0-128, <64 for game, >64 for chat)\n"); + } + if (show_notification_sound) { + printf(" -n, --notificate SOUNDID\tPlay notification sound (SOUNDID depends on device)\n"); + } + printf("\n"); + } + // ------ + + // ------ Category: Equalizer + bool show_equalizer = show_all || has_capability(device_found->capabilities, CAP_EQUALIZER); + bool show_equalizer_preset = show_all || has_capability(device_found->capabilities, CAP_EQUALIZER_PRESET); + + if (show_equalizer || show_equalizer_preset) { + printf("Equalizer:\n"); + if (show_equalizer) { + printf(" -e, --equalizer STRING\tSet equalizer curve (values separated by spaces, commas, or new-lines)\n"); + } + if (show_equalizer_preset) { + printf(" -p, --equalizer-preset NUMBER\tSet equalizer preset (0-3, 0 for default)\n"); + } + printf("\n"); + } + // ------ + + // ------ Category: Microphone + bool show_rotate_to_mute = show_all || has_capability(device_found->capabilities, CAP_ROTATE_TO_MUTE); + bool show_microphone_mute_led_brightness = show_all || has_capability(device_found->capabilities, CAP_MICROPHONE_MUTE_LED_BRIGHTNESS); + bool show_microphone_volume = show_all || has_capability(device_found->capabilities, CAP_MICROPHONE_VOLUME); + + if (show_rotate_to_mute || show_microphone_mute_led_brightness || show_microphone_volume) { + printf("Microphone:\n"); + if (show_rotate_to_mute) { + printf(" -r, --rotate-to-mute [0|1]\t\tToggle rotate to mute (0 = off, 1 = on)\n"); + } + if (show_microphone_mute_led_brightness) { + printf(" --microphone-mute-led-brightness NUMBER\tSet mic mute LED brightness (0-3)\n"); + } + if (show_microphone_volume) { + printf(" --microphone-volume NUMBER\t\tSet microphone volume (0-128)\n"); + } + printf("\n"); + } + // ------ + + if (show_all) { + printf("Advanced:\n"); + printf(" -f, --follow [SECS]\t\tRe-run commands after SECS seconds (default 2 seconds if not specified)\n"); + printf(" --timeout MS\t\t\tSet timeout for reading data (0-100000 ms, default 5000)\n"); + printf(" -?, --capabilities\t\tList supported features of the connected headset\n\n"); + + printf("Miscellaneous:\n"); + printf(" -u\t\t\t\tOutput udev rules to stdout/console\n"); + printf(" --dev\t\t\t\tDevelopment options\n"); + printf(" --readme-helper\t\tOutput table of device features for README.md\n"); + printf(" --test-device [profile]\tUse a built-in test device instead of a real one\n"); + printf(" \t profile is an optional number for different tests\n"); + printf(" -o, --output FORMAT\t\tOutput format (JSON, YAML, ENV, STANDARD)\n"); + printf("\n"); + } + + printf("Examples:\n"); + printf(" %s -b\t\tCheck the battery level\n", programname); + printf(" %s -s 64\tSet sidetone level to 64\n", programname); + printf(" %s -l 1 -v 1\tTurn on lights and voice prompts\n", programname); + printf("\n"); + + if (!show_all && device_found) + printf("\nHint:\tOptions were filtered to your device (%s)\n\tUse --help-all to show all options (including advanced ones)\n", device_found->device_name); } -// Makes parsing of optiona arguments easier +// for --follow +volatile sig_atomic_t follow = false; + +void interruptHandler(int signal_number) +{ + follow = false; +} + +// Makes parsing of optional arguments easier // Credits to https://cfengine.com/blog/2021/optional-arguments-with-getopt-long/ #define OPTIONAL_ARGUMENT_IS_PRESENT \ ((optarg == NULL && optind < argc && argv[optind][0] != '-') \ @@ -292,6 +470,8 @@ int main(int argc, char* argv[]) { int c; + int should_print_help = 0; + int should_print_help_all = 0; int sidetone_loudness = -1; int request_battery = 0; int request_chatmix = 0; @@ -305,10 +485,15 @@ int main(int argc, char* argv[]) int microphone_mute_led_brightness = -1; int microphone_volume = -1; int dev_mode = 0; - int follow = 0; unsigned follow_sec = 2; struct equalizer_settings* equalizer = NULL; + OutputType output_format = OUTPUT_STANDARD; + int test_device = 0; + + // Init all information of supported devices + init_devices(); + #define BUFFERLENGTH 1024 float* read_buffer = calloc(BUFFERLENGTH, sizeof(float)); @@ -318,12 +503,14 @@ int main(int argc, char* argv[]) { "chatmix", no_argument, NULL, 'm' }, { "dev", no_argument, NULL, 0 }, { "help", no_argument, NULL, 'h' }, + { "help-all", no_argument, NULL, 0 }, { "equalizer", required_argument, NULL, 'e' }, { "equalizer-preset", required_argument, NULL, 'p' }, { "microphone-mute-led-brightness", required_argument, NULL, 0 }, { "microphone-volume", required_argument, NULL, 0 }, { "inactive-time", required_argument, NULL, 'i' }, { "light", required_argument, NULL, 'l' }, + { "output", optional_argument, NULL, 'o' }, { "follow", optional_argument, NULL, 'f' }, { "notificate", required_argument, NULL, 'n' }, { "rotate-to-mute", required_argument, NULL, 'r' }, @@ -331,15 +518,14 @@ int main(int argc, char* argv[]) { "short-output", no_argument, NULL, 'c' }, { "timeout", required_argument, NULL, 0 }, { "voice-prompt", required_argument, NULL, 'v' }, + { "test-device", optional_argument, NULL, 0 }, + { "readme-helper", no_argument, NULL, 0 }, { 0, 0, 0, 0 } }; int option_index = 0; - // Init all information of supported devices - init_devices(); - - while ((c = getopt_long(argc, argv, "bchi:l:f::mn:r:s:uv:p:e:?", opts, &option_index)) != -1) { + while ((c = getopt_long(argc, argv, "bchi:l:f::mn:o::r:s:uv:p:e:?", opts, &option_index)) != -1) { char* endptr = NULL; // for strtol switch (c) { @@ -347,7 +533,7 @@ int main(int argc, char* argv[]) request_battery = 1; break; case 'c': - short_output = 1; + output_format = OUTPUT_SHORT; break; case 'e': { int size = get_float_data_from_parameter(optarg, read_buffer, BUFFERLENGTH); @@ -374,7 +560,7 @@ int main(int argc, char* argv[]) if (OPTIONAL_ARGUMENT_IS_PRESENT) { follow_sec = strtol(optarg, &endptr, 10); if (follow_sec == 0) { - printf("Usage: %s -f [secs timeout]\n", argv[0]); + fprintf(stderr, "Usage: %s -f [secs timeout]\n", argv[0]); return 1; } } @@ -383,40 +569,69 @@ int main(int argc, char* argv[]) inactive_time = strtol(optarg, &endptr, 10); if (*endptr != '\0' || endptr == optarg || inactive_time > 90 || inactive_time < 0) { - printf("Usage: %s -i 0-90, 0 is off\n", argv[0]); + fprintf(stderr, "Usage: %s -i 0-90, 0 is off\n", argv[0]); return 1; } break; case 'l': lights = strtol(optarg, &endptr, 10); if (*endptr != '\0' || endptr == optarg || lights < 0 || lights > 1) { - printf("Usage: %s -l 0|1\n", argv[0]); + fprintf(stderr, "Usage: %s -l 0|1\n", argv[0]); return 1; } break; case 'm': request_chatmix = 1; break; - case 'n': // todo + case 'n': notification_sound = strtol(optarg, &endptr, 10); if (*endptr != '\0' || endptr == optarg || notification_sound < 0 || notification_sound > 1) { - printf("Usage: %s -n 0|1\n", argv[0]); + fprintf(stderr, "Usage: %s -n 0|1\n", argv[0]); + return 1; + } + break; + case 'o': + { + bool output_specified = true; + + if (OPTIONAL_ARGUMENT_IS_PRESENT) { + if (strcasecmp(optarg, "JSON") == 0) + output_format = OUTPUT_JSON; + else if (strcasecmp(optarg, "YAML") == 0) + output_format = OUTPUT_YAML; + else if (strcasecmp(optarg, "ENV") == 0) + output_format = OUTPUT_ENV; + else if (strcasecmp(optarg, "STANDARD") == 0) + output_format = OUTPUT_STANDARD; + else if (strcasecmp(optarg, "SHORT") == 0) + output_format = OUTPUT_SHORT; + else + output_specified = false; + } else { + output_specified = false; + } + + if (output_specified == false) { + // short not listed because deprecated + fprintf(stderr, "Usage: %s -o JSON|YAML|ENV|STANDARD\n", argv[0]); return 1; } + break; + } case 'p': equalizer_preset = strtol(optarg, &endptr, 10); if (*endptr != '\0' || endptr == optarg || equalizer_preset < 0 || equalizer_preset > 3) { - printf("Usage: %s -p 0-3, 0 is default\n", argv[0]); + fprintf(stderr, "Usage: %s -p 0-3, 0 is default\n", argv[0]); return 1; } break; case 'r': rotate_to_mute = strtol(optarg, &endptr, 10); if (*endptr != '\0' || endptr == optarg || rotate_to_mute < 0 || rotate_to_mute > 1) { - printf("Usage: %s -r 0|1\n", argv[0]); + fprintf(stderr, "Usage: %s -r 0|1\n", argv[0]); return 1; } break; @@ -424,7 +639,7 @@ int main(int argc, char* argv[]) sidetone_loudness = strtol(optarg, &endptr, 10); if (*endptr != '\0' || endptr == optarg || sidetone_loudness > 128 || sidetone_loudness < 0) { - printf("Usage: %s -s 0-128\n", argv[0]); + fprintf(stderr, "Usage: %s -s 0-128\n", argv[0]); return 1; } break; @@ -435,7 +650,7 @@ int main(int argc, char* argv[]) case 'v': voice_prompts = strtol(optarg, &endptr, 10); if (*endptr != '\0' || endptr == optarg || voice_prompts < 0 || voice_prompts > 1) { - printf("Usage: %s -v 0|1\n", argv[0]); + fprintf(stderr, "Usage: %s -v 0|1\n", argv[0]); return 1; } break; @@ -443,30 +658,8 @@ int main(int argc, char* argv[]) print_capabilities = 1; break; case 'h': - printf("Headsetcontrol written by Sapd (Denis Arnst)\n\thttps://github.com/Sapd\n\n"); - printf("Parameters\n"); - printf(" -s, --sidetone level\t\tSets sidetone, level must be between 0 and 128\n"); - printf(" -b, --battery\t\t\tChecks the battery level\n"); - printf(" -n, --notificate soundid\tMakes the headset play a notifiation\n"); - printf(" -l, --light 0|1\t\tSwitch lights (0 = off, 1 = on)\n"); - printf(" -c, --short-output\t\tUse more machine-friendly output \n"); - printf(" -i, --inactive-time time\tSets inactive time in minutes, time must be between 0 and 90, 0 disables the feature.\n"); - printf(" -m, --chatmix\t\t\tRetrieves the current chat-mix-dial level setting between 0 and 128. Below 64 is the game side and above is the chat side.\n"); - printf(" -v, --voice-prompt 0|1\tTurn voice prompts on or off (0 = off, 1 = on)\n"); - printf(" -r, --rotate-to-mute 0|1\tTurn rotate to mute feature on or off (0 = off, 1 = on)\n"); - printf(" -e, --equalizer string\tSets equalizer to specified curve, string must contain band values (hex or decimal), with minimum and maximum values specific to the device and delimited by spaces, or commas, or new-lines e.g \"0, 0, 0, 0, 0\".\n"); - printf(" -p, --equalizer-preset number\tSets equalizer preset, number must be between 0 and 3, 0 sets the default\n"); - printf(" --microphone-mute-led-brightness number\tSets microphone mute LED brightness, number must be between 0 and 3\n"); - printf(" --microphone-volume number\tSets microphone volume, number must be between 0 and 128\n"); - printf(" -f, --follow [secs timeout]\tRe-run the commands after the specified seconds timeout or 2 by default\n"); - printf("\n"); - printf(" --timeout 0-100000\t\tSpecifies the timeout in ms for reading data from device (default 5000)\n"); - printf(" -?, --capabilities\t\tPrint every feature headsetcontrol supports of the connected headset\n"); - printf("\n"); - printf(" -u\tOutputs udev rules to stdout/console\n"); - - printf("\n"); - return 0; + should_print_help = 1; + break; case 0: if (strcmp(opts[option_index].name, "dev") == 0) { dev_mode = 1; @@ -475,30 +668,46 @@ int main(int argc, char* argv[]) hsc_device_timeout = strtol(optarg, &endptr, 10); if (*endptr != '\0' || endptr == optarg || hsc_device_timeout < 0 || hsc_device_timeout > 100000) { - printf("Usage: %s --timeout 0-100000\n", argv[0]); + fprintf(stderr, "Usage: %s --timeout 0-100000\n", argv[0]); return 1; } - break; + // fall through } else if (strcmp(opts[option_index].name, "microphone-mute-led-brightness") == 0) { microphone_mute_led_brightness = strtol(optarg, &endptr, 10); if (*endptr != '\0' || endptr == optarg || microphone_mute_led_brightness < 0 || microphone_mute_led_brightness > 3) { - printf("Usage: %s --microphone-mute-led-brightness 0-3\n", argv[0]); + fprintf(stderr, "Usage: %s --microphone-mute-led-brightness 0-3\n", argv[0]); return 1; } - break; + // fall through } else if (strcmp(opts[option_index].name, "microphone-volume") == 0) { microphone_volume = strtol(optarg, &endptr, 10); if (*endptr != '\0' || endptr == optarg || microphone_volume < 0 || microphone_volume > 128) { - printf("Usage: %s --microphone-volume 0-128\n", argv[0]); + fprintf(stderr, "Usage: %s --microphone-volume 0-128\n", argv[0]); return 1; } - break; + // fall through + } else if (strcmp(opts[option_index].name, "test-device") == 0) { + test_device = 1; + + if (OPTIONAL_ARGUMENT_IS_PRESENT) { + test_profile = strtol(optarg, &endptr, 10); + if (test_profile < 0) { + fprintf(stderr, "Usage: %s --test-device [testprofile]\n", argv[0]); + return 1; + } + } + // fall through + } else if(strcmp(opts[option_index].name, "readme-helper") == 0) { + print_readmetable(); + return 0; + } else if (strcmp(opts[option_index].name, "help-all") == 0) { + should_print_help_all = 1; } - // fall through + break; default: - printf("Invalid argument %c\n", c); + fprintf(stderr, "Invalid argument %c\n", c); return 1; } } @@ -517,9 +726,17 @@ int main(int argc, char* argv[]) static struct device device_found; // Look for a supported device - int headset_available = find_device(&device_found); - if (headset_available != 0) { - fprintf(stderr, "No supported headset found\n"); + int headset_available = find_device(&device_found, test_device); + + if (should_print_help || should_print_help_all) { + if (headset_available == 0) + print_help(argv[0], &device_found, should_print_help_all); + else + print_help(argv[0], NULL, should_print_help_all); + + return 0; + } else if (headset_available != 0) { + output(NULL, false, output_format); return 1; } @@ -527,103 +744,75 @@ int main(int argc, char* argv[]) hid_device* device_handle = NULL; char* hid_path = NULL; - // Set all features the user wants us to set - int error = 0; - -loop_start: - if (sidetone_loudness != -1) { - if ((error = handle_feature(&device_found, &device_handle, &hid_path, CAP_SIDETONE, &sidetone_loudness)) != 0) - goto error; - } - - if (lights != -1) { - if ((error = handle_feature(&device_found, &device_handle, &hid_path, CAP_LIGHTS, &lights)) != 0) - goto error; - } - - if (notification_sound != -1) { - if ((error = handle_feature(&device_found, &device_handle, &hid_path, CAP_NOTIFICATION_SOUND, ¬ification_sound)) != 0) - goto error; - } - - if (request_battery == 1) { - if ((error = handle_feature(&device_found, &device_handle, &hid_path, CAP_BATTERY_STATUS, &request_battery)) != 0) - goto error; - } - - if (inactive_time != -1) { - if ((error = handle_feature(&device_found, &device_handle, &hid_path, CAP_INACTIVE_TIME, &inactive_time)) != 0) - goto error; - } - - if (request_chatmix == 1) { - if ((error = handle_feature(&device_found, &device_handle, &hid_path, CAP_CHATMIX_STATUS, &request_chatmix)) != 0) - goto error; - } - - if (voice_prompts != -1) { - if ((error = handle_feature(&device_found, &device_handle, &hid_path, CAP_VOICE_PROMPTS, &voice_prompts)) != 0) - goto error; - } - - if (rotate_to_mute != -1) { - if ((error = handle_feature(&device_found, &device_handle, &hid_path, CAP_ROTATE_TO_MUTE, &rotate_to_mute)) != 0) - goto error; - } - - if (equalizer_preset != -1) { - if ((error = handle_feature(&device_found, &device_handle, &hid_path, CAP_EQUALIZER_PRESET, &equalizer_preset)) != 0) - goto error; - } - - if (microphone_mute_led_brightness != -1) { - if ((error = handle_feature(&device_found, &device_handle, &hid_path, CAP_MICROPHONE_MUTE_LED_BRIGHTNESS, µphone_mute_led_brightness)) != 0) - goto error; + // Initialize signal handler for CTRL + C +#ifdef _WIN32 + signal(SIGINT, interruptHandler); +#else + struct sigaction act; + act.sa_handler = interruptHandler; + sigaction(SIGINT, &act, NULL); +#endif + + FeatureRequest featureRequests[] = { + { CAP_SIDETONE, CAPABILITYTYPE_ACTION, &sidetone_loudness, sidetone_loudness != -1 }, + { CAP_LIGHTS, CAPABILITYTYPE_ACTION, &lights, lights != -1 }, + { CAP_NOTIFICATION_SOUND, CAPABILITYTYPE_ACTION, ¬ification_sound, notification_sound != -1 }, + { CAP_BATTERY_STATUS, CAPABILITYTYPE_INFO, &request_battery, request_battery == 1 }, + { CAP_INACTIVE_TIME, CAPABILITYTYPE_ACTION, &inactive_time, inactive_time != -1 }, + { CAP_CHATMIX_STATUS, CAPABILITYTYPE_INFO, &request_chatmix, request_chatmix == 1 }, + { CAP_VOICE_PROMPTS, CAPABILITYTYPE_ACTION, &voice_prompts, voice_prompts != -1 }, + { CAP_ROTATE_TO_MUTE, CAPABILITYTYPE_ACTION, &rotate_to_mute, rotate_to_mute != -1 }, + { CAP_EQUALIZER_PRESET, CAPABILITYTYPE_ACTION, &equalizer_preset, equalizer_preset != -1 }, + { CAP_MICROPHONE_MUTE_LED_BRIGHTNESS, CAPABILITYTYPE_ACTION, µphone_mute_led_brightness, microphone_mute_led_brightness != -1 }, + { CAP_MICROPHONE_VOLUME, CAPABILITYTYPE_ACTION, µphone_volume, microphone_volume != -1 }, + { CAP_EQUALIZER, CAPABILITYTYPE_ACTION, equalizer, equalizer != NULL }, + }; + int numFeatures = sizeof(featureRequests) / sizeof(featureRequests[0]); + assert(numFeatures == NUM_CAPABILITIES); + + // For specific output types, like YAML, we will do all actions - even when not specified - to aggreate all information + if (output_format == OUTPUT_YAML || output_format == OUTPUT_JSON || output_format == OUTPUT_ENV) { + for (int i = 0; i < numFeatures; i++) { + if (featureRequests[i].type == CAPABILITYTYPE_INFO && !featureRequests[i].should_process) { + if ((device_found.capabilities & B(featureRequests[i].cap)) == B(featureRequests[i].cap)) { + featureRequests[i].should_process = true; + } + } + } } - if (microphone_volume != -1) { - if ((error = handle_feature(&device_found, &device_handle, &hid_path, CAP_MICROPHONE_VOLUME, µphone_volume)) != 0) - goto error; - } + do { + for (int i = 0; i < numFeatures; i++) { + if (featureRequests[i].should_process) { + // Assuming handle_feature now returns FeatureResult + featureRequests[i].result = handle_feature(&device_found, &device_handle, &hid_path, featureRequests[i].cap, featureRequests[i].param); + } else { + // Populate with a default "not processed" result + featureRequests[i].result.status = FEATURE_NOT_PROCESSED; + featureRequests[i].result.message = strdup("Not processed"); + featureRequests[i].result.value = 0; + } + } - if (equalizer != NULL) { - error = handle_feature(&device_found, &device_handle, &hid_path, CAP_EQUALIZER, equalizer); - free(equalizer); + DeviceList deviceList; + deviceList.device = &device_found; + deviceList.num_devices = 1; + deviceList.featureRequests = featureRequests; + deviceList.size = numFeatures; - if ((error) != 0) - goto error; - } + output(&deviceList, print_capabilities != -1, output_format); - if (print_capabilities != -1) { - PRINT_INFO("Supported capabilities:\n\n"); - - // go through all enum capabilities - for (int i = 0; i < NUM_CAPABILITIES; i++) { - // When the capability i is included in .capabilities - if ((device_found.capabilities & B(i)) == B(i)) { - if (short_output) { - printf("%c", capabilities_str_short[i]); - } else { - printf("* %s\n", capabilities_str[i]); - } - } - } - } + if (follow) + sleep(follow_sec); + } while (follow); - if (argc <= 1) { - printf("You didn't set any arguments, so nothing happened.\nType %s -h for help.\n", argv[0]); + // Free memory from features + for (int i = 0; i < numFeatures; i++) { + free(featureRequests[i].result.message); } - if (follow) { - sleep(follow_sec); - goto loop_start; - } + free(equalizer); terminate_hid(&device_handle, &hid_path); - return 0; - -error: - terminate_hid(&device_handle, &hid_path); - return error; } diff --git a/src/output.c b/src/output.c new file mode 100644 index 0000000..3347281 --- /dev/null +++ b/src/output.c @@ -0,0 +1,727 @@ +#include "output.h" +#include "version.h" + +#include +#include +#include +#include + +const char* APIVERSION = "1.0"; +const char* HEADSETCONTROL_NAME = "HeadsetControl"; + +// Function to convert enum to string +const char* status_to_string(Status status) +{ + switch (status) { + case STATUS_SUCCESS: + return "success"; + case STATUS_FAILURE: + return "failure"; + case STATUS_PARTIAL: + return "partial"; + default: + return "unknown"; + } +} + +const char* battery_status_to_string(enum battery_status status) +{ + switch (status) { + case BATTERY_UNAVAILABLE: + return "BATTERY_UNAVAILABLE"; + case BATTERY_CHARGING: + return "BATTERY_CHARGING"; + case BATTERY_AVAILABLE: + return "BATTERY_DISCHARGING"; + // Add other cases as needed + default: + return "UNKNOWN"; + } +} + +/** + * @brief Adds an error to the HeadsetInfo struct + * + * @param info Headsetinfo struct + * @param source Error source/key as string + * @param message Error message + */ +static void addError(HeadsetInfo* info, const char* source, const char* message) +{ + if (info->error_count < MAX_ERRORS) { + info->errors[info->error_count].source = strdup(source); + info->errors[info->error_count].message = strdup(message); + info->error_count++; + } else { + fprintf(stderr, "Error: addError MAX_ERRORS exceeded\n"); + abort(); + } +} + +/** + * @brief Adds an action to the HeadsetInfo struct + * + * @param info Headsetinfo struct + * @param capbability Capability as enum + * @param device Device name as string + * @param status Status of the action + * @param value Value of the action (not used currently) + * @param error_message Error message if existing or NULL + */ +static void addAction(HeadsetInfo* info, enum capabilities capbability, const char* device, Status status, int value, const char* error_message) +{ + if (info->action_count < MAX_ACTIONS) { + info->actions[info->action_count].capability = capabilities_str_enum[capbability]; + info->actions[info->action_count].capability_str = capabilities_str[capbability]; + info->actions[info->action_count].device = strdup(device); + info->actions[info->action_count].status = status; + info->actions[info->action_count].value = 0; // currently not used + + if (error_message != NULL) + info->actions[info->action_count].error_message = strdup(error_message); + else + info->actions[info->action_count].error_message = NULL; + + info->action_count++; + } else { + fprintf(stderr, "Error: addAction MAX_ACTIONS exceeded\n"); + abort(); + } +} + +static HeadsetControlStatus initializeStatus(int num_devices); +static void initializeHeadsetInfo(HeadsetInfo* info, struct device* device); +static void processFeatureRequests(HeadsetInfo* info, FeatureRequest* featureRequests, int size, struct device* device); +static void outputByType(OutputType output, HeadsetControlStatus* status, HeadsetInfo* infos, bool print_capabilities); + +void output(DeviceList* deviceList, bool print_capabilities, OutputType output) +{ + int num_devices = deviceList ? deviceList->num_devices : 0; + + HeadsetControlStatus status = initializeStatus(num_devices); + HeadsetInfo* infos = calloc(num_devices, sizeof(HeadsetInfo)); + + // Iterate through all devices + for (int deviceIndex = 0; deviceIndex < num_devices; deviceIndex++) { + initializeHeadsetInfo(&infos[deviceIndex], deviceList[deviceIndex].device); + processFeatureRequests(&infos[deviceIndex], deviceList[deviceIndex].featureRequests, deviceList[deviceIndex].size, deviceList[deviceIndex].device); + } + + // Send all gathered information to respective output function + outputByType(output, &status, infos, print_capabilities); + + for (int i = 0; i < num_devices; i++) { + free(infos[i].idVendor); + free(infos[i].idProduct); + + for (int j = 0; j < infos[i].error_count; j++) { + free(infos[i].errors[j].source); + free(infos[i].errors[j].message); + } + + for (int j = 0; j < infos[i].action_count; j++) { + free(infos[i].actions[j].device); + free(infos[i].actions[j].error_message); + } + } + free(infos); +} + +HeadsetControlStatus initializeStatus(int num_devices) +{ + HeadsetControlStatus status = { 0 }; + status.version = VERSION; + status.api_version = APIVERSION; + status.name = HEADSETCONTROL_NAME; + status.hid_version = hid_version_str(); + status.device_count = num_devices; + return status; +} + +void initializeHeadsetInfo(HeadsetInfo* info, struct device* device) +{ + info->status = STATUS_SUCCESS; + asprintf(&info->idVendor, "0x%04x", device->idVendor); + asprintf(&info->idProduct, "0x%04x", device->idProduct); + info->device_name = device->device_name; + info->vendor_name = device->device_hid_vendorname; + info->product_name = device->device_hid_productname; + + info->capabilities_amount = 0; + + for (int i = 0; i < NUM_CAPABILITIES; i++) { + if (device->capabilities & B(i)) { // Check if the ith capability is supported + info->capabilities[info->capabilities_amount] = capabilities_str_enum[i]; + info->capabilities_str[info->capabilities_amount] = capabilities_str[i]; + info->capabilities_amount++; + } + } +} + +/** + * @brief Iterates through all requested features and processes their responses + * + * @param info struct with headset information to fill + * @param featureRequests struct with all feature requests + * @param size size of featureRequests + * @param device the device struct + */ +void processFeatureRequests(HeadsetInfo* info, FeatureRequest* featureRequests, int size, struct device* device) +{ + for (int i = 0; i < size; i++) { + FeatureRequest* request = &featureRequests[i]; + if (request->should_process) { + if (request->result.status == FEATURE_DEVICE_FAILED_OPEN) { + addError(info, capabilities_str[request->cap], request->result.message); + } else if (request->cap == CAP_BATTERY_STATUS) { + if (request->result.status == FEATURE_SUCCESS || request->result.status == FEATURE_INFO) { + info->has_battery_info = true; + + if (request->result.value == BATTERY_CHARGING) { + info->battery_status = (enum battery_status)request->result.value; + info->battery_level = 0; // when charging sometimes battery can be reported anyways (even when inaccurate), but needs adjustment in device struct + } else if (request->result.value == BATTERY_UNAVAILABLE) { + info->battery_status = (enum battery_status)request->result.value; + info->battery_level = 0; + } else { + info->battery_status = BATTERY_AVAILABLE; + info->battery_level = request->result.value; + } + } else if (request->result.status == FEATURE_ERROR) { + addError(info, capabilities_str[request->cap], request->result.message); + info->status = STATUS_PARTIAL; + } + } else if (request->cap == CAP_CHATMIX_STATUS) { + if (request->result.status == FEATURE_SUCCESS || request->result.status == FEATURE_INFO) { + info->has_chatmix_info = true; + info->chatmix = request->result.value; + } else if (request->result.status == FEATURE_ERROR) { + addError(info, capabilities_str[request->cap], request->result.message); + info->status = STATUS_PARTIAL; + } + } else if (request->type == CAPABILITYTYPE_ACTION) { + Status status = request->result.status == FEATURE_SUCCESS ? STATUS_SUCCESS : STATUS_FAILURE; + addAction(info, request->cap, device->device_name, status, request->result.value, request->result.message); + } + } + } +} + +static void output_json(HeadsetControlStatus* status, HeadsetInfo* infos); +static void output_yaml(HeadsetControlStatus* status, HeadsetInfo* infos); +static void output_env(HeadsetControlStatus* status, HeadsetInfo* infos); +static void output_short(HeadsetControlStatus* status, HeadsetInfo* info, bool print_capabilities); +static void output_standard(HeadsetControlStatus* status, HeadsetInfo* info, bool print_capabilities); + +void outputByType(OutputType output, HeadsetControlStatus* status, HeadsetInfo* infos, bool print_capabilities) +{ + switch (output) { + case OUTPUT_JSON: + output_json(status, infos); + break; + case OUTPUT_YAML: + output_yaml(status, infos); + break; + case OUTPUT_ENV: + output_env(status, infos); + break; + case OUTPUT_STANDARD: + output_standard(status, infos, print_capabilities); + break; + case OUTPUT_SHORT: + output_short(status, infos, print_capabilities); + break; + } +} + +void json_print_string(const char* str, int indent) +{ + for (int i = 0; i < indent; i++) { + putchar(' '); + } + printf("\"%s\"", str); +} + +void json_printint_key_value(const char* key, int value, int indent) +{ + for (int i = 0; i < indent; i++) { + putchar(' '); + } + printf("\"%s\": \"%d\"", key, value); +} + +void json_print_key_value(const char* key, const char* value, int indent) +{ + for (int i = 0; i < indent; i++) { + putchar(' '); + } + printf("\"%s\": \"%s\"", key, value); +} + +void json_printw_key_value(const char* key, const wchar_t* value, int indent) +{ + for (int i = 0; i < indent; i++) { + putchar(' '); + } + printf("\"%s\": \"%ls\"", key, value); +} + +void output_json(HeadsetControlStatus* status, HeadsetInfo* infos) +{ + printf("{\n"); + + json_print_key_value("name", status->name, 2); + printf(",\n"); + json_print_key_value("version", status->version, 2); + printf(",\n"); + json_print_key_value("api_version", status->api_version, 2); + printf(",\n"); + json_print_key_value("hidapi_version", status->hid_version, 2); + printf(",\n"); + + if (infos->action_count > 0) { + printf(" \"actions\": [\n"); + for (int i = 0; i < infos->action_count; i++) { + printf(" {\n"); + + json_print_key_value("capability", infos->actions[i].capability, 6); + printf(",\n"); + json_print_key_value("device", infos->actions[i].device, 6); + printf(",\n"); + json_print_key_value("status", status_to_string(infos->actions[i].status), 6); + + if (infos->actions[i].value > 0) { + printf(",\n"); + json_printint_key_value("value", infos->actions[i].value, 6); + } + + if (infos->actions[i].error_message != NULL && strlen(infos->actions[i].error_message) > 0) { + printf(",\n"); + json_print_key_value("error_message", infos->actions[i].error_message, 6); + } + + printf("\n }"); + if (i < infos->action_count - 1) { + printf(",\n"); + } + } + printf("\n ],\n"); + } + + // For integers, direct printing is still simplest + printf(" \"device_count\": %d,\n", status->device_count); + + printf(" \"devices\": [\n"); + for (int i = 0; i < status->device_count; i++) { + HeadsetInfo* info = &infos[i]; + printf(" {\n"); + + json_print_key_value("status", status_to_string(info->status), 6); + printf(",\n"); + json_print_key_value("device", info->device_name, 6); + printf(",\n"); + json_printw_key_value("vendor", info->vendor_name, 6); + printf(",\n"); + json_printw_key_value("product", info->product_name, 6); + printf(",\n"); + json_print_key_value("id_vendor", info->idVendor, 6); + printf(",\n"); + json_print_key_value("id_product", info->idProduct, 6); + printf(",\n"); + + printf(" \"capabilities\": [\n"); + for (int j = 0; j < info->capabilities_amount; j++) { + json_print_string(info->capabilities[j], 8); + if (j < info->capabilities_amount - 1) { + printf(", "); + } + printf("\n"); + } + printf(" ],\n"); + + printf(" \"capabilities_str\": [\n"); + for (int j = 0; j < info->capabilities_amount; j++) { + json_print_string(info->capabilities_str[j], 8); + if (j < info->capabilities_amount - 1) { + printf(", "); + } + printf("\n"); + } + printf(" ]"); + + if (info->has_battery_info) { + printf(",\n \"battery\": {\n"); + json_print_key_value("status", battery_status_to_string(info->battery_status), 8); + printf(",\n"); + printf(" \"level\": %d\n", info->battery_level); + printf(" }"); + } + + if (info->has_chatmix_info) { + printf(",\n \"chatmix\": %d", info->chatmix); + } + + // Start of errors object + if (info->error_count > 0) { + printf(",\n \"errors\": {\n"); + for (int j = 0; j < info->error_count; ++j) { + json_print_key_value(info->errors[j].source, info->errors[j].message, 8); + if (j < info->error_count - 1) { + printf(",\n"); + } + } + printf("\n }"); // End of errors object + } + + printf("\n }"); // Close the device object + if (i < status->device_count - 1) { + printf(",\n"); + } + } + printf("\n ]\n"); // Close the devices array + printf("}\n"); // Close the JSON object +} + +static const char* yaml_replace_spaces_with_dash(const char* str); + +void yaml_print(const char* key, const char* value, int indent) +{ + for (int i = 0; i < indent; i++) { + putchar(' '); + } + if (strlen(value) == 0) + printf("%s:\n", yaml_replace_spaces_with_dash(key)); + else + printf("%s: \"%s\"\n", yaml_replace_spaces_with_dash(key), value); +} + +void yaml_printw(const char* key, const wchar_t* value, int indent) +{ + for (int i = 0; i < indent; i++) { + putchar(' '); + } + + printf("%s: \"%ls\"\n", yaml_replace_spaces_with_dash(key), value); +} + +void yaml_printint(const char* key, const int value, int indent) +{ + for (int i = 0; i < indent; i++) { + putchar(' '); + } + printf("%s: %d\n", yaml_replace_spaces_with_dash(key), value); +} + +const char* yaml_replace_spaces_with_dash(const char* str) +{ + assert(strlen(str) < 64 && "replace_spaces_with_dash: too long"); + + static char result[64]; + int i = 0; + while (str[i] != '\0') { + // replace if space, and dont replace the space when a list started with dash + if (str[i] == ' ' && !(i == 1 && str[0] == '-')) { + result[i] = '-'; + } else { + result[i] = str[i]; + } + i++; + } + result[i] = '\0'; + return result; +} + +void yaml_print_listitem(const char* value, int indent) +{ + for (int i = 0; i < indent; i++) { + putchar(' '); + } + printf("- %s\n", value); +} + +void output_yaml(HeadsetControlStatus* status, HeadsetInfo* infos) +{ + printf("---\n"); + yaml_print("name", status->name, 0); + yaml_print("version", status->version, 0); + yaml_print("api_version", status->api_version, 0); + yaml_print("hidapi_version", status->hid_version, 0); + + if (infos->action_count > 0) { + yaml_print("actions", "", 0); + + for (int i = 0; i < infos->action_count; i++) { + yaml_print("- capability", infos->actions[i].capability, 2); + yaml_print("device", infos->actions[i].device, 4); + yaml_print("status", status_to_string(infos->actions[i].status), 4); + if (infos->actions[i].value > 0) + yaml_printint("value", infos->actions[i].value, 4); + if (infos->actions[i].error_message != NULL && strlen(infos->actions[i].error_message) > 0) { + yaml_print("error_message", infos->actions[i].error_message, 4); + } + } + } + + yaml_printint("device_count", status->device_count, 0); + + if (status->device_count > 0) { + yaml_print("devices", "", 0); + } + for (int i = 0; i < status->device_count; i++) { + HeadsetInfo* info = &infos[i]; + yaml_print("- status", status_to_string(info->status), 2); + yaml_print("device", info->device_name, 4); + yaml_printw("vendor", info->vendor_name, 4); + yaml_printw("product", info->product_name, 4); + yaml_print("id_vendor", info->idVendor, 4); + yaml_print("id_product", info->idProduct, 4); + + yaml_print("capabilities", "", 4); + for (int j = 0; j < info->capabilities_amount; j++) { + yaml_print_listitem(info->capabilities[j], 6); + } + + yaml_print("capabilities_str", "", 4); + for (int j = 0; j < info->capabilities_amount; j++) { + yaml_print_listitem(info->capabilities_str[j], 6); + } + + if (info->has_battery_info) { + yaml_print("battery", "", 4); + yaml_print("status", battery_status_to_string(info->battery_status), 6); + yaml_printint("level", info->battery_level, 6); + } + + if (info->has_chatmix_info) { + yaml_printint("chatmix", info->chatmix, 4); + } + + if (info->error_count > 0) { + yaml_print("errors", "", 4); + for (int j = 0; j < info->error_count; ++j) { + yaml_print(info->errors[j].source, info->errors[j].message, 6); + } + } + } +} + +static const char* env_format_key(const char* str); + +void env_print(const char* key, const char* value) +{ + printf("%s=\"%s\"\n", env_format_key(key), value); +} + +void env_printw(const char* key, const wchar_t* value) +{ + printf("%s=\"%ls\"\n", env_format_key(key), value); +} + +void env_printint(const char* key, const int value) +{ + printf("%s=%d\n", env_format_key(key), value); +} + +const char* env_format_key(const char* str) +{ + static char result[128]; + int i = 0, j = 0; + while (str[i] != '\0' && j < sizeof(result) - 1) { + if (str[i] == ' ' || str[i] == '-') { + result[j++] = '_'; + } else { + result[j++] = toupper((unsigned char)str[i]); + } + i++; + } + result[j] = '\0'; + return result; +} + +void output_env(HeadsetControlStatus* status, HeadsetInfo* infos) +{ + env_print("HEADSETCONTROL_NAME", status->name); + env_print("HEADSETCONTROL_VERSION", status->version); + env_print("HEADSETCONTROL_API_VERSION", status->api_version); + env_print("HEADSETCONTROL_HIDAPI_VERSION", status->hid_version); + + env_printint("ACTION_COUNT", infos->action_count); + for (int i = 0; i < infos->action_count; i++) { + char prefix[64]; + sprintf(prefix, "ACTION_%d", i); + + char key[128]; + + sprintf(key, "%s_CAPABILITY", prefix); + env_print(key, infos->actions[i].capability); + sprintf(key, "%s_DEVICE", prefix); + env_print(key, infos->actions[i].device); + sprintf(key, "%s_STATUS", prefix); + env_print(key, status_to_string(infos->actions[i].status)); + + if (infos->actions[i].value > 0) { + sprintf(key, "%s_VALUE", prefix); + env_printint(key, infos->actions[i].value); + } + + if (infos->actions[i].error_message != NULL && strlen(infos->actions[i].error_message) > 0) { + sprintf(key, "%s_ERROR_MESSAGE", prefix); + env_print(key, infos->actions[i].error_message); + } + } + + env_printint("DEVICE_COUNT", status->device_count); + for (int i = 0; i < status->device_count; i++) { + HeadsetInfo* info = &infos[i]; + char prefix[64]; + sprintf(prefix, "DEVICE_%d", i); + + env_print(prefix, info->device_name); + + char key[128]; + sprintf(key, "%s_CAPABILITIES_AMOUNT", prefix); + env_printint(key, info->capabilities_amount); + for (int j = 0; j < info->capabilities_amount; j++) { + sprintf(key, "%s_CAPABILITY_%d", prefix, j); + env_print(key, info->capabilities[j]); + } + + if (info->has_battery_info) { + sprintf(key, "%s_BATTERY_STATUS", prefix); + env_print(key, battery_status_to_string(info->battery_status)); + sprintf(key, "%s_BATTERY_LEVEL", prefix); + env_printint(key, info->battery_level); + } + + // Output chatmix information + if (info->has_chatmix_info) { + sprintf(key, "%s_CHATMIX", prefix); + env_printint(key, info->chatmix); + } + + // Output error information + snprintf(key, sizeof(key), "%s_ERROR_COUNT", prefix); + env_printint(key, info->error_count); + for (int j = 0; j < info->error_count; j++) { + sprintf(key, "%s_ERROR_%d_SOURCE", prefix, j + 1); + env_print(key, info->errors[j].source); + + sprintf(key, "%s_ERROR_%d_MESSAGE", prefix, j + 1); + env_print(key, info->errors[j].message); + } + } +} + +void output_standard(HeadsetControlStatus* status, HeadsetInfo* infos, bool print_capabilities) +{ + if (status->device_count == 0) { + printf("No supported device found\n"); + return; + } + + bool outputted = false; + + for (int i = 0; i < status->device_count; i++) { + HeadsetInfo* info = &infos[i]; + if (info->product_name != NULL && wcslen(info->product_name) > 0) + printf("Found %s (%ls)!\n\n", info->device_name, info->product_name); + else + printf("Found %s!\n\n", info->device_name); + + if (print_capabilities) { + printf("Capabilities:\n"); + for (int j = 0; j < info->capabilities_amount; j++) { + printf("* %s\n", capabilities_str[j]); + } + + outputted = true; + + printf("\nHint: Use --help while the device is connected to get a filtered list of parameters\n"); + continue; + } + + if (info->has_battery_info) { + printf("Battery:\n"); + printf("\tStatus: %s\n", battery_status_to_string(info->battery_status)); + if (info->battery_status != BATTERY_UNAVAILABLE) + printf("\tLevel: %d%%\n", info->battery_level); + + outputted = true; + } + + if (info->has_chatmix_info) { + printf("Chatmix: %d\n", infos->chatmix); + + outputted = true; + } + + for (int j = 0; j < info->error_count; ++j) { + printf("Error: [%s] %s\n", info->errors[j].source, info->errors[j].message); + + outputted = true; + } + + break; // list info only one device supported for now + } + + for (int i = 0; i < infos->action_count; i++) { + outputted = true; + + if (infos->actions[i].status == STATUS_SUCCESS) { + printf("Successfully set %s!\n", infos->actions[i].capability_str); + } else { + printf("%s\n", infos->actions[i].error_message); + } + } + + if (!outputted) { + printf("HeadsetControl (%s) written by Sapd (Denis Arnst)\n\thttps://github.com/Sapd/HeadsetControl\n\n", VERSION); + printf("You didn't set any arguments, so nothing happened.\nUse -h for help.\n"); + } +} + +void output_short(HeadsetControlStatus* status, HeadsetInfo* info, bool print_capabilities) +{ + if (status->device_count == 0) { + fprintf(stderr, "No supported headset found\n"); + } + + for (int i = 0; i < status->device_count; i++) { + if (i > 0) { + fprintf(stderr, "\nWarning: multiple headsets connected but not supported by short output\n"); + break; + } + + if (info->error_count > 0) { + for (int j = 0; j < info->error_count; ++j) + fprintf(stderr, "Error: [%s]: %s\n", info->errors[j].source, info->errors[j].message); + + break; + } + + if (print_capabilities) { + for (int j = 0; j < info->capabilities_amount; j++) { + printf("%c", capabilities_str_short[i]); + } + + continue; + } + + if (info->has_battery_info) { + if (info->battery_status == BATTERY_CHARGING) + printf("-1"); + else if (info->battery_status == BATTERY_UNAVAILABLE) + printf("-2"); + else + printf("%d", info->battery_level); + } else if (info->has_chatmix_info) { + printf("%d", info->chatmix); + } + } + + // Could in theory break scripts who rely on stderr for checking other errors + // So we put it in the end hoping that the respective script does not do it after getting results it needs + fflush(stdout); + fflush(stderr); + fprintf(stderr, "\nWarning: short output deprecated, use the -o option instead\n"); +} diff --git a/src/output.h b/src/output.h new file mode 100644 index 0000000..8bedab0 --- /dev/null +++ b/src/output.h @@ -0,0 +1,104 @@ +#pragma once + +#include "device.h" + +typedef enum OutputType { + OUTPUT_JSON, + OUTPUT_YAML, + OUTPUT_ENV, + OUTPUT_STANDARD, + OUTPUT_SHORT +} OutputType; + +/** + * @brief status of the application + */ +typedef struct { + const char* name; + char* version; + const char* api_version; + const char* hid_version; + int device_count; +} HeadsetControlStatus; + +typedef enum { + STATUS_SUCCESS, + STATUS_FAILURE, + STATUS_PARTIAL +} Status; + +/** + * @brief Action is the result of sending (writing) commands to the device + * + * For converting it to JSON/YAML etc. + */ +typedef struct { + const char* capability; + const char* capability_str; + char* device; + Status status; + int value; + char* error_message; +} Action; + +#define MAX_ERRORS 10 +#define MAX_ACTIONS 16 + +typedef struct { + char* source; // For example, "battery" + char* message; // Error message related to the source +} ErrorInfo; + +/** + * @brief Struct to hold information of a device for the output implementations + * + * For converting it to JSON/YAML etc. + */ +typedef struct { + Status status; + char* idVendor; + char* idProduct; + char* device_name; + wchar_t* vendor_name; + wchar_t* product_name; + const char* capabilities[NUM_CAPABILITIES]; + const char* capabilities_str[NUM_CAPABILITIES]; + int capabilities_amount; + + bool has_battery_info; + enum battery_status battery_status; + int battery_level; + + bool has_chatmix_info; + int chatmix; + + Action actions[MAX_ACTIONS]; + int action_count; + + ErrorInfo errors[MAX_ERRORS]; + int error_count; +} HeadsetInfo; + +/** + * @brief Struct to hold information of a feature request for the output implementations + * + */ +typedef struct { + /// List of feature requests (commands which the user wants to send to the device or features to request) + FeatureRequest* featureRequests; + /// Size of the feature request list + int size; + /// List of devices + struct device* device; + /// Size of devices + int num_devices; +} DeviceList; + +/** + * @brief Main output function + * + * @param deviceList List of devices + * @param print_capabilities If the user wanted to print capabilities + * @param output Output type + */ +void output(DeviceList* deviceList, bool print_capabilities, OutputType output); \ No newline at end of file diff --git a/src/utility.h b/src/utility.h index 710d387..d07d316 100644 --- a/src/utility.h +++ b/src/utility.h @@ -30,7 +30,7 @@ unsigned int round_to_multiples(unsigned int number, unsigned int multiple); * static size_t battery_estimate_size = sizeof(battery_estimate_percentages)/sizeof(battery_estimate_percentages[0]); * int level = spline_battery_level(battery_estimate_percentages, battery_estimate_voltages, battery_estimate_size, voltage_read); * @endcode - * + * * @param p percentage values to be associated with voltage values * @param v voltage values associated with percentage values * @param size number of percentage and voltage associations diff --git a/src/version.h.in b/src/version.h.in new file mode 100644 index 0000000..f5a0a7a --- /dev/null +++ b/src/version.h.in @@ -0,0 +1,3 @@ +#pragma once + +#define VERSION "@GIT_VERSION@"