1 /***
2     Copyright (C) 2016-2018 Denis Arnst (Sapd) <https://github.com/Sapd>
3 
4     This file is part of HeadsetControl.
5 
6     HeadsetControl is free software: you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation, either version 3 of the License, or
9     (at your option) any later version.
10 
11     HeadsetControl is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15 
16     You should have received a copy of the GNU General Public License
17     along with HeadsetControl.  If not, see <http://www.gnu.org/licenses/>.
18 ***/
19 
20 #include "dev.h"
21 #include "device_registry.h"
22 #include "hid_utility.h"
23 #include "utility.h"
24 
25 #include <hidapi.h>
26 
27 #include <assert.h>
28 #include <getopt.h>
29 #include <stdio.h>
30 #include <stdlib.h>
31 #include <string.h>
32 #include <unistd.h>
33 
34 // 0=false; 1=true
35 static int short_output = 0;
36 
37 /// printf only when short output not specified
38 #define PRINT_INFO(...)          \
39     {                            \
40         if (!short_output) {     \
41             printf(__VA_ARGS__); \
42         }                        \
43     }
44 
45 /**
46  *  This function iterates through all HID devices.
47  *
48  *  @return 0 when a supported device is found
49  */
find_device(struct device * device_found)50 static int find_device(struct device* device_found)
51 {
52     struct hid_device_info* devs;
53     struct hid_device_info* cur_dev;
54     int found = -1;
55     devs      = hid_enumerate(0x0, 0x0);
56     cur_dev   = devs;
57     while (cur_dev) {
58         found = get_device(device_found, cur_dev->vendor_id, cur_dev->product_id);
59 
60         if (found == 0) {
61             PRINT_INFO("Found %s!\n", device_found->device_name);
62             break;
63         }
64 
65         cur_dev = cur_dev->next;
66     }
67     hid_free_enumeration(devs);
68 
69     return found;
70 }
71 
72 /**
73  * @brief Generates udev rules, and prints them to STDOUT
74  *
75  * Goes through the implementation of all devices, and generates udev rules for use by Linux systems
76  */
print_udevrules()77 static void print_udevrules()
78 {
79     int i = 0;
80     struct device* device_found;
81 
82     printf("ACTION!=\"add|change\", GOTO=\"headset_end\"\n");
83     printf("\n");
84 
85     while (iterate_devices(i++, &device_found) == 0) {
86         printf("# %s\n", device_found->device_name);
87 
88         for (int i = 0; i < device_found->numIdProducts; i++)
89             printf("KERNEL==\"hidraw*\", SUBSYSTEM==\"hidraw\", ATTRS{idVendor}==\"%04x\", ATTRS{idProduct}==\"%04x\", TAG+=\"uaccess\"\n",
90                 (unsigned int)device_found->idVendor, (unsigned int)device_found->idProductsSupported[i]);
91 
92         printf("\n");
93     }
94 
95     printf("LABEL=\"headset_end\"\n");
96 }
97 
98 /**
99  * @brief Checks if an existing connection exists, and either uses it, or closes it and creates a new one
100  *
101  * A device - depending on the feature - needs differend Endpoints/Connections
102  * Instead of opening and keeping track of multiple connections, we close and open connections no demand
103  *
104  *
105  *
106  * @param existing_hid_path an existing connection path or NULL if none yet
107  * @param device_handle an existing device handle or NULL if none yet
108  * @param device headsetcontrol struct, containing vendor and productid
109  * @param cap which capability to use, to determine interfaceid and usageids
110  * @return 0 if successfull, or hid error code
111  */
dynamic_connect(char ** existing_hid_path,hid_device * device_handle,struct device * device,enum capabilities cap)112 static hid_device* dynamic_connect(char** existing_hid_path, hid_device* device_handle,
113     struct device* device, enum capabilities cap)
114 {
115     // Generate the path which is needed
116     char* hid_path = get_hid_path(device->idVendor, device->idProduct,
117         device->capability_details[cap].interface, device->capability_details[cap].usagepage, device->capability_details[cap].usageid);
118 
119     if (!hid_path) {
120         fprintf(stderr, "Requested/supported HID device not found or system error.\n");
121         fprintf(stderr, " HID Error: %S\n", hid_error(NULL));
122         return NULL;
123     }
124 
125     // A connection already exists
126     if (device_handle != NULL) {
127         // The connection which exists is the same we need, so simply return it
128         if (strcmp(*existing_hid_path, hid_path) == 0) {
129             return device_handle;
130         } else { // if its not the same, and already one connection is open, close it so we can make a new one
131             hid_close(device_handle);
132         }
133     }
134 
135     free(*existing_hid_path);
136 
137     device_handle = hid_open_path(hid_path);
138     if (device_handle == NULL) {
139         fprintf(stderr, "Failed to open requested device.\n");
140         fprintf(stderr, " HID Error: %S\n", hid_error(NULL));
141 
142         *existing_hid_path = NULL;
143         return NULL;
144     }
145 
146     *existing_hid_path = hid_path;
147     return device_handle;
148 }
149 
150 /**
151  * @brief Handle a requested feature
152  *
153  * @param device_found the headset to use
154  * @param device_handle points to an already open device_handle (if connection already exists) or points to null
155  * @param hid_path points to an already used path used to connect, or points to null
156  * @param cap requested feature
157  * @param param first parameter of the feature
158  * @return int 0 on success, some other number otherwise
159  */
handle_feature(struct device * device_found,hid_device ** device_handle,char ** hid_path,enum capabilities cap,int param)160 static int handle_feature(struct device* device_found, hid_device** device_handle, char** hid_path, enum capabilities cap, int param)
161 {
162     // Check if the headset implements the requested feature
163     if ((device_found->capabilities & B(cap)) == 0) {
164         fprintf(stderr, "Error: This headset doesn't support %s\n", capabilities_str[cap]);
165         return 1;
166     }
167 
168     *device_handle = dynamic_connect(hid_path, *device_handle,
169         device_found, cap);
170 
171     if (!device_handle | !(*device_handle))
172         return 1;
173 
174     int ret;
175 
176     switch (cap) {
177     case CAP_SIDETONE:
178         ret = device_found->send_sidetone(*device_handle, param);
179         break;
180 
181     case CAP_BATTERY_STATUS:
182         ret = device_found->request_battery(*device_handle);
183 
184         if (ret < 0)
185             break;
186         else if (ret == BATTERY_CHARGING)
187             short_output ? printf("-1") : printf("Battery: Charging\n");
188         else if (ret == BATTERY_UNAVAILABLE)
189             short_output ? printf("-2") : printf("Battery: Unavailable\n");
190         else
191             short_output ? printf("%d", ret) : printf("Battery: %d%%\n", ret);
192 
193         break;
194 
195     case CAP_NOTIFICATION_SOUND:
196         ret = device_found->notifcation_sound(*device_handle, param);
197         break;
198 
199     case CAP_LIGHTS:
200         ret = device_found->switch_lights(*device_handle, param);
201         break;
202 
203     case CAP_INACTIVE_TIME:
204         ret = device_found->send_inactive_time(*device_handle, param);
205 
206         if (ret < 0)
207             break;
208 
209         PRINT_INFO("Successfully set inactive time to %d minutes!\n", param);
210         break;
211 
212     case CAP_CHATMIX_STATUS:
213         ret = device_found->request_chatmix(*device_handle);
214 
215         if (ret < 0)
216             break;
217 
218         short_output ? printf("%d", ret) : printf("Chat-Mix: %d\n", ret);
219         break;
220 
221     case CAP_VOICE_PROMPTS:
222         ret = device_found->switch_voice_prompts(*device_handle, param);
223         break;
224 
225     case CAP_ROTATE_TO_MUTE:
226         ret = device_found->switch_rotate_to_mute(*device_handle, param);
227         break;
228 
229     case NUM_CAPABILITIES:
230         ret = -99; // silence warning
231 
232         assert(0);
233         break;
234     }
235 
236     if (ret < 0) {
237         fprintf(stderr, "Failed to set %s. Error: %d: %ls\n", capabilities_str[cap], ret, hid_error(*device_handle));
238         return 1;
239     }
240 
241     PRINT_INFO("Success!\n");
242     return 0;
243 }
244 
main(int argc,char * argv[])245 int main(int argc, char* argv[])
246 {
247     int c;
248 
249     int sidetone_loudness  = -1;
250     int request_battery    = 0;
251     int request_chatmix    = 0;
252     int notification_sound = -1;
253     int lights             = -1;
254     int inactive_time      = -1;
255     int voice_prompts      = -1;
256     int rotate_to_mute     = -1;
257     int print_capabilities = -1;
258     int dev_mode           = 0;
259 
260     struct option opts[] = {
261         { "battery", no_argument, NULL, 'b' },
262         { "capabilities", no_argument, NULL, '?' },
263         { "chatmix", no_argument, NULL, 'm' },
264         { "dev", no_argument, NULL, 0 },
265         { "help", no_argument, NULL, 'h' },
266         { "inactive-time", required_argument, NULL, 'i' },
267         { "light", required_argument, NULL, 'l' },
268         { "notificate", required_argument, NULL, 'n' },
269         { "rotate-to-mute", required_argument, NULL, 'r' },
270         { "sidetone", required_argument, NULL, 's' },
271         { "short-output", no_argument, NULL, 'c' },
272         { "voice-prompt", required_argument, NULL, 'v' },
273         { 0, 0, 0, 0 }
274     };
275 
276     int option_index = 0;
277 
278     // Init all information of supported devices
279     init_devices();
280 
281     while ((c = getopt_long(argc, argv, "bchi:l:mn:r:s:uv:?", opts, &option_index)) != -1) {
282         switch (c) {
283         case 'b':
284             request_battery = 1;
285             break;
286         case 'c':
287             short_output = 1;
288             break;
289         case 'i':
290             inactive_time = strtol(optarg, NULL, 10);
291 
292             if (inactive_time > 90 || inactive_time < 0) {
293                 printf("Usage: %s -s 0-90, 0 is off\n", argv[0]);
294                 return 1;
295             }
296             break;
297         case 'l':
298             lights = strtol(optarg, NULL, 10);
299             if (lights < 0 || lights > 1) {
300                 printf("Usage: %s -l 0|1\n", argv[0]);
301                 return 1;
302             }
303             break;
304         case 'm':
305             request_chatmix = 1;
306             break;
307         case 'n': // todo
308             notification_sound = strtol(optarg, NULL, 10);
309 
310             if (notification_sound < 0 || notification_sound > 1) {
311                 printf("Usage: %s -n 0|1\n", argv[0]);
312                 return 1;
313             }
314             break;
315         case 'r':
316             rotate_to_mute = strtol(optarg, NULL, 10);
317             if (rotate_to_mute < 0 || rotate_to_mute > 1) {
318                 printf("Usage: %s -r 0|1\n", argv[0]);
319                 return 1;
320             }
321             break;
322         case 's':
323             sidetone_loudness = strtol(optarg, NULL, 10);
324 
325             if (sidetone_loudness > 128 || sidetone_loudness < 0) {
326                 printf("Usage: %s -s 0-128\n", argv[0]);
327                 return 1;
328             }
329             break;
330         case 'u':
331             fprintf(stderr, "Generating udev rules..\n\n");
332             print_udevrules();
333             return 0;
334         case 'v':
335             voice_prompts = strtol(optarg, NULL, 10);
336             if (voice_prompts < 0 || voice_prompts > 1) {
337                 printf("Usage: %s -v 0|1\n", argv[0]);
338                 return 1;
339             }
340             break;
341         case '?':
342             print_capabilities = 1;
343             break;
344         case 'h':
345             printf("Headsetcontrol written by Sapd (Denis Arnst)\n\thttps://github.com/Sapd\n\n");
346             printf("Parameters\n");
347             printf("  -s, --sidetone level\t\tSets sidetone, level must be between 0 and 128\n");
348             printf("  -b, --battery\t\t\tChecks the battery level\n");
349             printf("  -n, --notificate soundid\tMakes the headset play a notifiation\n");
350             printf("  -l, --light 0|1\t\tSwitch lights (0 = off, 1 = on)\n");
351             printf("  -c, --short-output\t\tUse more machine-friendly output \n");
352             printf("  -i, --inactive-time time\tSets inactive time in minutes, time must be between 0 and 90, 0 disables the feature.\n");
353             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");
354             printf("  -v, --voice-prompt 0|1\tTurn voice prompts on or off (0 = off, 1 = on)\n");
355             printf("  -r, --rotate-to-mute 0|1\tTurn rotate to mute feature on or off (0 = off, 1 = on)\n");
356             printf("  -?, --capabilities\t\tPrint every feature headsetcontrol supports of the connected headset\n");
357             printf("\n");
358             printf("  -u\tOutputs udev rules to stdout/console\n");
359 
360             printf("\n");
361             return 0;
362         case 0:
363             if (strcmp(opts[option_index].name, "dev") == 0) {
364                 dev_mode = 1;
365                 break;
366             }
367         default:
368             printf("Invalid argument %c\n", c);
369             return 1;
370         }
371     }
372 
373     if (dev_mode) {
374         // use +1 to make sure the first parameter is some previous argument (which normally would be the name of the program)
375         return dev_main(argc - optind + 1, &argv[optind - 1]);
376     } else {
377         for (int index = optind; index < argc; index++)
378             fprintf(stderr, "Non-option argument %s\n", argv[index]);
379     }
380 
381     // describes the headsetcontrol device, when a headset was found
382     static struct device device_found;
383 
384     // Look for a supported device
385     int headset_available = find_device(&device_found);
386     if (headset_available != 0) {
387         fprintf(stderr, "No supported headset found\n");
388         return 1;
389     }
390 
391     // We open connection to HID devices on demand
392     hid_device* device_handle = NULL;
393     char* hid_path            = NULL;
394 
395     // Set all features the user wants us to set
396 
397     if (sidetone_loudness != -1) {
398         if (handle_feature(&device_found, &device_handle, &hid_path, CAP_SIDETONE, sidetone_loudness) != 0)
399             goto error;
400     }
401 
402     if (lights != -1) {
403         if (handle_feature(&device_found, &device_handle, &hid_path, CAP_LIGHTS, lights) != 0)
404             goto error;
405     }
406 
407     if (notification_sound != -1) {
408         if (handle_feature(&device_found, &device_handle, &hid_path, CAP_NOTIFICATION_SOUND, notification_sound) != 0)
409             goto error;
410     }
411 
412     if (request_battery == 1) {
413         if (handle_feature(&device_found, &device_handle, &hid_path, CAP_BATTERY_STATUS, request_battery) != 0)
414             goto error;
415     }
416 
417     if (inactive_time != -1) {
418         if (handle_feature(&device_found, &device_handle, &hid_path, CAP_INACTIVE_TIME, inactive_time) != 0)
419             goto error;
420     }
421 
422     if (request_chatmix == 1) {
423         if (handle_feature(&device_found, &device_handle, &hid_path, CAP_CHATMIX_STATUS, request_chatmix) != 0)
424             goto error;
425     }
426 
427     if (voice_prompts != -1) {
428         if (handle_feature(&device_found, &device_handle, &hid_path, CAP_VOICE_PROMPTS, voice_prompts) != 0)
429             goto error;
430     }
431 
432     if (rotate_to_mute != -1) {
433         if (handle_feature(&device_found, &device_handle, &hid_path, CAP_ROTATE_TO_MUTE, rotate_to_mute) != 0)
434             goto error;
435     }
436 
437     if (print_capabilities != -1) {
438         PRINT_INFO("Supported capabilities:\n\n");
439 
440         // go through all enum capabilities
441         for (int i = 0; i < NUM_CAPABILITIES; i++) {
442             // When the capability i is included in .capabilities
443             if ((device_found.capabilities & B(i)) == B(i)) {
444                 if (short_output) {
445                     printf("%c", capabilities_str_short[i]);
446                 } else {
447                     printf("* %s\n", capabilities_str[i]);
448                 }
449             }
450         }
451     }
452 
453     if (argc <= 1) {
454         printf("You didn't set any arguments, so nothing happened.\nType %s -h for help.\n", argv[0]);
455     }
456 
457     terminate_hid(&device_handle, &hid_path);
458 
459     return 0;
460 
461 error:
462     terminate_hid(&device_handle, &hid_path);
463     return 1;
464 }
465