diff --git a/DAEMON.md b/DAEMON.md index 0c72b315c8..fe8c5d251f 100644 --- a/DAEMON.md +++ b/DAEMON.md @@ -128,6 +128,37 @@ Macros are a more advanced form of key binding, controlled with the `macro` comm Assigning a macro to a key will cause its binding to be ignored; for instance, `macro a:+b,-b` will cause A to generate a B character regardless of its binding. However, `macro lctrl+a:+b,-b` will cause A to generate a B only when Ctrl is also held down. +### Macro playback delay + +There are two types of playback delay that can be set with macros; global and local. Setting a _global delay_ value introduces a time delay between events during macro execution or playback. _Local delay_ allows setting the delay after an individual event, overriding the global delay value for that event. Thus global delay can be used to set the overall playback speed of macros and local delays can be used to tune individual events within a macro. + +All delay values are specified in microseconds (us) and are positive values from `0` to `UINT_MAX - 1`. This means delays range from 0 to just over 1 hour (4,294,967,294us, 4,294 seconds, 71 minutes, or 1.19 hours). A value of zero (0) represents no delay between actions. + +#### Global macro delay (default delay) + +Global delay allows macro playback speed to be changed. It sets the time between (actually after) each recorded macro event. If global delay is set to 1 microsecond then a 1 ms delay will follow each individual macro event when the macro is triggered. + +The _global delay_ is set with the ckb-daemon's existing (in testing branch) `delay` command followed by an unsigned integer representing the number of microseconds to wait after each macro action and before the next. + +Global delay can also be set to `on` which maintains backwards compatibility with the current development of `ckb-daemon` for long macro playback. That is, setting the global delay to `on` introduces a 30us and a 100us delay based on the macro's length during playback. + +**NOTE**: This setting also introduces a delay after the last macro action. This functionality exists in the current testing branch and was left as-is. It is still to be determined if this is a bug or a feature. + +**Examples:** +* `delay 1000` sets a 1,000us delay between action playback. +* `delay on` sets long macro delay; 30us for actions between 20 and 200, 100us for actions > 200. +* `delay off` sets no delay (same as 0). +* `delay 0` sets no delay (same as off). +* `delay spearmint-potato` is invalid input, sets no delay (same as off). + +#### Local macro delay (keystroke delay) + +Local Delay allows each macro action to have a post-action delay associated with it. This allows a macro to vary it's playback speed for each event. If no local delay is specified for a macro action, then the global `delay` (above) is used. All delay values are in microsecods (us) as with the global delay setting. + +***Examples:*** +* `macro g5:+d,-d,+e=5000,-e,+l,-l=10000,+a,-a,+y,-y=1000000,+enter,-enter` define a macro for `g5` with a 5,000us delay between the `e` down and `e` up actions. A 1,000us delay between `l` up and `a` down, a delay of one second (1,000,000us) after `y` up and before `enter`, and the global delay for all other actions. +* `macro g5:+d,-d=0` use default delay between `d` down and `d` up and no delay (0us) after `d` up. This removes the noted feature/bug (above) where the last action has a trailing delay associated with it. + DPI and mouse settings ---------------------- diff --git a/README.md b/README.md index 2d2024e84a..433750460e 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,8 @@ The user interface is still a work in progress. - Reactive lighting - Multiple profiles/modes with hardware save function - Adjustable mouse DPI with ability to change DPI on button press +- Key macros (G-Keys also); Have a look at https://youtu.be/qhrKP03_NrM for a short video tutorial +- Key macro delays: Handle delays between keystrokes when playing a macro Closing ckb will actually minimize it to the system tray. Use the Quit option from the tray icon or the settings screen to exit the application. @@ -169,7 +171,6 @@ Closing ckb will actually minimize it to the system tray. Use the Quit option fr - **v0.3 release:** - Ability to store profiles separately from devices, import/export them - More functions for the Win Lock key -- Key macros - **v0.4 release:** - Ability to import CUE profiles - Ability to tie profiles to which application has focus diff --git a/VERSION b/VERSION index cf8571a1b4..177515bab9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -beta-v0.2.7 +beta-v0.2.8 diff --git a/ckb.pro b/ckb.pro index 28ec6063ec..210f9621d5 100644 --- a/ckb.pro +++ b/ckb.pro @@ -1,5 +1,5 @@ -lessThan(QT_VERSION, 5.0) { - error("ckb requires at least Qt 5.0!") +lessThan(QT_VERSION, 5.2) { + error("ckb requires at least Qt 5.2!") } TEMPLATE = subdirs diff --git a/migmac.sh b/migmac.sh new file mode 100755 index 0000000000..17c2870ab3 --- /dev/null +++ b/migmac.sh @@ -0,0 +1,48 @@ +#!/bin/bash +SAVEFILE=~/.config/ckb/ckb.conf + +################### +# +# Change the format of macro definitons from 4- to 5-parts. +# +# + +# Find a line, beginning with '$macro:', +# followed by exact 3 colon-separated strings. +# The may not be a 4th string (it would be the new format). +# If found, add a ':x' at the end. +# Otherwise just print it. +SEDLINE='/$macro:[^:]*:[^:]*:[^:]*$/s/$/:x/' + +cp $SAVEFILE $SAVEFILE.orig + +# some statistics +echo -n "how many \$macro:-Statements to deal with: " +grep "$macro:" $SAVEFILE | wc -l + +echo -n "That number entries do we find: " +sed -e $SEDLINE $SAVEFILE | grep "$macro:" | wc -l + +echo -n "how many lines without \$macro:-statements: " +grep -v "$macro:" $SAVEFILE | wc -l + +echo -n "That many entries are ignored: " +sed -e $SEDLINE $SAVEFILE | grep -v "$macro:" | wc -l + +read -p "Is the information correct? [y|n] " RL +RL=${RL:-n} + +if [[ ${RL} != "y" ]]; then + echo you typed $RL. + echo "Aborted." + exit -1; +fi + +echo you typed $RL. +sed -e $SEDLINE $SAVEFILE > $SAVEFILE.new +rm -f $SAVEFILE +mv $SAVEFILE.new $SAVEFILE +wc $SAVEFILE +grep :x$ $SAVEFILE + +exit 0 diff --git a/src/ckb-daemon/command.c b/src/ckb-daemon/command.c index 1da35d4a57..87a8c12672 100644 --- a/src/ckb-daemon/command.c +++ b/src/ckb-daemon/command.c @@ -200,18 +200,28 @@ int readcmd(usbdevice* kb, const char* line){ } continue; } - case DELAY: - kb->delay = (!strcmp (word, "on")); // independendant from parameter to handle false commands like "delay off" + case DELAY: { + long int delay; + if(sscanf(word, "%ld", &delay) == 1 && 0 <= delay && delay < UINT_MAX) { + // Add delay of `newdelay` microseconds to macro playback + kb->delay = (unsigned int)delay; + } else if(strcmp(word, "on") == 0) { + // allow previous syntax, `delay on` means use old `long macro delay` + kb->delay = UINT_MAX; + } else { + // bad parameter to handle false commands like "delay off" + kb->delay = 0; // No delay. + } continue; + } case RESTART: { char mybuffer[] = "no reason specified"; - if (sscanf(line, " %[^\n]", word) == -1) { ///> Because length of word is length of line + 1, there should be no problem with buffer overflow. + if (sscanf(line, " %[^\n]", word) == -1) { ///< Because length of word is length of line + 1, there should be no problem with buffer overflow. word = mybuffer; } vt->do_cmd[command](kb, mode, notifynumber, 0, word); continue; } - default:; } diff --git a/src/ckb-daemon/device.c b/src/ckb-daemon/device.c index e1c07f5496..39be88eae6 100644 --- a/src/ckb-daemon/device.c +++ b/src/ckb-daemon/device.c @@ -4,17 +4,34 @@ #include "profile.h" #include "usb.h" -int hwload_mode = 1; +int hwload_mode = 1; ///< hwload_mode = 1 means read hardware once. should be enough // Device list -usbdevice keyboard[DEV_MAX]; +usbdevice keyboard[DEV_MAX]; ///< remember all usb devices. Needed for closeusb(). pthread_mutex_t devlistmutex = PTHREAD_MUTEX_INITIALIZER; -pthread_mutex_t devmutex[DEV_MAX] = { [0 ... DEV_MAX-1] = PTHREAD_MUTEX_INITIALIZER }; -pthread_mutex_t inputmutex[DEV_MAX] = { [0 ... DEV_MAX-1] = PTHREAD_MUTEX_INITIALIZER }; +pthread_mutex_t devmutex[DEV_MAX] = { [0 ... DEV_MAX-1] = PTHREAD_MUTEX_INITIALIZER }; ///< Mutex for handling the usbdevice structure +pthread_mutex_t inputmutex[DEV_MAX] = { [0 ... DEV_MAX-1] = PTHREAD_MUTEX_INITIALIZER }; ///< Mutex for dealing with usb input frames +pthread_mutex_t macromutex[DEV_MAX] = { [0 ... DEV_MAX-1] = PTHREAD_MUTEX_INITIALIZER }; ///< Protecting macros against lightning: Both use usb_send +pthread_mutex_t macromutex2[DEV_MAX] = { [0 ... DEV_MAX-1] = PTHREAD_MUTEX_INITIALIZER }; ///< Protecting the single link list of threads and the macrovar +pthread_cond_t macrovar[DEV_MAX] = { [0 ... DEV_MAX-1] = PTHREAD_COND_INITIALIZER }; ///< This variable is used to stop and wakeup all macro threads which have to wait. +/// \brief . +/// +/// \brief _start_dev get fw-info and pollrate; if available, install new firmware; get all hardware profiles. +/// \param kb the normal kb pointer to the usbdevice. Is also valid for mice. +/// \param makeactive if set to 1, activate the device via setactive() +/// \return 0 if success, other else +/// int _start_dev(usbdevice* kb, int makeactive){ // Get the firmware version from the device if(kb->pollrate == 0){ + /// + /// - This hacker code is tricky in mutliple aspects. What it means is: + /// \n if hwload_mode == 0: just set pollrate to 0 and clear features in the bottom lines of the if-block. + /// \n if hwload_mode == 1: if the device has FEAT_HWLOAD active, call getfwversion(). If it returns true, there was an error while detecting fw-version. Put error message, reset FEAT_HWLOAD and finalize as above. + /// \n if hwload_mode == 2: if the device has FEAT_HWLOAD active, call getfwversion(). If it returns true, there was an error while detecting fw-version. Put error message and return directly from function with error. + /// \n Why do not you just write it down? + /// if(!hwload_mode || (HAS_FEATURES(kb, FEAT_HWLOAD) && getfwversion(kb))){ if(hwload_mode == 2) // hwload=always. Report setup failure. @@ -30,14 +47,22 @@ int _start_dev(usbdevice* kb, int makeactive){ kb->features &= ~(FEAT_FWVERSION | FEAT_FWUPDATE); } } + /// + /// - Now check if device needs a firmware update. + /// If so, set it up and leave the function without error. + /// if(NEEDS_FW_UPDATE(kb)){ - // Device needs a firmware update. Finish setting up but don't do anything. + /// - Device needs a firmware update. Finish setting up but don't do anything. ckb_info("Device needs a firmware update. Please issue a fwupdate command.\n"); kb->features = FEAT_RGB | FEAT_FWVERSION | FEAT_FWUPDATE; kb->active = 1; return 0; } - // Load profile from device + /// + /// - Load profile from device if the hw-pointer is not set yet and hw-loading is possible and allowed. + /// \n return error if mode == 2 (load always) and loading got an error. Else reset HWLOAD feature, because hwload must be 1. + /// \n\n That is real Horror code. + /// if(!kb->hw && hwload_mode && HAS_FEATURES(kb, FEAT_HWLOAD)){ if(hwloadprofile(kb, 1)){ if(hwload_mode == 2) diff --git a/src/ckb-daemon/device.h b/src/ckb-daemon/device.h index 2118bb1650..e6b3b60218 100644 --- a/src/ckb-daemon/device.h +++ b/src/ckb-daemon/device.h @@ -21,6 +21,14 @@ extern pthread_mutex_t devmutex[DEV_MAX]; extern pthread_mutex_t inputmutex[DEV_MAX]; #define imutex(kb) (inputmutex + INDEX_OF(kb, keyboard)) +// Needed to synchronize sending macro-keys to the os and sending color info to the device +extern pthread_mutex_t macromutex[DEV_MAX]; +#define mmutex(kb) (macromutex + INDEX_OF(kb, keyboard)) +extern pthread_mutex_t macromutex2[DEV_MAX]; +#define mmutex2(kb) (macromutex2 + INDEX_OF(kb, keyboard)) +extern pthread_cond_t macrovar[DEV_MAX]; +#define mvar(kb) (macrovar + INDEX_OF(kb, keyboard)) + // Sets up device hardware, after software initialization is finished. Also used during resets // Should be called only from setupusb/resetusb int start_dev(usbdevice* kb, int makeactive); @@ -29,6 +37,10 @@ int start_kb_nrgb(usbdevice* kb, int makeactive); // Activates/deactives software control on a keyboard. Return 0 on success int setactive_kb(usbdevice* kb, int active); int setactive_mouse(usbdevice* kb, int active); +/// +/// \brief setactive() calls via the corresponding kb->vtable either the active() or the idle() function. +/// \n active() is called if the parameter makeactive is true, idle if it is false. +/// \n What function is called effectively is device dependent. Have a look at \a device_vtable.c for more information. #define setactive(kb, makeactive) ((makeactive) ? (kb)->vtable->active((kb), 0, 0, 0, 0) : (kb)->vtable->idle((kb), 0, 0, 0, 0)) // Command: Activate a keyboard @@ -62,4 +74,5 @@ void setmodeindex_nrgb(usbdevice* kb, int index); #define ACT_M2 11 #define ACT_M3 12 +#include #endif // DEVICE_H diff --git a/src/ckb-daemon/device_vtable.c b/src/ckb-daemon/device_vtable.c index 4d1a4c2d50..862103d762 100644 --- a/src/ckb-daemon/device_vtable.c +++ b/src/ckb-daemon/device_vtable.c @@ -24,7 +24,8 @@ static int int1_int_none(usbdevice* kb, int dummy){ return 0; } -// RGB keyboard vtable +/// \brief RGB keyboard vtable holds functions for each device type. +/// const devcmd vtable_keyboard = { .hwload = cmd_hwload_kb, .hwsave = cmd_hwsave_kb, @@ -62,13 +63,13 @@ const devcmd vtable_keyboard = { .restart = cmd_restart, .start = start_dev, - .setmodeindex = int1_void_none, + .setmodeindex = int1_void_none, ///< is just for non rgb keyboards .allocprofile = allocprofile, .loadprofile = loadprofile, .freeprofile = freeprofile, .updatergb = updatergb_kb, .updateindicators = updateindicators_kb, - .updatedpi = int1_int_none + .updatedpi = int1_int_none ///< This is for mice only }; // Non-RGB keyboard vtable (K70) @@ -109,13 +110,13 @@ const devcmd vtable_keyboard_nonrgb = { .restart = cmd_restart, .start = start_kb_nrgb, - .setmodeindex = setmodeindex_nrgb, + .setmodeindex = setmodeindex_nrgb, ///< this is special for non RGBs .allocprofile = allocprofile, .loadprofile = loadprofile_none, .freeprofile = freeprofile, - .updatergb = int1_int_none, + .updatergb = int1_int_none, ///< non RGB keyboards do not have an rgb update function .updateindicators = updateindicators_kb, - .updatedpi = int1_int_none + .updatedpi = int1_int_none ///< This is for mice only }; // RGB mouse vtable @@ -156,11 +157,11 @@ const devcmd vtable_mouse = { .restart = cmd_restart, .start = start_dev, - .setmodeindex = int1_void_none, - .allocprofile = allocprofile, - .loadprofile = loadprofile, - .freeprofile = freeprofile, - .updatergb = updatergb_mouse, - .updateindicators = int1_void_none, - .updatedpi = updatedpi + .setmodeindex = int1_void_none, ///< Mice do not have different modes + .allocprofile = allocprofile, ///< same for all keyboards and mice + .loadprofile = loadprofile, ///< same for all keyboards and mice + .freeprofile = freeprofile, ///< same for all keyboards and mice + .updatergb = updatergb_mouse, ///< special for mice + .updateindicators = int1_void_none, ///< Mice do not have keyboard indicators like num + .updatedpi = updatedpi ///< special for mice }; diff --git a/src/ckb-daemon/devnode.c b/src/ckb-daemon/devnode.c index ce69263ee8..d5bfa6df25 100644 --- a/src/ckb-daemon/devnode.c +++ b/src/ckb-daemon/devnode.c @@ -35,6 +35,23 @@ int rm_recursive(const char* path){ return remove(path); } +/// +/// \brief _updateconnected Update the list of connected devices. +/// +/// \ normally is /dev/input/ckb or /input/ckb. +/// \n Open the normal file under \0/connected for writing. +/// For each device connected, print its devicepath+number, +/// the serial number of the usb device and the usb name of the device connected to that usb interface. +/// \n eg: +/// \n /dev/input/ckb1 0F022014ABABABABABABABABABABA999 Corsair K95 RGB Gaming Keyboard +/// \n /dev/input/ckb2 0D02303DBACBACBACBACBACBACBAC998 Corsair M65 RGB Gaming Mouse +/// +/// Set the file ownership to root. +/// If the glob var gid is explicitly set to something different from -1 (the initial value), set file permission to 640, else to 644. +/// This is used if you start the daemon with --gid=\ Parameter. +/// +/// Because several independent threads may call updateconnected(), protect that procedure with locking/unlocking of \b devmutex. +/// void _updateconnected(){ pthread_mutex_lock(devmutex); char cpath[strlen(devpath) + 12]; diff --git a/src/ckb-daemon/devnode.h b/src/ckb-daemon/devnode.h index fd8d320313..a0b3a9319c 100644 --- a/src/ckb-daemon/devnode.h +++ b/src/ckb-daemon/devnode.h @@ -4,10 +4,10 @@ #include "includes.h" #include "usb.h" -// Device path base ("/dev/input/ckb" or "/var/run/ckb") +/// Device path base ("/dev/input/ckb" or "/var/run/ckb") const char *const devpath; -// Group ID for the control nodes. -1 to give read/write access to everybody +/// Group ID for the control nodes. -1 to give read/write access to everybody extern long gid; // Simple file permissions @@ -17,22 +17,25 @@ extern long gid; #define S_CUSTOM (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP) #define S_CUSTOM_R (S_IRUSR | S_IWUSR | S_IRGRP) -// Update the list of connected devices. +/// Update the list of connected devices. void updateconnected(); -// Create a dev path for the keyboard at index. Returns 0 on success. + +/// Create a dev path for the keyboard at index. Returns 0 on success. int mkdevpath(usbdevice* kb); -// Remove the dev path for the keyboard at index. Returns 0 on success. + +/// Remove the dev path for the keyboard at index. Returns 0 on success. int rmdevpath(usbdevice* kb); -// Creates a notification node for the specified keyboard. +/// Creates a notification node for the specified keyboard. int mknotifynode(usbdevice* kb, int notify); -// Removes a notification node for the specified keyboard. + +/// Removes a notification node for the specified keyboard. int rmnotifynode(usbdevice* kb, int notify); -// Writes a keyboard's firmware version and poll rate to its device node. +/// Writes a keyboard's firmware version and poll rate to its device node. int mkfwnode(usbdevice* kb); -// Custom readline is needed for FIFOs. fopen()/getline() will die if the data is sent in too fast. +/// Custom readline is needed for FIFOs. fopen()/getline() will die if the data is sent in too fast. typedef struct _readlines_ctx* readlines_ctx; void readlines_ctx_init(readlines_ctx* ctx); void readlines_ctx_free(readlines_ctx ctx); diff --git a/src/ckb-daemon/includes.h b/src/ckb-daemon/includes.h index 1e29ae5065..f5074b6d55 100644 --- a/src/ckb-daemon/includes.h +++ b/src/ckb-daemon/includes.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #include diff --git a/src/ckb-daemon/input.c b/src/ckb-daemon/input.c index 78a5c3943c..be7d6e1d8b 100644 --- a/src/ckb-daemon/input.c +++ b/src/ckb-daemon/input.c @@ -12,39 +12,154 @@ int macromask(const uchar* key1, const uchar* key2){ return 1; } +/// +/// \brief pt_head is the head pointer for the single linked thread list managed by macro_pt_en/dequeue(). +static ptlist_t* pt_head = 0; +/// \brief pt_tail is the tail pointer for the single linked thread list managed by macro_pt_en/dequeue(). +static ptlist_t* pt_tail = 0; + +/// +/// \brief macro_pt_enqueue Save the new thread in the single linked list (FIFO). +/// \attention Becuase multiple threads may use this function in parallel, +/// save the critical section with a mutex. +/// +static void macro_pt_enqueue() { + ptlist_t* new_elem = malloc(sizeof(ptlist_t)); + if (!new_elem) { + perror("macro_pt_enqueue: "); + exit (-1); ///< exit on critical situation; \todo find a better exit strategy if no more mem available. + } + new_elem->next = 0; + new_elem->thread_id = pthread_self(); ///< The element knows its ID byself + if (pt_head == 0) { + pt_head = pt_tail = new_elem; ///< new list, first element + } else { + pt_tail->next = new_elem; ///< existing list, append on last element (FIFO) + pt_tail = new_elem; + } +} + +/// +/// \brief macro_pt_dequeue gets the first thread id of the list and returns the thread_id stored in it. +/// \return the ptread_id of the first element. If list is empty, return 0. +/// \attention Becuase multiple threads may use this function in parallel, +/// save the critical section with a mutex. +/// +static pthread_t macro_pt_dequeue() { + pthread_t retval = 0; + ptlist_t* elem = 0; + if (pt_head == 0 && pt_tail == 0) { + ckb_err("macro_pt_dequeue: called on empty list.\n"); + return 0; ///< why are we called? + } + elem = pt_head; + pt_head = pt_head->next; + if (pt_head == 0) pt_tail = 0; ///< Was last element in the list, so clear tail. + retval = elem->thread_id; ///< save the return value before deleting element + free(elem); + return retval; +} + +/// +/// \brief macro_pt_first returns the first pthread_id but does not remove the first entry. +/// \return the pthread_id of the first element in the list or 0 if list is empty. +/// \attention Becuase multiple threads may use this function in parallel, +/// save the critical section with a mutex (avoid NIL-ptr) +/// +static pthread_t macro_pt_first() { + return pt_head? pt_head->thread_id : 0; +} + +/// +/// \brief play_macro is the code for all threads started to play a macro. +/// \param param \a parameter_t to store Kb-ptr and macro-ptr (thread may get only one user-parameter) +/// \return 0 on success, -1 else (no one is interested in it except the kernel...) +/// +static void* play_macro(void* param) { + parameter_t* ptr = (parameter_t*) param; + usbdevice* kb = ptr->kb; + keymacro* macro = ptr->macro; + + /// First have a look if we are the first and only macro-thread to run. If not, wait. + /// So enqueue our thread first, so it is remembered for us and can be seen by all others. + pthread_mutex_lock(mmutex2(kb)); + macro_pt_enqueue(); + // ckb_info("Entering critical section with 0x%lx. Queue head is 0x%lx\n", (unsigned long int)pthread_self(), (unsigned long int)macro_pt_first()); + while (macro_pt_first() != pthread_self()) { ///< If the first thread in the list is not our, another one is running + // ckb_info("Now waiting with 0x%lx because of 0x%lx\n", (unsigned long int)pthread_self(), (unsigned long int)macro_pt_first()); + pthread_cond_wait(mvar(kb), mmutex2(kb)); + // ckb_info("Waking up with 0x%lx\n", (unsigned long int)pthread_self()); + } + pthread_mutex_unlock(mmutex2(kb)); ///< Give all new threads the chance to enter the block. + + /// Send events for each keypress in the macro + pthread_mutex_lock(mmutex(kb)); ///< Synchonization between macro output and color information + for (int a = 0; a < macro->actioncount; a++) { + macroaction* action = macro->actions + a; + if (action->rel_x != 0 || action->rel_y != 0) + os_mousemove(kb, action->rel_x, action->rel_y); + else { + os_keypress(kb, action->scan, action->down); + pthread_mutex_unlock(mmutex(kb)); ///< use this unlock / relock for enablling the parallel running colorization + if (action->delay != UINT_MAX) { ///< local delay set + usleep(action->delay); + } else if (kb->delay != UINT_MAX) { ///< use default global delay + usleep(kb->delay); + } else if (a < (macro->actioncount - 1)) { ///< use delays depending on macro length + if (a > 200) { + usleep (100); + } else if (a > 20) { + usleep(30); + } + } + pthread_mutex_lock(mmutex(kb)); + } + } + + pthread_mutex_lock(mmutex2(kb)); ///< protect the linked list and the mvar + // ckb_info("Now leaving 0x%lx and waking up all others\n", (unsigned long int)pthread_self()); + macro_pt_dequeue(); + pthread_cond_broadcast(mvar(kb)); ///< Wake up all waiting threads + pthread_mutex_unlock(mmutex2(kb)); ///< for the linked list and the mvar + + pthread_mutex_unlock(mmutex(kb)); ///< Sync keyboard input/output and colorization + return 0; +} + +/// +/// \brief inputupdate_keys Handle input from Keyboard or mouse; start Macrof if detected. +/// \param kb +/// static void inputupdate_keys(usbdevice* kb){ usbmode* mode = kb->profile->currentmode; binding* bind = &mode->bind; usbinput* input = &kb->input; + // Don't do anything if the state hasn't changed if(!memcmp(input->prevkeys, input->keys, N_KEYBYTES_INPUT)) return; // Look for macros matching the current state - int macrotrigger = 0; - if(kb->active){ - for(int i = 0; i < bind->macrocount; i++){ + if (kb->active) { + for (int i = 0; i < bind->macrocount; i++) { keymacro* macro = &bind->macros[i]; - if(macromask(input->keys, macro->combo)){ - if(!macro->triggered){ - macrotrigger = 1; - macro->triggered = 1; - // Send events for each keypress in the macro - for(int a = 0; a < macro->actioncount; a++){ - macroaction* action = macro->actions + a; - if(action->rel_x != 0 || action->rel_y != 0) - os_mousemove(kb, action->rel_x, action->rel_y); - else { - os_keypress(kb, action->scan, action->down); - if (kb->delay) { - if (a > 200) usleep (100); - else if (a > 20) usleep(30); - } + if (macromask(input->keys, macro->combo)) { + if (!macro->triggered) { + parameter_t* params = malloc(sizeof(parameter_t)); + if (params == 0) { + perror("inputupdate_keys got no more mem:"); + } else { + pthread_t thread = 0; + params->kb = kb; + params->macro = macro; + int retval = pthread_create(&thread, 0, play_macro, (void*)params); + if (retval) { + perror("inputupdate_keys: Creating thread returned not null"); + } else { + macro->triggered = 1; } } } - } else { - macro->triggered = 0; - } + } else macro->triggered = 0; } } // Make a list of keycodes to send. Rearrange them so that modifier keydowns always come first @@ -68,8 +183,8 @@ static void inputupdate_keys(usbdevice* kb){ char old = oldb & mask, new = newb & mask; // If the key state changed, send it to the input device if(old != new){ - // Don't echo a key press if a macro was triggered or if there's no scancode associated - if(!macrotrigger && !(scancode & SCAN_SILENT)){ + // Don't echo a key press if there's no scancode associated + if(!(scancode & SCAN_SILENT)){ if(IS_MOD(scancode)){ if(new){ // Modifier down: Add to the end of modifier keys @@ -111,11 +226,14 @@ static void inputupdate_keys(usbdevice* kb){ } } } - // Process all queued keypresses - int totalkeys = modcount + keycount + rmodcount; - for(int i = 0; i < totalkeys; i++){ - int scancode = events[i]; - os_keypress(kb, (scancode < 0 ? -scancode : scancode) - 1, scancode > 0); + /// Process all queued keypresses if no macro is running yet. + /// \todo If we want to get all keys typed while a macro is played, add the code for it here. + if (!macro_pt_first()) { + int totalkeys = modcount + keycount + rmodcount; + for(int i = 0; i < totalkeys; i++){ + int scancode = events[i]; + os_keypress(kb, (scancode < 0 ? -scancode : scancode) - 1, scancode > 0); + } } } @@ -241,7 +359,7 @@ static void _cmd_macro(usbmode* mode, const char* keys, const char* assignment){ int empty = 1; int left = strlen(keys), right = strlen(assignment); int position = 0, field = 0; - char keyname[12]; + char keyname[24]; while(position < left && sscanf(keys + position, "%10[^+]%n", keyname, &field) == 1){ int keycode; if((sscanf(keyname, "#%d", &keycode) && keycode >= 0 && keycode < N_KEYS_INPUT) @@ -276,9 +394,23 @@ static void _cmd_macro(usbmode* mode, const char* keys, const char* assignment){ // Scan the actions position = 0; field = 0; - while(position < right && sscanf(assignment + position, "%11[^,]%n", keyname, &field) == 1){ + // max action = old 11 chars plus 12 chars which is the max 32-bit int 4294967295 size + while(position < right && sscanf(assignment + position, "%23[^,]%n", keyname, &field) == 1){ if(!strcmp(keyname, "clear")) break; + + // Check for local key delay of the form '[+-]=' + long int long_delay; // scanned delay value, used to keep delay in range. + unsigned int delay = UINT_MAX; // computed delay value. UINT_MAX means use global delay value. + char real_keyname[12]; // temp to hold the left side (key) of the = + int scan_matches = sscanf(keyname, "%11[^=]=%ld", real_keyname, &long_delay); + if (scan_matches == 2) { + if (0 <= long_delay && long_delay < UINT_MAX) { + delay = (unsigned int)long_delay; + strcpy(keyname, real_keyname); // keyname[24], real_keyname[12] + } + } + int down = (keyname[0] == '+'); if(down || keyname[0] == '-'){ int keycode; @@ -287,6 +419,7 @@ static void _cmd_macro(usbmode* mode, const char* keys, const char* assignment){ // Set a key numerically macro.actions[macro.actioncount].scan = keymap[keycode].scan; macro.actions[macro.actioncount].down = down; + macro.actions[macro.actioncount].delay = delay; macro.actioncount++; } else { // Find this key in the keymap @@ -294,6 +427,7 @@ static void _cmd_macro(usbmode* mode, const char* keys, const char* assignment){ if(keymap[i].name && !strcmp(keyname + 1, keymap[i].name)){ macro.actions[macro.actioncount].scan = keymap[i].scan; macro.actions[macro.actioncount].down = down; + macro.actions[macro.actioncount].delay = delay; macro.actioncount++; break; } diff --git a/src/ckb-daemon/input.h b/src/ckb-daemon/input.h index 7625df5b30..55ef137d07 100644 --- a/src/ckb-daemon/input.h +++ b/src/ckb-daemon/input.h @@ -47,5 +47,21 @@ void os_mousemove(usbdevice* kb, int x, int y); // Perform OS-specific setup for indicator lights. Called when the device is created. Return 0 on success. int os_setupindicators(usbdevice* kb); +/// +/// \brief struct parameter contains the values for a fresh started macro_play thread. +/// \a parameter_t is the typedef for it. +/// +typedef struct parameter { + usbdevice* kb; + keymacro* macro; +} parameter_t; + +/// \brief struct ptlist is one element in the single linked list to store macro_play threads waiting for their execution +/// \a ptlist_t is the typedef for it. +/// +typedef struct ptlist { + struct ptlist* next; + pthread_t thread_id; +} ptlist_t; #endif // INPUT_H diff --git a/src/ckb-daemon/input_linux.c b/src/ckb-daemon/input_linux.c index b42347e3ff..20e6d39350 100644 --- a/src/ckb-daemon/input_linux.c +++ b/src/ckb-daemon/input_linux.c @@ -46,6 +46,12 @@ int uinputopen(struct uinput_user_dev* indev, int mouse){ return fd + 1; } +/// +/// \brief os_inputopen +/// \param kb +/// \return +/// +/// Some tips on using [uinput_user_dev in](http://thiemonge.org/getting-started-with-uinput) int os_inputopen(usbdevice* kb){ // Create the new input device int index = INDEX_OF(kb, keyboard); @@ -161,8 +167,8 @@ void* _ledthread(void* ctx){ uchar ileds = 0; // Read LED events from the uinput device struct input_event event; - while(read(kb->uinput_kb - 1, &event, sizeof(event)) > 0){ - if(event.type == EV_LED && event.code < 8){ + while (read(kb->uinput_kb - 1, &event, sizeof(event)) > 0) { + if (event.type == EV_LED && event.code < 8){ char which = 1 << event.code; if(event.value) ileds |= which; diff --git a/src/ckb-daemon/led_keyboard.c b/src/ckb-daemon/led_keyboard.c index b1383f22c6..f327a03b4c 100644 --- a/src/ckb-daemon/led_keyboard.c +++ b/src/ckb-daemon/led_keyboard.c @@ -201,9 +201,11 @@ int loadrgb_kb(usbdevice* kb, lighting* light, int mode){ { 0xff, 0x03, 24, 0 }, }; - /// Since Firmware Version 2.05 the answers for getting the stored color-maps from the hardware + /// Since Firmware Version 2.05 for K95RGB the answers for getting the stored color-maps from the hardware /// has changed a bit. So comparing for the correct answer cannot validate against the cmd, /// and has to be done against a third map. + /// Up to now we know, that K70RGB Pro and K70 Lux RGB have firmware version 2.04 and having the problem also. + /// So we have to determine in the most inner loop the firmware version and type of KB to select the correct compare-table. uchar cmp_pkt[4][4] = { { 0x0e, 0x14, 0x03, 0x01 }, @@ -211,21 +213,28 @@ int loadrgb_kb(usbdevice* kb, lighting* light, int mode){ { 0x0e, 0xff, 0x02, 60 }, { 0x0e, 0xff, 0x03, 24 }, }; - // Read colors + /// Read colors uchar* colors[3] = { light->r, light->g, light->b }; for(int clr = 0; clr < 3; clr++){ for(int i = 0; i < 4; i++){ if(!usbrecv(kb, data_pkt[i + clr * 4], in_pkt[i])) return -1; - // Make sure the first four bytes match - // see comment above - // if(memcmp(p, data_pkt[i + clr * 4], 4)){ - if (memcmp(in_pkt[i], (kb->fwversion >= 0x0205)? cmp_pkt[i] : data_pkt[i + clr * 4], 4)) { + + uchar* comparePacket = data_pkt[i + clr * 4]; ///< That is the old comparison method: you get back what you sent. + /// Normally a firmware version >= 2.05 runs with the new compare array. + /// Up to now there is a 2.04 running in K70 RGB Lux with the same behavior. + /// It seems that K70RGB has the same problem + if ((kb->fwversion >= 0x205) + || ((kb->fwversion >= 0x204) + && ((kb->product == P_K70_LUX_NRGB) || (kb->product == P_K70_LUX)))) { + comparePacket = cmp_pkt[i]; + } + + if (memcmp(in_pkt[i], comparePacket, 4)) { ckb_err("Bad input header\n"); - ckb_err("color = %d, i = %d, mode = %d\nInput(Antwort): %2.2x %2.2x %2.2x %2.2x %2.2x %2.2x %2.2x %2.2x\nOutput (Frage): %2.2x %2.2x %2.2x %2.2x\n", clr, i, mode, - in_pkt[i][0], in_pkt[i][1], in_pkt[i][2], in_pkt[i][3], in_pkt[i][4], in_pkt[i][5], in_pkt[i][6], in_pkt[i][7], - // data_pkt[i + clr * 4][0], data_pkt[i + clr * 4 ][1], data_pkt[i + clr * 4 ][2], data_pkt[i + clr * 4 ][3]); - cmp_pkt[i][0], cmp_pkt[i][1], cmp_pkt[i][2], cmp_pkt[i][3]); + ckb_err("color = %d, i = %d, mode = %d\nOutput (Request): %2.2x %2.2x %2.2x %2.2x\nInput(Reply): %2.2x %2.2x %2.2x %2.2x %2.2x %2.2x %2.2x %2.2x\n", clr, i, mode, + comparePacket[0], comparePacket[1], comparePacket[2], comparePacket[3], + in_pkt[i][0], in_pkt[i][1], in_pkt[i][2], in_pkt[i][3], in_pkt[i][4], in_pkt[i][5], in_pkt[i][6], in_pkt[i][7]); in_pkt[2][0] = 0x99; in_pkt[2][1] = 0x99; in_pkt[2][2] = 0x99; diff --git a/src/ckb-daemon/structures.h b/src/ckb-daemon/structures.h index 07a9e0fc6c..4a38920440 100644 --- a/src/ckb-daemon/structures.h +++ b/src/ckb-daemon/structures.h @@ -28,6 +28,7 @@ typedef struct { short scan; // Key scancode, OR short rel_x, rel_y; // Mouse movement char down; // 0 for keyup, 1 for keydown (ignored if rel_x != 0 || rel_y != 0) + uint delay; // us delay after action; UINT_MAX for use global delay } macroaction; // Key macro @@ -246,8 +247,8 @@ typedef struct { uchar hw_ileds, hw_ileds_old, ileds; // Color dithering in use char dither; - // Flag to check, if large macros should be sent delayed - char delay; + // Flag to check if large macros should be sent delayed + uint delay; } usbdevice; #endif // STRUCTURES_H diff --git a/src/ckb-daemon/usb.c b/src/ckb-daemon/usb.c index 0f93193f49..45051fe865 100644 --- a/src/ckb-daemon/usb.c +++ b/src/ckb-daemon/usb.c @@ -8,21 +8,65 @@ #include "profile.h" #include "usb.h" +/// brief . +/// +/// \brief usbmutex is a never referenced mutex! +/// +/// \todo We should have a look why this mutex is never used. +/// pthread_mutex_t usbmutex = PTHREAD_MUTEX_INITIALIZER; -// Reset stopper for when the program shuts down +/// brief . +/// +/// \brief reset_stop is boolean: Reset stopper for when the program shuts down. +/// +/// Is set only by \a quit() to true (1) +/// to inform several usb_* functions to end their loops and tries. volatile int reset_stop = 0; -// Mask of features to exclude from all devices +/// brief . +/// +/// \brief features_mask Mask of features to exclude from all devices +/// +/// That bit mask ist set to enable all (-1). +/// When interpreting the input parameters, some of these bits can be cleared. +/// \n At the moment binding, notifying and mouse-acceleration can be disabled via command line. +/// \n Have a look at \a main() in main.c for details. int features_mask = -1; -// Vendor/product string representations +/// brief . +/// +/// vendor_str returns "corsair" iff the given \a vendor argument is equal to \a V_CORSAIR \c (0x1bc) +/// else it returns "" +/// +/// \attention There is also a string defined V_CORSAIR_STR, which returns the device number as string in hex "1b1c". const char* vendor_str(short vendor){ if(vendor == V_CORSAIR) return "corsair"; return ""; } +/// brief . +/// +/// At present, various models and their properties are known from corsair products. +/// Some models differ in principle (mice and keyboards), +/// others differ in the way they function (for example, RGB and non RGB), but they are very similar. +/// +/// Here, only the first point is taken into consideration and we return a unified model string. +/// If the model is not known with its number, \a product_str returns an empty string. +/// +/// The model numbers and corresponding strings wwith the numbers in hex-string are defined in \c usb.h +/// +/// At present, this function is used to initialize \c kb->name and to give information in debug strings. +/// +/// \attention The combinations below have to fit to the combinations in the macros mentioned above. +/// So if you add a device with a new number, change both. +/// +/// \todo There are macros defined in usb.h to detect all the combinations below. +/// the only difference is the parameter: +/// The macros need the \a kb*, +/// product_str() needs the \a product \a ID +/// const char* product_str(short product){ if(product == P_K95 || product == P_K95_NRGB || product == P_K95_PLATINUM) return "k95"; @@ -41,18 +85,71 @@ const char* product_str(short product){ return ""; } -// Vtable selector +/// brief . +/// +/// \brief get_vtable returns the correct vtable pointer +/// \param vendor short usb vendor ID +/// \param product short usb product ID +/// \return Depending on the type and model, the corresponding vtable pointer is returned (see below) +/// +/// At present, we have three different vtables: +/// - \c vtable_mouse is used for all mouse types. This may be wrong with some newer mice? +/// - \c vtable_keyboard is used for all RGB Keyboards. +/// - \c vtable_keyboard_nonrgb for all the rest. +/// +/// \todo Is the last point really a good decision and always correct? +/// static const devcmd* get_vtable(short vendor, short product){ return IS_MOUSE(vendor, product) ? &vtable_mouse : IS_RGB(vendor, product) ? &vtable_keyboard : &vtable_keyboard_nonrgb; } // USB device main loop +/// brief . +/// +/// \brief devmain is called by _setupusb +/// \param kb the pointer to the device. Even if it has the name kb, it is valid also for a mouse (the whole driver seems to be implemented first for a keyboard). +/// \return always a nullptr +/// +/// # Synchronization +/// The syncing via mutexes is interesting: +/// 1. \a imutex (the Input mutex)\n +/// This one is locked in \c setupusb(). +/// That function does only two things: Locking the mutex and trying to start a thread at \c _setupusb(). +/// _setupusb() unlocks \a imutex after getting some buffers and initalizing internal structures from the indicators +/// (this function often gets problems with error messages like "unable to read indicators" or "Timeout bla blubb"). +/// \warning have a look at \a updateindicators() later. +/// \warning if creating the thread is not successful, the \a imutex remains blocked. Have a look at setupusb() later. +/// +/// 2. \a dmutex (the Device mutex)\n +/// This one is very interesting, because it is handled in devmain(). +/// It seems that it is locked only in \a _ledthread(), which is a thread created in \a os_setupindicators(). +/// os_setupindicators() again is called in \a _setupusb() long before calling devmain(). +/// So this mutex is locked when we start the function as the old comment says.\n +/// Before reading from the FIFO and direct afterwards an unlock..lock sequence is implemented here. +/// Even if only the function readlines() should be surrounded by the unlock..lock, +/// the variable definition of the line pointer is also included here. Not nice, but does not bother either. +/// Probably the Unlock..lock is needed so that now another process can change the control structure \a linectx while we wait in readlines(). +/// \todo Hope to find the need for dmutex usage later. +/// \n Should this function be declared as pthread_t* function, because of the defintion of pthread-create? But void* works also... +/// static void* devmain(usbdevice* kb){ - // dmutex should still be locked when this is called + /// \attention dmutex should still be locked when this is called int kbfifo = kb->infifo - 1; + /// + /// First a \a readlines_ctx buffer structure is initialized by \c readlines_ctx_init(). readlines_ctx linectx; readlines_ctx_init(&linectx); + /// + /// After some setup functions, beginning in _setupusb() which has called devmain(), + /// we read the command input-Fifo designated to that device in an endless loop. + /// This loop has two possible exits (plus reaction to signals, not mentioned here). while(1){ + /// + /// If the reading via readlines() is successful (we might have read multiple lines), + /// the interpretation is done by readcmd() iff the connection to the device is still available + /// (checked via IS_CONNECTED(kb)). + /// This is true if the kb-structure has a handle and an event pointer both != Null). + /// If not, the loop is left (the first exit point). pthread_mutex_unlock(dmutex(kb)); // Read from FIFO const char* line; @@ -61,8 +158,18 @@ static void* devmain(usbdevice* kb){ // End thread when the handle is removed if(!IS_CONNECTED(kb)) break; + /// + /// if nothing is in the line buffer (some magic interrupt?), + /// continue in the endless while without any reaction. if(lines){ + /// \todo readcmd() gets a \b line, not \b lines. Have a look on that later. + /// \n Is the condition IS_CONNECTED valid? What functions change the condititon for the macro? if(readcmd(kb, line)){ + /// + /// If interpretation and communication with the usb device got errors, + /// they are signalled by readcmd() (non zero retcode). + /// In this case the usb device is closed via closeusb() + /// and the endless loop is left (the second exit point). // USB transfer failed; destroy device closeusb(kb); break; @@ -70,11 +177,53 @@ static void* devmain(usbdevice* kb){ } } pthread_mutex_unlock(dmutex(kb)); + /// + /// After leaving the endless loop the readlines-ctx structure and its buffers are freed by readlines_ctx_free(). readlines_ctx_free(linectx); return 0; } +/// brief . +/// +/// \brief _setupusb A horrible function for setting up an usb device +/// \param context As _setupusb() is called as a new thread, the kb* is transferred as void* +/// \return a ptread_t* 0, here casted as void*. Retval is always null +/// +/// The basic structure of the function is somewhat habituated. +/// It is more like an assembler routine than a structured program. +/// This is not really bad, but just getting used to. +/// +/// After every action, which can be practically fault-prone, +/// the routine goes into the same error handling: +/// It goes via goto to one of two exit labels. +/// The difference is whether or not an unlock has to be performed on the imutex variable. + + +/// +/// In both cases, closeusb() is called, then an unlock is performed on the dmutex. +/// +/// The only case where this error handling is not performed is the correct return of the call to devmain(). +/// Here simply the return value of devmain() is passed to the caller. +/// +/// In either case, the routine terminates with a void* 0 +/// because either devmain() has returned constant null or the routine itself returns zero. +/// +/// The basic idea of this routine is the following: +/// + static void* _setupusb(void* context){ + /// \n First some initialization of kb standard structured and local vars is done. + /// - \b kb is set to the pointer given from start environment + /// - local vars \b vendor and \b product are set to the values from the corresponding fields of kb + /// - local var \b vt \b and the \b kb->vtable are both set to the retval of \a get_vtable() + /// - \b kb->features are set depending on the type of hardware connected: + /// - set either to standard non rgb (all common flags like binding, notify, FW, hardware-loading etc) + /// or in case of RGB-device set to standard + RGB, pollrate-change and fw-update + /// - exclude all features which are disabled via feature_mask (set by daemon CLI parameters) + /// - if it is a mouse, add adjust-rate + /// - if it is a monochrome device, set the flag for RGB-protocol, but single color + /// - the standard delay time is initialized in kb->usbdelay + /// usbdevice* kb = context; // Set standard fields short vendor = kb->vendor, product = kb->product; @@ -85,9 +234,24 @@ static void* _setupusb(void* context){ kb->usbdelay = USB_DELAY_DEFAULT; // Perform OS-specific setup + /// + /// - A fixed 100ms wait is the start. + /// Although the DELAY_LONG macro is given a parameter, it is ignored. Occasionally refactor it. DELAY_LONG(kb); + + /// + /// - The first relevant point is the operating system-specific opening of the interface in os_setupusb(). + /// As a result, some parameters should be set in kb (name, serial, fwversion, epcount = number of usb endpoints), + /// and all endpoints should be claimed with usbclaim(). + /// Claiming is the only point where os_setupusb() can produce an error (-1, otherwise 0). if(os_setupusb(kb)) goto fail; + + /// + /// - The following two statements deal with possible errors when setting the kb values in the current routine: + /// If the version or the name was not read correctly, they are set to default values: + /// - serial is set to ": -NoID" + /// - the name is set to " ". // Make up a device name and serial if they weren't assigned if(!kb->serial[0]) snprintf(kb->serial, SERIAL_LEN, "%04x:%04x-NoID", kb->vendor, kb->product); @@ -95,31 +259,116 @@ static void* _setupusb(void* context){ snprintf(kb->name, KB_NAME_LEN, "%s %s", vendor_str(kb->vendor), product_str(kb->product)); // Set up an input device for key events + /// + /// - Then the user level input subsystem is activated via os_openinput(). + /// There are two file descriptors, one for the mouse and one for the keyboard. + /// As mentioned in structures.h, not the just opened FD numbers are stored under kb->uinput_kb or kb->uinput_mouse, but the values increased by 1! + /// The reason is, if the open fails or not open has been done until now, + /// that struct member is set to 0, not to -1 or other negative value. + /// So all usage of this kb->handle must be something like \c "kb->handle - 1", as you can find it in the code. if(os_inputopen(kb)) goto fail; + /// + /// - The next action is to create a separate thread, which gets as parameter kb and starts with os_inputmain(). + /// The thread is immediately detached so that it can return its resource completely independently if it should terminate. if(pthread_create(&kb->inputthread, 0, os_inputmain, kb)) goto fail; pthread_detach(kb->inputthread); + /// + /// - The same happens with os_setupindicators(), + /// which initially initializes all LED variables in kb to off and then starts the _ledthread() thread + /// with kb as parameter and then detaches it. + /// Here again only the generation of the thread can fail. if(os_setupindicators(kb)) goto fail; // Set up device + /// + /// - Via an entry in the vable (allocprofile, identical for all three vtable types), + /// allocprofile() is called in profile.c. + /// With a valid parameter kb, a usbprofile structure is allocated and stored as a kb->profile. + /// Then initmode() is called for each of the initializable modes (MODE_COUNT, currently 6). + /// This procedure creates the memory space for the mode information, + /// initializes the range to 0, and then sets the light.forceupdate and dpi.forceupdate to true. + /// This forces an update later in the initialization of the device. + /// \n The first mode is set as the current mode and two force flags are set + /// (this seems to be mode-intersecting flags for light and update). + /// \warning There is no error handling for the allocprofile() and initmode() procedures. + /// However, since they allocate storage areas, the subsequent assignments and initializations can run in a SEGV. vt->allocprofile(kb); + /// + /// - Not completely understandable is why now via the vtable the function updateindicators() is called. + /// But this actually happens in the just started thread _ledthread(). + /// Either the initialization is wrong und must done here with force or the overview is lost, what happens when...\n + /// Regardless: For a mouse nothing happens here, for a keyboard updateindicators_kb() is called via the entry in kb->vtable. + /// The first parameter is kb again, the second is constant 1 (means force = true). + /// This causes the LED status to be sent after a 5ms delay via os_sendindicators() + /// (ioctl with a \c usbdevfs_ctrltransfer). + /// \n The notification is sent to all currently open notification channels then. + /// \n Setupindicators() and with it updateindicators_kb() can fail. vt->updateindicators(kb, 1); + /// + /// - From this point - if an error is detected - the error label is addressed by goto statement, + /// which first performs an unlock on the imutex. + /// This is interesting because the next statement is exactly this: An unlock on the imutex. pthread_mutex_unlock(imutex(kb)); + /// + /// - Via vtable the \a kb->start() function is called next. + /// This is the same for a mouse and an RGB keyboard: start_dev(), + /// for a non RGB keyboard it is start_kb_nrgb(). + /// \n First parameter is as always kb, second is 0 (makeactive = false). + /// - In start_kb_nrgb() set the keyboard into a so-called software mode (NK95_HWOFF) + /// via ioctl with \c usbdevfs_ctrltransfer in function _nk95cmd(), + /// which will in turn is called via macro nk95cmd() via start_kb_nrgb(). + /// \n Then two dummy values (active and pollrate) are set in the kb structure and ready. + /// - start_dev() does a bit more - because this function is for both mouse and keyboard. + /// start_dev() calls - after setting an extended timeout parameter - _start_dev(). Both are located in device.c. + /// - First, _start_dev() attempts to determine the firmware version of the device, + /// but only if two conditions are met: + /// hwload-mode is not null (then hw-loading is disabled) + /// and the device has the FEAT_HWLOAD feature. + /// Then the firmware and the poll rate are fetched via getfwversion(). + /// \n If hwload_mode is set to "load only once" (==1), then the HWLOAD feature is masked, so that no further reading can take place. + /// - Now check if device needs a firmware update. If so, set it up and leave the function without error. + /// - Else load the hardware profile from device if the hw-pointer is not set and hw-loading is possible and allowed. + /// \n Return error if mode == 2 (load always) and loading got an error. + /// Else mask the HWLOAD feature, because hwload must be 1 and the error couold be a repeated hw-reading. + /// \n Puh, that is real Horror code. It seems to be not faulty, but completely unreadable. + /// + /// - Finally, the second parameter of _startdev() is used to check whether the device is to be activated. + /// Depending on the parameter, the active or the idle-member in the correspondig vtable is called. + /// These are device-dependent again: + /// Device | active | idle + /// ------ | ------ | ---- + /// RGB Keyboard | cmd_active_kb() means: start the device with a lot of kb-specific initializers (software controlled mode) | cmd_idle_kb() set the device with a lot of kb-specific initializers into the hardware controlled mode) + /// non RGB Keyboard | cmd_io_none() means: Do nothing | cmd_io_none() means: Do nothing + /// Mouse | cmd_active_mouse() similar to cmd_active_kb() | cmd_idle_mouse similar to cmd_idle_kb() + /// + /// - If either \a start() succeeded or the next following usb_tryreset(), it goes on, otherwise again a hard abort occurs. if(vt->start(kb, 0) && usb_tryreset(kb)) goto fail_noinput; - + /// + /// - Next, go to mkdevpath(). After securing the EUID (effective UID) especially for macOS, work starts really in _mkdevpath(). + /// Create - no matter how many devices were registered - + /// either the ckb0/ files \b version, \b pid and \b connected + /// or the \b cmd command fifo, the first notification fifo \b notify0, \b model and \b serial as well as the \b features of the device and the \b pollrate. // Make /dev path if(mkdevpath(kb)) goto fail_noinput; - + /// + /// - If all this is done and no error has occurred, + /// a debug info is printed ("Setup finished for ckbx") + /// updateconnected() writes the new device into the text file under ckb0/ + /// and devmain() is called. // Finished. Enter main loop int index = INDEX_OF(kb, keyboard); ckb_info("Setup finished for %s%d\n", devpath, index); updateconnected(); + /// + /// devmain()'s return value is returned by _setupusb() when we terminate. return devmain(kb); - + /// + /// - The remaining code lines are the two exit labels as described above fail: pthread_mutex_unlock(imutex(kb)); fail_noinput: @@ -128,12 +377,33 @@ static void* _setupusb(void* context){ return 0; } +/// \brief . +/// +/// Set up a USB device after its handle is open. Spawns a new thread _setupusb() with standard parameter kb. +/// dmutex must be locked prior to calling this function. The function will unlock it when finished. +/// In kb->thread the thread id is mentioned, because closeusb() needs this info for joining that thread again. +/// void setupusb(usbdevice* kb){ pthread_mutex_lock(imutex(kb)); if(pthread_create(&kb->thread, 0, _setupusb, kb)) ckb_err("Failed to create USB thread\n"); } +/// \brief . +/// +/// First is checked, whether a firmware-upgrade is indicated for the device. +/// If so, revertusb() returns 0. +/// \todo Why is this useful? Are there problems seen with deactivating a device with older fw-version??? Why isn't this an error indicating reason and we return success (0)? +/// +/// Anyway, the following steps are similar to some other procs, dealing with low level usb handling: +/// - If we do not have an RGB device, a simple setting to Hardware-mode (NK95_HWON) is sent to the device via n95cmd(). +/// \todo The return value of nk95cmd() is ignored (but sending the ioctl may produce an error and _nk95_cmd will indicate this), instead revertusb() returns success in any case. +/// +/// - If we have an RGB device, setactive() is called with second param active = false. +/// That function will have a look on differences between keyboards and mice. +/// \n More precisely setactive() is just a macro to call via the kb->vtable enties either the active() or the idle() function where the vtable points to. +/// setactive() may return error indications. If so, revertusb() returns -1, otherwise 0 in any other case. +/// int revertusb(usbdevice* kb){ if(NEEDS_FW_UPDATE(kb)) return 0; @@ -146,6 +416,13 @@ int revertusb(usbdevice* kb){ return 0; } +/// \brief . +/// +/// First reset the device via os_resetusb() after a long delay (it may send something to the host). +/// If this worked (retval == 0), give the device another long delay +/// Then perform the initialization via the device specific start() function entry in kb->vtable +/// and if this is successful also, return the result of the device depenten updatergb() with force=true. +/// int _resetusb(usbdevice* kb, const char* file, int line){ // Perform a USB reset DELAY_LONG(kb); @@ -161,6 +438,30 @@ int _resetusb(usbdevice* kb, const char* file, int line){ return 0; } +/// \brief . +/// +/// This function is called if an usb command ran into an error +/// in case of one of the following two situations: +/// - When setting up a new usb device and the start() function got an error (\see _setupusb()) +/// - If upgrading to a new firmware gets an error (\see cmd_fwupdate()). +/// +/// The previous action which got the error will NOT be re-attempted. +/// +/// In an endless loop usb_tryreset() tries to reset the given usb device via the macro resetusb(). +/// \n This macro calls _resetusb() with debugging information. +/// \n _resetusb() sends a command via the operating system dependent function os_resetusb() +/// and - if successful - reinitializes the device. +/// os_resetusb() returns -2 to indicate a broken device and all structures should be removed for it. +/// \n In that case, the loop is terminated, an error message is produced and usb_tryreset() returns -1. +/// +/// In case resetusb() has success, the endless loop is left via a return 0 (success). +/// \n If the return value from resetusb() is -1, the loop is continued with the next try. +/// +/// If the global variable \b reset_stop is set directly when the function is called or after each try, +/// usb_tryreset() stops working and returns -1. +/// +/// \todo Why does usb_tryreset() hide the information returned from resetusb()? Isn't it needed by the callers? +/// int usb_tryreset(usbdevice* kb){ if(reset_stop) return -1; @@ -178,16 +479,65 @@ int usb_tryreset(usbdevice* kb){ return -1; } -// device.c +/// +/// hwload_mode is defined in device.c +/// extern int hwload_mode; +/// \brief . +/// +/// \todo A lot of different conditions are combined in this code. Don't think, it is good in every combination... +/// +/// The main task of _usbsend () is to transfer the complete logical message from the buffer beginning with \a messages to count * MSG_SIZE. +/// \n According to usb 2.0 specification, a USB transmits a maximum of 64 byte user data packets. +/// For the transmission of longer messages we need a segmentation. +/// And that is exactly what happens here. +/// +/// The message is given one by one to os_usbsend() in MSG_SIZE (= 64) byte large bites. +/// \attention This means that the buffer given as argument must be n * MSG_SIZE Byte long. +/// +/// An essential constant parameter which is relevant for os_usbsend() only is is_recv = 0, which means sending. +/// +/// Now it gets a little complicated again: +/// - If os_usbsend() returns 0, only zero bytes could be sent in one of the packets, +/// or it was an error (-1 from the systemcall), but not a timeout. +/// How many Bytes were sent in total from earlier calls does not seem to matter, +/// _usbsend() returns a total of 0. +/// - Returns os_usbsend() -1, first check if \b reset_stop is set globally +/// or (incomprehensible) hwload_mode is not set to "always". +/// In either case, _usbsend() returns 0, +/// otherwise it is assumed to be a temporary transfer error and it simply retransmits the physical packet after a long delay. +/// - If the return value of os_usbsend() was neither 0 nor -1, +/// it specifies the numer of bytes transferred. +/// \n Here is an information hiding conflict with os_usbsend() (at least in the Linux version): +/// \n If os_usbsend() can not transfer the entire packet, +/// errors are thrown and the number of bytes sent is returned. +/// _usbsend() interprets this as well +/// and remembers the total number of bytes transferred in the local variable \b total_sent. +/// Subsequently, however, transmission is continued with the next complete MSG_SIZE block +/// and not with the first of the possibly missing bytes. +/// \todo Check whether this is the same in the macOS variant. It is not dramatic, but if errors occur, it can certainly irritate the devices completely if they receive incomplete data streams. Do we have errors with the messages "Wrote YY bytes (expected 64)" in the system logs? If not, we do not need to look any further. +/// +/// When the last packet is transferred, +/// _usbsend() returns the effectively counted set of bytes (from \b total_sent). +/// This at least gives the caller the opportunity to check whether something has been lost in the middle. +/// +/// A bit strange is the structure of the program: +/// Handling the \b count MSG_SIZE blocks to be transferred is done +/// in the outer for (...) loop. +/// Repeating the transfer with a treatable error is managed by the inner while(1) loop. +/// \n This must be considered when reading the code; +/// The "break" on successful block transfer leaves the inner while, not the for (...). +/// int _usbsend(usbdevice* kb, const uchar* messages, int count, const char* file, int line){ int total_sent = 0; for(int i = 0; i < count; i++){ // Send each message via the OS function while(1){ + pthread_mutex_lock(mmutex(kb)); ///< Synchonization between macro and color information DELAY_SHORT(kb); int res = os_usbsend(kb, messages + i * MSG_SIZE, 0, file, line); + pthread_mutex_unlock(mmutex(kb)); if(res == 0) return 0; else if(res != -1){ @@ -204,17 +554,63 @@ int _usbsend(usbdevice* kb, const uchar* messages, int count, const char* file, return total_sent; } +/// \brief . +/// +/// To fully understand this, you need to know about usb: +/// All control is at the usb host (the CPU). +/// If the device wants to communicate something to the host, +/// it must wait for the host to ask. +/// The usb protocol defines the cycles and periods in which actions are to be taken. +/// +/// So in order to receive a data packet from the device, +/// the host must first send a send request. +/// \n This is done by _usbrecv() in the first block +/// by sending the MSG_SIZE large data block from \b out_msg via os_usbsend() as it is a machine depending implementation. +/// The usb target device is as always determined over kb. +/// +/// For os_usbsend() to know that it is a receive request, +/// the \b is_recv parameter is set to true (1). +/// With this, os_usbsend () generates a control package for the hardware, not a data packet. +/// +/// If sending of the control package is not successful, +/// a maximum of 5 times the transmission is repeated (including the first attempt). +/// If a non-cancelable error is signaled or the drive is stopped via reset_stop, +/// _usbrecv() immediately returns 0. +/// +/// After this, the function waits for the requested response from the device using os_usbrecv (). +/// +/// os_usbrecv() returns 0, -1 or something else. +/// \n Zero signals a serious error which is not treatable and _usbrecv() also returns 0. +/// \n -1 means that it is a treatable error - a timeout for example - +/// and therefore the next transfer attempt is started after a long pause (DELAY_LONG) +/// if not reset_stop or the wrong hwload_mode require a termination with a return value of 0. +/// +/// After 5 attempts, _usbrecv () returns and returns 0 as well as an error message. +/// +/// When data is received, the number of received bytes is returned. +/// This should always be MSG_SIZE, +/// but os_usbrecv() can also return less. +/// It should not be more, +/// because then there would be an unhandled buffer overflow, +/// but it could be less. +/// This would be signaled in os_usbrecv () with a message. +/// +/// The buffers behind \b out_msg and \b in_msg are MSG_SIZE at least (currently 64 Bytes). +/// More is ok but useless, less brings unpredictable behavior. +/// int _usbrecv(usbdevice* kb, const uchar* out_msg, uchar* in_msg, const char* file, int line){ - // Try a maximum of 3 times - for(int try = 0; try < 5; try++){ + // Try a maximum of 5 times + for (int try = 0; try < 5; try++) { // Send the output message + pthread_mutex_lock(mmutex(kb)); ///< Synchonization between macro and color information DELAY_SHORT(kb); int res = os_usbsend(kb, out_msg, 1, file, line); - if(res == 0) + pthread_mutex_unlock(mmutex(kb)); + if (res == 0) return 0; - else if(res == -1){ + else if (res == -1) { // Retry on temporary failure - if(reset_stop) + if (reset_stop) return 0; DELAY_LONG(kb); continue; @@ -235,6 +631,49 @@ int _usbrecv(usbdevice* kb, const uchar* out_msg, uchar* in_msg, const char* fil return 0; } +/// \brief . +/// +/// An imutex lock ensures first of all, that no communication is currently running from the viewpoint of the driver to the user input device +/// (ie the virtual driver with which characters or mouse movements are sent from the daemon to the operating system as inputs). +/// +/// If the \b kb has an acceptable value != 0, +/// the index of the device is looked for +/// and with this index os_inputclose() is called. +/// After this no more characters can be sent to the operating system. +/// +/// Then the connection to the usb device is capped by os_closeusb(). +/// \todo What is not yet comprehensible is the call to updateconnected() BEFORE os_closeusb(). +/// Should that be in the other sequence? +/// Or is updateconnected() not displaying the connected usb devices, +/// but the representation which uinput devices are loaded? +/// Questions about questions ... +/// +/// If there is no valid \b handle, only updateconnected() is called. +/// We are probably trying to disconnect a connection under construction. +/// Not clear. +/// +/// The cmd pipe as well as all open notify pipes are deleted via rmdevpath (). +/// \n This means that nothing can happen to the input path - +/// so the device-specific imutex is unlocked again and remains unlocked. +/// +/// Also the dmutex is unlocked now, but only to join the thread, +/// which was originally taken under \b kb->thread +/// (which started with _setupusb()) with pthread_join() again. +/// Because of the closed devices that thread would have to quit sometime +/// \see the hack note with rmdevpath()) +/// +/// As soon as the thread is caught, the dmutex is locked again, which is what I do not understand yet: What other thread can do usb communication now? +/// \n If the vtabel exists for the given kb +/// (why not? It seems to have race conditions here!!), +/// via the vtable the actually device-specific, but still everywhere identical freeprofile() is called. +/// This frees areas that are no longer needed. +/// Then the \b usbdevice structure in its array is set to zero completely. +/// +/// Error handling is rather unusual in closeusb(); +/// Everything works +/// (no matter what the called functions return), +/// and closeusb() always returns zero (success). +/// int closeusb(usbdevice* kb){ pthread_mutex_lock(imutex(kb)); if(kb->handle){ diff --git a/src/ckb-daemon/usb.h b/src/ckb-daemon/usb.h index 3dc8a63964..3be2a12e80 100644 --- a/src/ckb-daemon/usb.h +++ b/src/ckb-daemon/usb.h @@ -4,7 +4,37 @@ #include "includes.h" #include "keymap.h" -// Vendor/product codes +/// \file usb.h +/// Definitions for using USB interface +/// +/// \details Vendor/product codes +/// +/// The list of defines in the first part of the file describes the various types of equipment from Corsair +/// and summarizes them according to specific characteristics. +/// \n Each device type is described with two defines: +/// - On the one hand the device ID with which the device can be recognized on the USB as a short +/// - and on the other hand the same representation as a string, but without leading "0x". +/// +/// First entry-pair is the Provider ID (vendorID) from Corsair. +/// +/// Block No. | contains | Devices are bundled via +/// --------- | -------- | ----------------------- +/// 1 | The first block contains the K65-like keyboards, regardless of their properties (RGB, ...). | In summary, they can be queried using the macro IS_K65(). +/// 2 | the K70-like Keyboards with all their configuration types | summarized by IS_K70(). +/// 3 | the K95 series keyboards | collected with the macro IS_K95(). +/// 4 | strafe keyboards | IS_STRAFE() +/// 5 | M65 mice with and without RGB | IS_M65() +/// 6 | The SABRE and HARPOON mice.\n Maybe this will be divided int two different blocks later because of different nummber of special keys | IS_SABRE() +/// 7 | The Scimitar mouse devices | IS_SCIMITAR() +/// + +/// \brief For the following Defines please see "Detailed Description" +/// +/// \warning When adding new devices please update src/ckb/fwupgradedialog.cpp as well. +/// \n It should contain the same vendor/product IDs for any devices supporting firmware updates. +/// \n In the same way, all other corresponding files have to be supplemented or modified: +/// Currently known for this are \b usb_linux.c and \b usb_mac.c +/// #define V_CORSAIR 0x1b1c #define V_CORSAIR_STR "1b1c" @@ -68,89 +98,225 @@ #define P_SCIMITAR_PRO_STR "1b3e" #define IS_SCIMITAR(kb) ((kb)->vendor == V_CORSAIR && ((kb)->product == P_SCIMITAR || (kb)->product == P_SCIMITAR_PRO)) -// NOTE: when adding new devices please update src/ckb/fwupgradedialog.cpp as well. -// It should contain the same vendor/product IDs for any devices supporting firmware updates. - -//uncomment to see USB packets sent to the device -//#define DEBUG_USB +/// +/// uncomment the following Define to see USB packets sent to the device +// #define DEBUG_USB -//uncomment to see USB packets sent from the device -//#define DEBUG_USB_RECV +/// +///uncomment to see USB packets sent from the device +// #define DEBUG_USB_RECV +/// +/// \brief vendor_str Vendor/product string representations +/// \param vendor \a short vendor ID +/// \return a string: either "" or "corsair" const char* vendor_str(short vendor); + +/// +/// \brief product_str returns a condensed view on what type of device we have. +/// \param product is the \a short USB device product ID +/// \return string to identify a type of device (see below) const char* product_str(short product); -// RGB vs non-RGB test -// (note: non-RGB Strafe is still considered "RGB" in that it shares the same protocol. The difference is denoted with the "monochrome" feature) +/// RGB vs non-RGB test +/// (note: non-RGB Strafe is still considered "RGB" in that it shares the same protocol. +/// The difference is denoted with the "monochrome" feature). #define IS_RGB(vendor, product) ((vendor) == (V_CORSAIR) && (product) != (P_K65_NRGB) && (product) != (P_K70_NRGB) && (product) != (P_K95_NRGB)) + +/// The difference between non RGB and monochrome is, that monochrome has lights, but just in one color. +/// nonRGB has no lights. +/// Change this if new \b monochrome devices are added #define IS_MONOCHROME(vendor, product) ((vendor) == (V_CORSAIR) && (product) == (P_STRAFE_NRGB)) + +/// For calling with a usbdevice*, vendor and product are extracted and IS_RGB() is returned. #define IS_RGB_DEV(kb) IS_RGB((kb)->vendor, (kb)->product) + +/// For calling with a usbdevice*, vendor and product are extracted and IS_MONOCHROME() is returned. #define IS_MONOCHROME_DEV(kb) IS_MONOCHROME((kb)->vendor, (kb)->product) -// Full color range (16.8M) vs partial color range (512) +/// Full color range (16.8M) vs partial color range (512) #define IS_FULLRANGE(kb) (IS_RGB((kb)->vendor, (kb)->product) && (kb)->product != P_K65 && (kb)->product != P_K70 && (kb)->product != P_K95) -// Mouse vs keyboard test +/// Mouse vs keyboard test #define IS_MOUSE(vendor, product) ((vendor) == (V_CORSAIR) && ((product) == (P_M65) || (product) == (P_M65_PRO) || (product) == (P_SABRE_O) || (product) == (P_SABRE_L) || (product) == (P_SABRE_N) || (product) == (P_SCIMITAR) || (product) == (P_SCIMITAR_PRO) || (product) == (P_SABRE_O2))) + +/// For calling with a usbdevice*, vendor and product are extracted and IS_MOUSE() is returned. #define IS_MOUSE_DEV(kb) IS_MOUSE((kb)->vendor, (kb)->product) -// USB delays for when the keyboards get picky about timing +/// USB delays for when the keyboards get picky about timing +/// That was the original comment, but it is used anytime. +/// The short delay is used before any send or receive #define DELAY_SHORT(kb) usleep((int)(kb)->usbdelay * 1000) // base (default: 5ms) + +/// the medium delay is used after sending a command before waiting for the answer. #define DELAY_MEDIUM(kb) usleep((int)(kb)->usbdelay * 10000) // x10 (default: 50ms) + +/// The longest delay takes place where something went wrong (eg when resetting the device) #define DELAY_LONG(kb) usleep(100000) // long, fixed 100ms + +/// This constant is used to initialize \b kb->usbdelay. +/// It is used in many places (see macros above) but often also overwritten to the fixed value of 10. +/// Pure Hacker code. #define USB_DELAY_DEFAULT 5 -// Start the USB main loop. Returns program exit code when finished +/// Start the USB main loop. Returns program exit code when finished int usbmain(); -// Stop the USB system. -void usbkill(); -// Note: Lock a device's dmutex (see device.h) before accessing the USB interface. +/// Stop the USB system. +void usbkill(); -// Set up a USB device after its handle is open. Spawns a new thread. -// dmutex must be locked prior to calling this function. The function will unlock it when finished. +/// \attention Lock a device's dmutex (see device.h) before accessing the USB interface. +/// +/// \brief setupusb starts a thread with kb as parameter and _setupusb() as entrypoint. +/// \param kb THE usbdevice* used everywhere +/// \param[OUT] kb->thread is used to store the thread ID of the fresh created thread. void setupusb(usbdevice* kb); -// OS-specific setup. Return 0 on success. + +/// +/// \brief os_setupusb OS-specific setup for a specific usb device. +/// \param kb THE usbdevice* +/// \return 0 on success, -1 otherwise. int os_setupusb(usbdevice* kb); -// Per keyboard input thread (OS specific). Will be detached from the main thread, so it needs to clean up its own resources. -void* os_inputmain(void* kb); -// Puts a USB device back into hardware mode. Returns 0 on success. +/// +/// \brief os_inputmain is run in a separate thread and will be detached from the main thread, so it needs to clean up its own resources. +/// \param context THE usbdevice* ; Because os_inputmain() is started as a new thread, its formal parameter is named "context". +/// \return null +/// +void* os_inputmain(void* context); + +/// \brief revertusb sets a given device to inactive (hardware controlled) mode if not a fw-ugrade is indicated +/// \param kb THE usbdevice* +/// \return 0 on success or if device needs firmware upgrade, -1 otherwise int revertusb(usbdevice* kb); -// Close a USB device and remove device entry. Returns 0 on success + +/// +/// \brief closeusb Close a USB device and remove device entry. +/// \param[IN,OUT] kb +/// \return Returns 0 (everytime. No error handling is done!) int closeusb(usbdevice* kb); + +/// +/// \brief os_closeusb unclaim it, destroy the udev device and clear data structures at kb +/// \param[IN,OUT] kb THE usbdevice* +/// void os_closeusb(usbdevice* kb); -// Reset a USB device. Returns 0 on success, -1 if device should be removed + +/// +/// \brief _resetusb Reset a USB device. +/// \param kb THE usbdevice* +/// \param file filename for error messages +/// \param line line where it is called for error messages +/// \return Returns 0 on success, -1 if device should be removed int _resetusb(usbdevice* kb, const char* file, int line); + +/// resetusb() is just a macro to call _resetusb() with debuggin constants (file, lineno) #define resetusb(kb) _resetusb(kb, __FILE_NOPATH__, __LINE__) + +/// +/// \brief os_resetusb is the os specific implementation for resetting usb +/// \param kb THE usbdevice* +/// \param file filename for error messages +/// \param line line where it is called for error messages +/// \return Returns 0 on success, -2 if device should be removed and -1 if reset should by tried again int os_resetusb(usbdevice* kb, const char* file, int line); -// Write data to a USB device. Returns number of bytes written or zero on failure. +/// +/// \brief _usbsend send a logical message completely to the given device +/// \param kb THE usbdevice* +/// \param[IN] messages a Pointer to the first byte of the logical message +/// \param[IN] count how many MSG_SIZE buffers is the logical message long? +/// \param[IN] file for debugging +/// \param[IN] line for debugging +/// \param[in] reset_stop global variable is read +/// \return number of Bytes sent (ideal == count * MSG_SIZE);\n 0 if a block could not be sent and it was not a timeout OR \b reset_stop was required or \b hwload_mode is not set to "always" int _usbsend(usbdevice* kb, const uchar* messages, int count, const char* file, int line); + +/// \brief usbsend macro is used to wrap _usbsend() with debugging information (file and lineno) +/// \param kb THE usbdevice* +/// \param[IN] messages a Pointer to the first byte of the logical message +/// \param[IN] count how many MSG_SIZE buffers is the logical message long? #define usbsend(kb, messages, count) _usbsend(kb, messages, count, __FILE_NOPATH__, __LINE__) -// Requests data from a USB device by first sending an output packet and the reading the response. Returns number of bytes read or zero on failure. + +/// +/// \brief _usbrecv Request data from a USB device by first sending an output packet and then reading the response. +/// \param kb THE usbdevice* +/// \param[IN] out_msg What information does the caller want from the device? +/// \param[OUT] in_msg Here comes the answer; The names represent the usb view, not the view of this function! So INput from usb is OUTput of this function. +/// \param[IN] file for debugging +/// \param[IN] line for debugging +/// \param[IN] reset_stop global variable is read +/// \return number of bytes read or zero on failure. int _usbrecv(usbdevice* kb, const uchar* out_msg, uchar* in_msg, const char* file, int line); + +/// \brief usbrecv macro is used to wrap _usbrecv() with debugging information (file and lineno) +/// \param kb THE usbdevice* +/// \param[IN] out_msg What information does the caller want from the device? +/// \param[OUT] in_msg Here comes the answer; The names represent the usb view, not the view of this function! So INput from usb is OUTput of this function. #define usbrecv(kb, out_msg, in_msg) _usbrecv(kb, out_msg, in_msg, __FILE_NOPATH__, __LINE__) -// OS: Send a USB message to the device. Return number of bytes written, zero for permanent failure, -1 for try again +/// \details +/// \brief os_usbsend sends a data packet (MSG_SIZE = 64) Bytes long +/// \param kb THE usbdevice* +/// \param out_msg the MSGSIZE char long buffer to send +/// \param is_recv if true, just send an ioctl for further reading packets. If false, send the data at \b out_msg. +/// \param file for debugging +/// \param line for debugging +/// \return -1 on timeout (try again), 0 on hard error, numer of bytes sent otherwise int os_usbsend(usbdevice* kb, const uchar* out_msg, int is_recv, const char* file, int line); -// OS: Gets input from a USB device. Return same as above. + +/// +/// \brief os_usbrecv receives a max MSGSIZE long buffer from usb device +/// \param kb THE usbdevice* +/// \param in_msg the buffer to fill with the message received +/// \param file for debugging +/// \param line for debugging +/// \return -1 on timeout, 0 on hard error, numer of bytes received otherwise int os_usbrecv(usbdevice* kb, uchar* in_msg, const char* file, int line); -// OS: Update HID indicator LEDs (Num Lock, Caps, etc). Read from kb->ileds. + +/// +/// \brief os_sendindicators update the indicators for the special keys (Numlock, Capslock and what else?) +/// \param kb THE usbdevice* void os_sendindicators(usbdevice* kb); -// Non-RGB K95 command. Returns 0 on success. +/// +/// \brief _nk95cmd If we control a non RGB keyboard, set the keyboard via ioctl with usbdevfs_ctrltransfer +/// \param kb THE usbdevice* +/// \param bRequest the byte array with the usb request +/// \param wValue a usb wValue +/// \param file for error message +/// \param line for error message +/// \return 1 (true) on failure, 0 (false) on success. int _nk95cmd(usbdevice* kb, uchar bRequest, ushort wValue, const char* file, int line); + +/// \brief nk95cmd() macro is used to wrap _nk95cmd() with debugging information (file and lineno). +/// the command structure is different: +/// \n Just the bits 23..16 are used as bits 7..0 for bRequest +/// \n Bits 15..0 are used as wValue #define nk95cmd(kb, command) _nk95cmd(kb, (command) >> 16 & 0xFF, (command) & 0xFFFF, __FILE_NOPATH__, __LINE__) -#define NK95_HWOFF 0x020030 // Hardware playback off -#define NK95_HWON 0x020001 // Hardware playback on -#define NK95_M1 0x140001 // Mode switches +/// Hardware-specific commands for the K95 nonRGB, +/// \see [usb2.0 documentation for details](http://www.usb.org/developers/docs/usb_20.zip). +/// Set Hardware playback off +#define NK95_HWOFF 0x020030 + +/// Hardware playback on +#define NK95_HWON 0x020001 + +/// Switch to mode 1 +#define NK95_M1 0x140001 + +/// Switch to mode 2 #define NK95_M2 0x140002 + +/// Switch to mode 3 #define NK95_M3 0x140003 -// Tries to reset a USB device after a failed action. Returns 0 on success. -// The previous action will NOT be re-attempted. +/// +/// \brief usb_tryreset does what the name means: Try to reset the usb via resetusb() +/// \param[in,out] kb THE usbdevice* +/// \param[in] reset_stop global variable is read +/// \return 0 on success, -1 otherwise int usb_tryreset(usbdevice* kb); #endif // USB_H diff --git a/src/ckb-daemon/usb_linux.c b/src/ckb-daemon/usb_linux.c index 8673f86f41..5f5853d225 100644 --- a/src/ckb-daemon/usb_linux.c +++ b/src/ckb-daemon/usb_linux.c @@ -6,11 +6,68 @@ #ifdef OS_LINUX +/// \details +/// \brief all open usb devices have their system path names here in this array. +#define DEBUG + static char kbsyspath[DEV_MAX][FILENAME_MAX]; -int os_usbsend(usbdevice* kb, const uchar* out_msg, int is_recv, const char* file, int line){ +//// +/// \brief os_usbsend sends a data packet (MSG_SIZE = 64) Bytes long +/// +/// os_usbsend has two functions: +/// - if is_recv == false, it tries to send a given MSG_SIZE buffer via the usb interface given with kb. +/// - otherwise a request is sent via the usb device to initiate the receiving of a message from the remote device. +/// +/// The functionality for sending distinguishes two cases, +/// depending on the version number of the firmware of the connected device: +/// \n If the firmware is less or equal 1.2, the transmission is done via an ioctl(). +/// The ioctl() is given a struct usbdevfs_ctrltransfer, in which the relevant parameters are entered: +/// +/// bRequestType | bRequest | wValue | EP | size | Timeout | data +/// ------------ | -------- | ------ | -- | ---- | ------- | ---- +/// 0x21 | 0x09 | 0x0200 | endpoint / IF to be addressed from epcount-1 | MSG_SIZE | 5000 (=5ms) | the message buffer pointer +/// Host to Device, Type=Class, Recipient=Interface | 9 = Send data? | specific | last or pre-last device # | 64 | 5000 | out_msg +/// +/// \n The ioctl command is USBDEVFS_CONTROL. +/// +/// The same constellation is used if the device is requested to send its data (is_recv = true). +/// +/// For a more recent firmware and is_recv = false, +/// the ioctl command USBDEVFS_CONTROL is not used +/// (this tells the bus to enter the control mode), +/// but the bulk method is used: USBDEVFS_BULK. +/// This is astonishing, because all of the endpoints are type Interrupt, not bulk. +/// +/// Anyhow, forthis purpose a different structure is used for the ioctl() (struct \b usbdevfs_bulktransfer) +/// and this is also initialized differently: +/// \n The length and timeout parameters are given the same values as above. +/// The formal parameter out_msg is also passed as a buffer pointer. +/// For the endpoints, the firmware version is differentiated again: +/// \n For a firmware version between 1.3 and <2.0 endpoint 4 is used, +/// otherwise (it can only be >=2.0) endpoint 3 is used. +/// +/// \todo Since the handling of endpoints has already led to problems elsewhere, this implementation is extremely hardware-dependent and critical! +/// \n Eg. the new keyboard K95PLATINUMRGB has a version number significantly less than 2.0 - will it run with this implementation? +/// +/// The ioctl() - no matter what type - +/// returns the number of bytes sent. +/// Now comes the usual check: +/// - If the return value is -1 AND the error is a timeout (ETIMEOUT), +/// os_usbsend() will return -1 to indicate that it is probably a recoverable problem and a retry is recommended. +/// - For another negative value or other error identifier OR 0 bytes sent, 0 is returned as a heavy error identifier. +/// - In all other cases, the function returns the number of bytes sent. +/// +/// If this is not the entire blocksize (MSG_SIZE bytes), +/// an error message is issued on the standard error channel +/// [warning "Wrote YY bytes (expected 64)"]. +/// +/// If DEBUG_USB is set during compilation, +/// the number of bytes sent and their representation are logged to the error channel. +/// +int os_usbsend(usbdevice* kb, const uchar* out_msg, int is_recv, const char* file, int line) { int res; - if(kb->fwversion >= 0x120 && !is_recv){ + if (kb->fwversion >= 0x120 && !is_recv) { struct usbdevfs_bulktransfer transfer; memset(&transfer, 0, sizeof(transfer)); transfer.ep = (kb->fwversion >= 0x130 && kb->fwversion < 0x200) ? 4 : 3; @@ -22,13 +79,14 @@ int os_usbsend(usbdevice* kb, const uchar* out_msg, int is_recv, const char* fil struct usbdevfs_ctrltransfer transfer = { 0x21, 0x09, 0x0200, kb->epcount - 1, MSG_SIZE, 5000, (void*)out_msg }; res = ioctl(kb->handle - 1, USBDEVFS_CONTROL, &transfer); } - if(res <= 0){ - ckb_err_fn("%s\n", file, line, res ? strerror(errno) : "No data written"); + + if (res <= 0){ + ckb_err_fn(" %s, res = 0x%x\n", file, line, res ? strerror(errno) : "No data written", res); if(res == -1 && errno == ETIMEDOUT) return -1; else return 0; - } else if(res != MSG_SIZE) + } else if (res != MSG_SIZE) ckb_warn_fn("Wrote %d bytes (expected %d)\n", file, line, res, MSG_SIZE); #ifdef DEBUG_USB char converted[MSG_SIZE*3 + 1]; @@ -39,6 +97,36 @@ int os_usbsend(usbdevice* kb, const uchar* out_msg, int is_recv, const char* fil return res; } +/// +/// \brief os_usbrecv receives a max MSGSIZE long buffer from usb device + +/// os_usbrecv does what its name says: +/// +/// The comment at the beginning of the procedure +/// causes the suspicion that the firmware versionspecific distinction +/// is missing for receiving from usb endpoint 3 or 4. +/// The commented code contains only the reception from EP4, +/// but this may be wrong for a software version 2.0 or higher (see the code for os-usbsend ()). +/// +/// \n So all the receiving is done via an ioctl() like in os_usbsend. +/// The ioctl() is given a struct usbdevfs_ctrltransfer, in which the relevant parameters are entered: +/// +/// bRequestType | bRequest | wValue | EP | size | Timeout | data +/// ------------ | -------- | ------ | -- | ---- | ------- | ---- +/// 0xA1 | 0x01 | 0x0200 | endpoint to be addressed from epcount - 1 | MSG_SIZE | 5ms | the message buffer pointer +/// Device to Host, Type=Class, Recipient=Interface | 1 = RECEIVE? | specific | Interface # | 64 | 5000 | in_msg +/// +/// The ioctl() returns the number of bytes received. +/// Here is the usual check again: +/// - If the return value is -1 AND the error is a timeout (ETIMEOUT), +/// os_usbrecv() will return -1 to indicate that it is probably a recoverable problem and a retry is recommended. +/// - For another negative value or other error identifier OR 0 bytes are received, 0 is returned as an identifier for a heavy error. +/// - In all other cases, the function returns the number of bytes received. +/// +/// If this is not the entire blocksize (MSG_SIZE bytes), +/// an error message is issued on the standard error channel +/// [warning "Read YY bytes (expected 64)"]. +/// int os_usbrecv(usbdevice* kb, uchar* in_msg, const char* file, int line){ int res; // This is what CUE does, but it doesn't seem to work on linux. @@ -71,6 +159,34 @@ int os_usbrecv(usbdevice* kb, uchar* in_msg, const char* file, int line){ return res; } +/// +/// \brief _nk95cmd If we control a non RGB keyboard, set the keyboard via ioctl with usbdevfs_ctrltransfer +/// +/// To send control packets to a non RGB non color K95 Keyboard, +/// use this function. Normally it is called via the nk95cmd() macro. +/// +/// If it is the wrong device for which the function is called, 0 is returned and nothing done. +/// Otherwise a usbdevfs_ctrltransfer structure is filled and an USBDEVFS_CONTROL ioctl() called. +/// +/// bRequestType | bRequest | wValue | EP | size | Timeout | data +/// ------------ | -------- | ------ | -- | ---- | ------- | ---- +/// 0x40 | see table below to switch hardware-modus at Keyboard | wValue | device | MSG_SIZE | 5ms | the message buffer pointer +/// Host to Device, Type=Vendor, Recipient=Device | bRequest parameter | given wValue Parameter | device 0 | 0 data to write | 5000 | null +/// +/// If a 0 or a negative error number is returned by the ioctl, an error message is shown depending on the errno or "No data written" if retval was 0. +/// In either case 1 is returned to indicate the error. +/// If the ioctl returned a value > 0, 0 is returned to indicate no error. +/// +/// Currently the following combinations for bRequest and wValue are used: +/// Device | what it might to do | constant | bRequest | wValue +/// ------ | ------------------- | -------- | -------- | ------ +/// non RGB Keyboard | set HW-modus on (leave the ckb driver) | HWON | 0x0002 | 0x0030 +/// non RGB Keyboard | set HW-modus off (initialize the ckb driver) | HWOFF | 0x0002 | 0x0001 +/// non RGB Keyboard | set light modus M1 in single-color keyboards | NK95_M1 | 0x0014 | 0x0001 +/// non RGB Keyboard | set light modus M2 in single-color keyboards | NK95_M2 | 0x0014 | 0x0002 +/// non RGB Keyboard | set light modus M3 in single-color keyboards | NK95_M3 | 0x0014 | 0x0003 +/// \see usb.h +/// int _nk95cmd(usbdevice* kb, uchar bRequest, ushort wValue, const char* file, int line){ if(kb->product != P_K95_NRGB) return 0; @@ -83,13 +199,44 @@ int _nk95cmd(usbdevice* kb, uchar bRequest, ushort wValue, const char* file, int return 0; } -void os_sendindicators(usbdevice* kb){ +/// \brief . +/// +/// \brief os_sendindicators update the indicators for the special keys (Numlock, Capslock and what else?) +/// +/// Read the data from kb->ileds ans send them via ioctl() to the keyboard. +/// +/// bRequestType | bRequest | wValue | EP | size | Timeout | data +/// ------------ | -------- | ------ | -- | ---- | ------- | ---- +/// 0x21 | 0x09 | 0x0200 | Interface 0 | MSG_SIZE 1 Byte | timeout 0,5ms | the message buffer pointer +/// Host to Device, Type=Class, Recipient=Interface (why not endpoint?) | 9 = SEND? | specific | 0 | 1 | 500 | struct* kb->ileds +/// +/// \n The ioctl command is USBDEVFS_CONTROL. +/// +void os_sendindicators(usbdevice* kb) { + static int countForReset = 0; struct usbdevfs_ctrltransfer transfer = { 0x21, 0x09, 0x0200, 0x00, 1, 500, &kb->ileds }; int res = ioctl(kb->handle - 1, USBDEVFS_CONTROL, &transfer); - if(res <= 0) + if(res <= 0) { ckb_err("%s\n", res ? strerror(errno) : "No data written"); + if (usb_tryreset(kb) == 0 && countForReset++ < 3) { + os_sendindicators(kb); + } + } } +/// +/// \brief os_inputmain This function is run in a separate thread and will be detached from the main thread, so it needs to clean up its own resources. +/// \todo This function is a collection of many tasks. It should be divided into several sub-functions for the sake of greater convenience: +/// +/// 1. set up an URB (Userspace Ressource Buffer) to communicate with the USBDEVFS_* ioctl()s +/// 2. perform the ioctl() +/// 3. interpretate the information got into the URB buffer or handle error situations and retry operation or leave the endless loop +/// 4. inform the os about the data +/// 5. loop endless via 2. +/// 6. if endless loop has gone, deinitalize the interface, free buffers etc. +/// 7. return null +/// + void* os_inputmain(void* context){ usbdevice* kb = context; int fd = kb->handle - 1; @@ -97,13 +244,35 @@ void* os_inputmain(void* context){ int index = INDEX_OF(kb, keyboard); ckb_info("Starting input thread for %s%d\n", devpath, index); - // Monitor input transfers on all endpoints for non-RGB devices - // For RGB, monitor all but the last, as it's used for input/output + /// Here the actions in detail: + /// + /// Monitor input transfers on all endpoints for non-RGB devices + /// For RGB, monitor all but the last, as it's used for input/output int urbcount = IS_RGB(vendor, product) ? (kb->epcount - 1) : kb->epcount; - struct usbdevfs_urb urbs[urbcount]; + if (urbcount == 0) { + ckb_err("urbcount = 0, so there is nothing to claim in os_inputmain()\n"); + return 0; + } + + /// Get an usbdevfs_urb data structure and clear it via memset() + struct usbdevfs_urb urbs[urbcount + 1]; memset(urbs, 0, sizeof(urbs)); + + /// Hopefully the buffer lengths are equal for all devices with congruent types. + /// You can find out the correctness for your device with lsusb --v or similar on macOS. + /// Currently the following combinations are known and implemented: + /// + /// device | detect with macro combination | endpoint # | buffer-length + /// ------ | ----------------------------- | ---------- | ------------- + /// each | none | 0 | 8 + /// RGB Mouse | IS_RGB && IS_MOUSE | 1 | 10 + /// RGB Keyboard | IS_RGB && !IS_MOUSE | 1 | 21 + /// RGB Mouse or Keyboard | IS_RGB | 2 | MSG_SIZE (64) + /// non RGB Mouse or Keyboard | !IS_RGB | 1 | 4 + /// non RGB Mouse or Keyboard | !IS_RGB | 2 | 15 + /// urbs[0].buffer_length = 8; - if(IS_RGB(vendor, product)){ + if(urbcount > 1 && IS_RGB(vendor, product)) { if(IS_MOUSE(vendor, product)) urbs[1].buffer_length = 10; else @@ -115,31 +284,53 @@ void* os_inputmain(void* context){ urbs[1].buffer_length = 4; urbs[2].buffer_length = 15; } - // Submit URBs + + /// Now submit all the URBs via ioctl(USBDEVFS_SUBMITURB) with type USBDEVFS_URB_TYPE_INTERRUPT (the endpoints are defined as type interrupt). + /// Endpoint number is 0x80..0x82 or 0x83, depending on the model. for(int i = 0; i < urbcount; i++){ urbs[i].type = USBDEVFS_URB_TYPE_INTERRUPT; urbs[i].endpoint = 0x80 | (i + 1); urbs[i].buffer = malloc(urbs[i].buffer_length); ioctl(fd, USBDEVFS_SUBMITURB, urbs + i); } - // Start monitoring input - while(1){ + + /// The userSpaceFS knows the URBs now, so start monitoring input + while (1) { struct usbdevfs_urb* urb = 0; - if(ioctl(fd, USBDEVFS_REAPURB, &urb)){ - if(errno == ENODEV || errno == ENOENT || errno == ESHUTDOWN) + + /// if the ioctl returns something != 0, let's have a deeper look what happened. + /// Broken devices or shutting down the entire system leads to closing the device and finishing this thread. + if (ioctl(fd, USBDEVFS_REAPURB, &urb)) { + if (errno == ENODEV || errno == ENOENT || errno == ESHUTDOWN) // Stop the thread if the handle closes break; else if(errno == EPIPE && urb){ - // On EPIPE, clear halt on the endpoint + /// If just an EPIPE ocurred, give the device a CLEAR_HALT and resubmit the URB. ioctl(fd, USBDEVFS_CLEAR_HALT, &urb->endpoint); // Re-submit the URB if(urb) ioctl(fd, USBDEVFS_SUBMITURB, urb); urb = 0; } + continue; } - if(urb){ - // Process input (if any) + + /// A correct REAPURB returns a Pointer to the URB which we now have a closer look into. + /// Lock all following actions with imutex. + /// + if (urb) { + + /// Process the input depending on type of device. Interprete the actual size of the URB buffer + /// + /// device | detect with macro combination | seems to be endpoint # | actual buffer-length | function called + /// ------ | ----------------------------- | ---------------------- | -------------------- | --------------- + /// mouse (RGB and non RGB) | IS_MOUSE | nA | 8, 10 or 11 | hid_mouse_translate() + /// mouse (RGB and non RGB) | IS_MOUSE | nA | MSG_SIZE (64) | corsair_mousecopy() + /// RGB Keyboard | IS_RGB && !IS_MOUSE | 1 | 8 (BIOS Mode) | hid_kb_translate() + /// RGB Keyboard | IS_RGB && !IS_MOUSE | 2 | 5 or 21, KB inactive! | hid_kb_translate() + /// RGB Keyboard | IS_RGB && !IS_MOUSE | 3? | MSG_SIZE | corsair_kbcopy() + /// non RGB Keyboard | !IS_RGB && !IS_MOUSE | nA | nA | hid_kb_translate() + /// pthread_mutex_lock(imutex(kb)); if(IS_MOUSE(vendor, product)){ switch(urb->actual_length){ @@ -171,17 +362,24 @@ void* os_inputmain(void* context){ corsair_kbcopy(kb->input.keys, -(urb->endpoint & 0xF), urb->buffer); break; } - } else + } else { // Non-RGB input hid_kb_translate(kb->input.keys, urb->endpoint & 0xF, urb->actual_length, urb->buffer); + } + /// + /// The input data is transformed and copied to the kb structure. Now give it to the OS and unlock the imutex afterwards. inputupdate(kb); pthread_mutex_unlock(imutex(kb)); - // Re-submit the URB + + /// Re-submit the URB for the next run. ioctl(fd, USBDEVFS_SUBMITURB, urb); urb = 0; } } - // Clean up + + /// + /// If the endless loop is terminated, clean up by discarding the URBs via ioctl(USBDEVFS_DISCARDURB), + /// free the URB buffers and return a null pointer as thread exit code. ckb_info("Stopping input thread for %s%d\n", devpath, index); for(int i = 0; i < urbcount; i++){ ioctl(fd, USBDEVFS_DISCARDURB, urbs + i); @@ -190,14 +388,32 @@ void* os_inputmain(void* context){ return 0; } -int usbunclaim(usbdevice* kb, int resetting){ +/// \brief . +/// +/// \brief usbunclaim do an unclaiming of the usb device gicen by kb. +/// \param kb THE usbdevice* +/// \param resetting boolean flag: If resseting is true, the caller will perform a bus reset command after unclaiming the device. +/// \return always 0. +/// +/// Unclaim all endpoints for a given device (remeber the decrementing of the file descriptor) +/// via ioctl(USBDEVFS_DISCARDURB). +/// +/// Afterwards - if ressetting is false - do a USBDEVFS_CONNECT for EP 0 and 1. +/// If it is a non RGB device, connect EP 2 also. +/// The comment mentions RGB keyboards only, but as I understand the code, this is valid also for RGB mice. +/// +/// There is no error handling yet. +/// Function is called in usb_linux.c only, so it is declared as static now. +/// +static int usbunclaim(usbdevice* kb, int resetting) { int handle = kb->handle - 1; int count = kb->epcount; - for(int i = 0; i < count; i++) + for (int i = 0; i < count; i++) { ioctl(handle, USBDEVFS_RELEASEINTERFACE, &i); + } // For RGB keyboards, the kernel driver should only be reconnected to interfaces 0 and 1 (HID), and only if we're not about to do a USB reset. // Reconnecting any of the others causes trouble. - if(!resetting){ + if (!resetting) { struct usbdevfs_ioctl ctl = { 0, USBDEVFS_CONNECT, 0 }; ioctl(handle, USBDEVFS_IOCTL, &ctl); ctl.ifno = 1; @@ -211,6 +427,13 @@ int usbunclaim(usbdevice* kb, int resetting){ return 0; } +/// \brief . +/// +/// \brief os_closeusb is the linux specific implementation for closing an active usb port. +/// \n If a valid handle is given in the kb structure, the usb port is unclaimed (usbunclaim()). +/// \n The device in unrefenced via library function udev_device_unref(). +/// \n handle, udev and the first char of kbsyspath are cleared to 0 (empty string for kbsyspath). +/// void os_closeusb(usbdevice* kb){ if(kb->handle){ usbunclaim(kb, 0); @@ -223,17 +446,38 @@ void os_closeusb(usbdevice* kb){ kbsyspath[INDEX_OF(kb, keyboard)][0] = 0; } -int usbclaim(usbdevice* kb){ +/// \brief . +/// +/// \brief usbclaim does claiming all EPs for the usb device gicen by kb. +/// \param kb THE usbdevice* +/// \return 0 on success, -1 otherwise. +/// +/// Claim all endpoints for a given device (remeber the decrementing of the file descriptor) +/// via ioctl(USBDEVFS_DISCONNECT) and ioctl(USBDEVFS_CLAIMINTERFACE). +/// +/// Error handling is done for the ioctl(USBDEVFS_CLAIMINTERFACE) only. If this fails, now an error message is thrown and -1 is returned. +/// Function is called in usb_linux.c only, so it is declared as static now. +/// +static int usbclaim(usbdevice* kb){ int count = kb->epcount; +#ifdef DEBUG + ckb_info("claiming %d endpoints\n", count); +#endif // DEBUG + for(int i = 0; i < count; i++){ struct usbdevfs_ioctl ctl = { i, USBDEVFS_DISCONNECT, 0 }; ioctl(kb->handle - 1, USBDEVFS_IOCTL, &ctl); - if(ioctl(kb->handle - 1, USBDEVFS_CLAIMINTERFACE, &i)) + if(ioctl(kb->handle - 1, USBDEVFS_CLAIMINTERFACE, &i)) { + ckb_err("Failed to claim interface %d: %s\n", i, strerror(errno)); return -1; + } } return 0; } +/// +/// \brief TEST_RESET doesa "try / catch" for resetting the usb interface +/// #define TEST_RESET(op) \ if(op){ \ ckb_err_fn("resetusb failed: %s\n", file, line, strerror(errno)); \ @@ -242,7 +486,17 @@ int usbclaim(usbdevice* kb){ return -2; /* else, remove device */ \ } -int os_resetusb(usbdevice* kb, const char* file, int line){ +/// \brief . +/// +/// Try to reset an usb device in a linux user space driver. +/// 1. unclaim the device, but do not reconnect the system driver (second param resetting = true) +/// 2. reset the device via USBDEVFS_RESET command +/// 3. claim the device again. +/// Returns 0 on success, -2 if device should be removed and -1 if reset should by tried again +/// +/// \todo it seems that no one wants to try the reset again. But I'v seen it somewhere... +/// +int os_resetusb(usbdevice* kb, const char* file, int line) { TEST_RESET(usbunclaim(kb, 1)); TEST_RESET(ioctl(kb->handle - 1, USBDEVFS_RESET)); TEST_RESET(usbclaim(kb)); @@ -250,6 +504,11 @@ int os_resetusb(usbdevice* kb, const char* file, int line){ return 0; } +/// \brief . +/// +/// \brief strtrim trims a string by removing leading and trailing spaces. +/// \param string +/// void strtrim(char* string){ // Find last non-space char* last = string; @@ -268,8 +527,16 @@ void strtrim(char* string){ memmove(string, first, last - first); } -int os_setupusb(usbdevice* kb){ - // Copy device description and serial +/// \brief . +/// +/// Perform the operating system-specific opening of the interface in os_setupusb(). +/// As a result, some parameters should be set in kb (name, serial, fwversion, epcount = number of usb endpoints), +/// and all endpoints should be claimed with usbclaim(). +/// Claiming is the only point where os_setupusb() can produce an error (-1). +/// +int os_setupusb(usbdevice* kb) { + /// + /// - Copy device description and serial struct udev_device* dev = kb->udev; const char* name = udev_device_get_sysattr_value(dev, "product"); if(name) @@ -279,24 +546,44 @@ int os_setupusb(usbdevice* kb){ if(serial) strncpy(kb->serial, serial, SERIAL_LEN); strtrim(kb->serial); - // Copy firmware version (needed to determine USB protocol) + /// + /// - Copy firmware version (needed to determine USB protocol) const char* firmware = udev_device_get_sysattr_value(dev, "bcdDevice"); if(firmware) sscanf(firmware, "%hx", &kb->fwversion); else kb->fwversion = 0; int index = INDEX_OF(kb, keyboard); + + /// - Do some output about connecting interfaces ckb_info("Connecting %s at %s%d\n", kb->name, devpath, index); - // Claim the USB interfaces + /// + /// - Claim the USB interfaces + /// + /// \todo in these modules a pullrequest is outstanding + /// const char* ep_str = udev_device_get_sysattr_value(dev, "bNumInterfaces"); +#ifdef DEBUG + ckb_info("claiming interfaces. name=%s, serial=%s, firmware=%s; Got >>%s<< as ep_str\n", name, serial, firmware, ep_str); +#endif //DEBUG kb->epcount = 0; if(ep_str) sscanf(ep_str, "%d", &kb->epcount); - if(kb->epcount == 0){ - // This shouldn't happen, but if it does, assume EP count based on what the device is supposed to have - kb->epcount = (HAS_FEATURES(kb, FEAT_RGB) ? 4 : 3); - ckb_warn("Unable to read endpoint count from udev, assuming %d...\n", kb->epcount); + if(kb->epcount < 2){ + // IF we have an RGB KB with 0 or 1 endpoints, it will be in BIOS mode. + ckb_err("Unable to read endpoint count from udev, assuming %d and reading >>%s<< or device is in BIOS mode\n", kb->epcount, ep_str); + if (usb_tryreset(kb) == 0) { ///< Try to reset the device and recall the function + static int retryCount = 0; ///< Don't do this endless in recursion + if (retryCount++ < 5) { + return os_setupusb(kb); ///< os_setupusb() has a return value (used as boolean) + } + } + return -1; + // ToDo are there special versions we have to detect? If there are, that was the old code to handle it: + // This shouldn't happen, but if it does, assume EP count based onckb_warn what the device is supposed to have + // kb->epcount = (HAS_FEATURES(kb, FEAT_RGB) ? 4 : 3); + // ckb_warn("Unable to read endpoint count from udev, assuming %d and reading >>%s<<...\n", kb->epcount, ep_str); } if(usbclaim(kb)){ ckb_err("Failed to claim interfaces: %s\n", strerror(errno)); @@ -305,13 +592,16 @@ int os_setupusb(usbdevice* kb){ return 0; } -int usbadd(struct udev_device* dev, short vendor, short product){ +int usbadd(struct udev_device* dev, short vendor, short product) { const char* path = udev_device_get_devnode(dev); const char* syspath = udev_device_get_syspath(dev); if(!path || !syspath || path[0] == 0 || syspath[0] == 0){ ckb_err("Failed to get device path\n"); return -1; } +#ifdef DEBUG + ckb_info(">>>vendor = 0x%x, product = 0x%x, path = %s, syspath = %s\n", vendor, product, path, syspath); +#endif // DEDBUG // Find a free USB slot for(int index = 1; index < DEV_MAX; index++){ usbdevice* kb = keyboard + index; @@ -348,7 +638,9 @@ int usbadd(struct udev_device* dev, short vendor, short product){ return -1; } -static struct udev* udev; +static struct udev* udev; ///< struct udef is defined in /usr/include/libudev.h + +/// \todo These two thread vasriables seem to be unused: usbtread, udevthread pthread_t usbthread, udevthread; // String -> numeric model map @@ -356,6 +648,12 @@ typedef struct { const char* name; short number; } _model; +/// +/// \attention when adding new hardware this file hat to be changed too. +/// +/// In this structure array \a models[] for each device the name +/// (the device id as string in hex without leading 0x) +/// and its usb device id as short must be entered in this array. static _model models[] = { // Keyboards { P_K65_STR, P_K65 }, @@ -385,7 +683,17 @@ static _model models[] = { }; #define N_MODELS (sizeof(models) / sizeof(_model)) -// Add a udev device. Returns 0 if device was recognized/added. +/// +/// \brief Add a udev device. Returns 0 if device was recognized/added. +/// \brief If the device id can be found, call usbadd() with the appropriate parameters. +/// \param dev the functions usb_*_device get a struct udev* with the neccessary hardware-related information. +/// \return the retval of usbadd() or 1 if either vendor is not corsair or product is not mentioned in model[]. +/// +/// First get the idVendor via udev_device_get_sysattr_value(). If this is equal to the ID-string of corsair ("1b1c"), +/// get the idProduct on the same way. +/// \n If we can find the model name in the model array, +/// call usbadd() with the model number. +/// \todo So why the hell not a transformation between the string and the short presentation? Lets check if the string representation is used elsewhere. static int usb_add_device(struct udev_device* dev){ const char* vendor = udev_device_get_sysattr_value(dev, "idVendor"); if(vendor && !strcmp(vendor, V_CORSAIR_STR)){ @@ -401,7 +709,16 @@ static int usb_add_device(struct udev_device* dev){ return 1; } -// Remove a udev device. +/// +/// \brief usb_rm_device find the usb port to remove and close it via closeusb(). +/// \param dev the functions usb_*_device get a struct udev* with the neccessary hardware-related information. +/// +/// First try to find the system path of the device given in parameter dev. +/// The index where the name is found is the same index we need to address the global keyboard array. +/// That array holds all usbdevices. +/// \n Searching for the correct name in kbsyspath-array and closing the usb via closeusb() +/// are protected by lock..unlock of the corresponding devmutex arraymember. +/// static void usb_rm_device(struct udev_device* dev){ // Device removed. Look for it in our list of keyboards const char* syspath = udev_device_get_syspath(dev); @@ -415,6 +732,20 @@ static void usb_rm_device(struct udev_device* dev){ } } +/// +/// \brief udev_enum use the udev_enumerate_add_match_subsystem() to get all you need but only that. +/// +/// Reduce the hits of the enumeration by limiting to usb as technology and corsair as idVendor. +/// Then filter with udev_enumerate_scan_devices () all hits. +/// +/// The following call to udev_enumerate_get_list_entry() fetches the entire hitlist as udev_list_entry *. +/// \n Use udev_list_entry_foreach() to iterate through the hit set. +/// \n If both the device name exists (udev_list_entry_get_name) +/// and the subsequent creation of a new udev_device (udev_device_new_from_syspath) is ok, +/// the new device is added to the list with usb_add_device(). +/// +/// If the latter does not work, the new device is released again (udev_device_unref ()). +/// \n After the last iteration, the enumerator is released with udev_enumerate_unref (). static void udev_enum(){ struct udev_enumerate* enumerator = udev_enumerate_new(udev); udev_enumerate_add_match_subsystem(enumerator, "usb"); @@ -438,20 +769,34 @@ static void udev_enum(){ udev_enumerate_unref(enumerator); } +/// \brief . +/// +/// \brief usbmain is called by main() after setting up all other stuff. +/// \return 0 normally or -1 if fatal error occurs (up to now only if no new devices are available) +/// int usbmain(){ + /// First check whether the uinput module is loaded by the kernel. + /// \todo Why isn't missing of uinput + /// a fatal error? + /// // Load the uinput module (if it's not loaded already) if(system("modprobe uinput") != 0) ckb_warn("Failed to load uinput module\n"); - // Create the udev object - if(!(udev = udev_new())){ - ckb_fatal("Failed to initialize udev\n"); + /// + /// Create the udev object with udev_new() (is a function from libudev.h) + /// terminate -1 if error + if(!(udev = udev_new())) { + ckb_fatal("Failed to initialize udev in usbmain(), usb_linux.c\n"); return -1; } - // Enumerate all currently connected devices + /// + /// Enumerate all currently connected devices udev_enum(); + /// \todo lae. here the work has to go on... + /// // Done scanning. Enter a loop to poll for device updates struct udev_monitor* monitor = udev_monitor_new_from_netlink(udev, "udev"); udev_monitor_filter_add_match_subsystem_devtype(monitor, "usb", 0); diff --git a/src/ckb-daemon/usb_mac.c b/src/ckb-daemon/usb_mac.c index 1ccb6995b3..1b7986a933 100644 --- a/src/ckb-daemon/usb_mac.c +++ b/src/ckb-daemon/usb_mac.c @@ -99,6 +99,8 @@ static int get_pipe_index(usb_iface_t handle, int desired_direction){ int os_usbsend(usbdevice* kb, const uchar* out_msg, int is_recv, const char* file, int line){ kern_return_t res = kIOReturnSuccess; + /// + /// \todo Be aware: This condition is exact inverted to the condition in the linux dependent os_usbsend(). It may be correct, but please check it. if(kb->fwversion < 0x120 || is_recv){ int ep = kb->epcount; // For old devices, or for receiving data, use control transfers diff --git a/src/ckb/ckbsettings.cpp b/src/ckb/ckbsettings.cpp index a26b896f3c..4d014bdc81 100644 --- a/src/ckb/ckbsettings.cpp +++ b/src/ckb/ckbsettings.cpp @@ -17,6 +17,13 @@ QMutex settingsMutex(QMutex::Recursive), settingsCacheMutex(QMutex::Recursive); #define lockMutexStatic2 QMutexLocker locker2(&settingsMutex) #define lockMutexCache QMutexLocker locker(&settingsCacheMutex) +#if QT_VERSION < QT_VERSION_CHECK(5, 5, 0) +#define qInfo qDebug +#define qWarning qDebug +#define qFatal qDebug +#define qCritical qDebug +#endif + static QSettings* globalSettings(){ if(!_globalSettings){ lockMutexStatic; diff --git a/src/ckb/kb.cpp b/src/ckb/kb.cpp index 22a8b986bf..eafeb30f3e 100644 --- a/src/ckb/kb.cpp +++ b/src/ckb/kb.cpp @@ -418,7 +418,7 @@ void Kb::frameUpdate(){ cmd.write(QString("\n@%1 ").arg(notifyNumber).toLatin1()); bind->update(cmd, changed); cmd.write(" "); - perf->update(cmd, changed); + perf->update(cmd, notifyNumber, changed); cmd.write("\n"); cmd.flush(); } diff --git a/src/ckb/kbanimwidget.ui b/src/ckb/kbanimwidget.ui index 29d25be7b8..f6559e4739 100644 --- a/src/ckb/kbanimwidget.ui +++ b/src/ckb/kbanimwidget.ui @@ -6,8 +6,8 @@ 0 0 - 613 - 300 + 675 + 218 diff --git a/src/ckb/kbbind.cpp b/src/ckb/kbbind.cpp index 2519113891..e3e3d36a6f 100644 --- a/src/ckb/kbbind.cpp +++ b/src/ckb/kbbind.cpp @@ -244,7 +244,7 @@ void KbBind::update(QFile& cmd, bool force){ if(_winLock) cmd.write(" unbind lwin rwin"); - // At last, send Macro definitions if avalilable. + // At last, send Macro definitions if available. // If no definitions are made, clear macro will be sent only to reset all macros, cmd.write(macros.toLatin1()); lastCmd = &cmd; diff --git a/src/ckb/kbbindwidget.ui b/src/ckb/kbbindwidget.ui index 797410f40a..0e2bb7194b 100644 --- a/src/ckb/kbbindwidget.ui +++ b/src/ckb/kbbindwidget.ui @@ -6,10 +6,16 @@ 0 0 - 581 - 473 + 533 + 230 + + + 0 + 0 + + Form @@ -61,7 +67,7 @@ - + 0 0 @@ -86,10 +92,16 @@ 0 + + QLayout::SetDefaultConstraint + + + true + - + 0 0 @@ -100,12 +112,6 @@ 0 - - - 140 - 16777215 - - Copy to mode... @@ -114,17 +120,26 @@ - + 0 0 + + 1 + Click to select keys Qt::AlignCenter + + 10 + + + -1 + @@ -141,12 +156,6 @@ 0 - - - 140 - 16777215 - - Reset @@ -161,10 +170,13 @@ Qt::Vertical + + QSizePolicy::Fixed + - 20 - 40 + 0 + 20 @@ -192,23 +204,11 @@ - + 0 0 - - - 0 - 280 - - - - - 16777215 - 280 - - 0 @@ -228,7 +228,7 @@ - + 0 0 @@ -239,11 +239,6 @@ - label - line - label_2 - line_2 - widget diff --git a/src/ckb/kblightwidget.ui b/src/ckb/kblightwidget.ui index 1d775f0398..100d2cfdf2 100644 --- a/src/ckb/kblightwidget.ui +++ b/src/ckb/kblightwidget.ui @@ -6,8 +6,8 @@ 0 0 - 632 - 540 + 596 + 450 @@ -71,6 +71,9 @@ + + QLayout::SetDefaultConstraint + @@ -78,8 +81,8 @@ - 40 - 20 + 0 + 0 @@ -105,28 +108,16 @@ - + - + 0 0 - - - 140 - 0 - - - - - 140 - 16777215 - - Change color... @@ -135,7 +126,7 @@ - + 0 0 @@ -151,23 +142,11 @@ - + 0 0 - - - 140 - 0 - - - - - 140 - 16777215 - - New animation... @@ -175,6 +154,22 @@ + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 20 + + + + @@ -184,8 +179,8 @@ - 40 - 20 + 0 + 0 @@ -197,10 +192,13 @@ Qt::Vertical + + QSizePolicy::Minimum + - 20 - 40 + 0 + 20 @@ -210,7 +208,7 @@ - + 0 0 @@ -239,6 +237,12 @@ + + + 0 + 0 + + 20 @@ -288,16 +292,25 @@ Qt::Vertical + + QSizePolicy::Fixed + - 20 - 40 + 0 + 20 + + + 0 + 0 + + 75 @@ -318,11 +331,11 @@ - - - 0 - 170 - + + + 0 + 0 + @@ -332,7 +345,14 @@ 0 - + + + + 0 + 0 + + + @@ -349,7 +369,7 @@ ColorButton QPushButton -
colorbutton.h
+
colorbutton.h
KbAnimWidget diff --git a/src/ckb/kbperf.cpp b/src/ckb/kbperf.cpp index 5293a5cc76..36fb400223 100644 --- a/src/ckb/kbperf.cpp +++ b/src/ckb/kbperf.cpp @@ -370,7 +370,7 @@ void KbPerf::angleSnap(bool newAngleSnap){ _needsUpdate = _needsSave = true; } -void KbPerf::update(QFile& cmd, bool force, bool saveCustomDpi){ +void KbPerf::update(QFile& cmd, int notifyNumber, bool force, bool saveCustomDpi){ if(!force && !_needsUpdate) return; emit settingsUpdated(); @@ -408,7 +408,7 @@ void KbPerf::update(QFile& cmd, bool force, bool saveCustomDpi){ cmd.write(output); } // Enable indicator notifications - cmd.write(" inotify all"); + cmd.write(QString("\n@%1 inotify all").arg(notifyNumber).toLatin1()); // Set indicator state const char* iNames[HW_I_COUNT] = { "num", "caps", "scroll" }; for(int i = 0; i < HW_I_COUNT; i++){ diff --git a/src/ckb/kbperf.h b/src/ckb/kbperf.h index 75e73d5364..60b3116432 100644 --- a/src/ckb/kbperf.h +++ b/src/ckb/kbperf.h @@ -109,7 +109,7 @@ class KbPerf : public QObject // Updates settings to the driver. Write "mode %d" first. Disable saveCustomDpi when writing a hardware profile or other permanent storage. // By default, nothing will be written unless the settings have changed. Use force = true or call setNeedsUpdate() to override. - void update(QFile& cmd, bool force = false, bool saveCustomDpi = true); + void update(QFile& cmd, int notifyNumber, bool force = false, bool saveCustomDpi = true); inline void setNeedsUpdate() { _needsUpdate = true; } // Get indicator status to send to KbLight diff --git a/src/ckb/kbprofiledialog.ui b/src/ckb/kbprofiledialog.ui index 3998678201..2caabbf505 100644 --- a/src/ckb/kbprofiledialog.ui +++ b/src/ckb/kbprofiledialog.ui @@ -6,8 +6,8 @@ 0 0 - 400 - 300 + 108 + 152 diff --git a/src/ckb/kbwidget.ui b/src/ckb/kbwidget.ui index 2cd9c877cc..28f29dcac5 100644 --- a/src/ckb/kbwidget.ui +++ b/src/ckb/kbwidget.ui @@ -6,10 +6,16 @@ 0 0 - 773 - 539 + 933 + 323 + + + 0 + 0 + + Form @@ -45,7 +51,7 @@ - + 82 0 @@ -79,7 +85,7 @@ - + 0 0 @@ -124,7 +130,7 @@ - + 0 0 @@ -151,7 +157,14 @@ 6 - + + + + 0 + 0 + + + @@ -173,7 +186,14 @@ 6 - + + + + 0 + 0 + + + diff --git a/src/ckb/keyaction.cpp b/src/ckb/keyaction.cpp index 0e3ce38bc2..4c872f0237 100644 --- a/src/ckb/keyaction.cpp +++ b/src/ckb/keyaction.cpp @@ -498,8 +498,8 @@ void KeyAction::adjustDisplay(){ /// macroAction ist called while being in the macro pane /// and clicking Apply with something in the Macro Text Box. /// It tags that input with "$macro:" for further recognition. -/// \param macroDef holds the String containing parts 2-4 of a complete macro definition. -/// \return QString holding the complete G-Key macro definition (parts 1-4) +/// \param macroDef holds the String containing parts 2-5 of a complete macro definition. +/// \return QString holding the complete G-Key macro definition (parts 1-5) /// QString KeyAction::macroAction(QString macroDef) { return QString ("$macro:%1").arg(macroDef); diff --git a/src/ckb/keyaction.h b/src/ckb/keyaction.h index b12d2d8e50..06d000bb59 100644 --- a/src/ckb/keyaction.h +++ b/src/ckb/keyaction.h @@ -37,7 +37,7 @@ class KeyAction : public QObject /// returns the complete string except the leading "$" /// (the $ may confuse some caller). /// \return QString - /// All 4 parts are returned in one QString. + /// All 5 parts are returned in one QString. /// If no definition exists, return "" /// inline QString macroFullLine() const { @@ -47,15 +47,16 @@ class KeyAction : public QObject ////////// /// \brief isValidMacro checks whether a keyAction contains a valid macro. /// This is done easily: If the macro action starts with $macro: - /// and has four elements, delimited by ":", we may assume, + /// and has five elements, delimited by ":", we may assume, /// that is a structural correct macro action. + /// If it has 4 entries only, it is an older definition and ok also. /// \return bool as true iff the macro definition contains all four elements. /// inline bool isValidMacro() const { if (isMacro()) { QStringList ret; ret =_value.split(":"); - return (ret.count() == 4); + return ((ret.count() >= 4) && (ret.count() <= 5)); } else { return false; } @@ -64,8 +65,10 @@ class KeyAction : public QObject ////////// /// \brief macroLine returns all interresting content for a macro definition. /// \return QStringList returns the Macro Key Definition, - /// Readble Macro String and - /// Readable Macro Comment as QStringList. + /// Readble Macro String, + /// Readable Macro Comment and + /// the original timing information (if it exists as a 5th part) + /// as QStringList. /// inline QStringList macroLine() const { if (isValidMacro()) { @@ -81,9 +84,25 @@ class KeyAction : public QObject /// \return QString macroContent /// inline QString macroContent() const { + // return isValidMacro() ? _value.split(":")[1].replace(QRegExp("=\\d+"), "") : ""; ///< Is used if we have ckb without delay handling return isValidMacro() ? _value.split(":")[1] : ""; } + ////////// + /// \brief macroTiming returns the macro key definition with original timing infos + /// (the fifth and up to now last part of the macro action). + /// If the implementation does not know anything about delays and has no 5th part, + /// return first part. + /// \return QString macroTiming + /// + inline QString macroTiming() const { + if (isValidMacro()) { + QStringList rval = _value.split(":"); + return (rval.length() == 4)? rval[1] : rval[4]; + } + return QString(""); + } + ////////// /// \brief Debug output for invalid macro Definitions /// @@ -94,6 +113,7 @@ class KeyAction : public QObject /// This sequence will program the keyboard and is hardly readable /// 3. Readable Macro String: This is displayed in pteMacroText /// 4. Readable Macro Comment:This is displayed in pteMacroComment + /// 5. completely unreadable original macro information with timing values /// void macroDisplay(); diff --git a/src/ckb/kperfwidget.cpp b/src/ckb/kperfwidget.cpp index 3af3c17cae..889c72a968 100644 --- a/src/ckb/kperfwidget.cpp +++ b/src/ckb/kperfwidget.cpp @@ -3,6 +3,10 @@ #include "modeselectdialog.h" #include +/// +/// \brief KPerfWidget::KPerfWidget sets up the UI for Keyboard Performace panel +/// \param parent +/// KPerfWidget::KPerfWidget(QWidget *parent) : QWidget(parent), ui(new Ui::KPerfWidget) @@ -51,10 +55,20 @@ KPerfWidget::KPerfWidget(QWidget *parent) : k95Widgets << ui->modeBox << ui->modeColorOn << ui->modeColorOff << ui->macroBox << ui->macroColorOn << ui->macroColorOff << ui->k95Label1 << ui->k95Label2 << ui->k95Label3 << ui->k95Label4 << ui->k95Label5 << ui->k95Label6 << ui->k95Line << ui->k95Spacer; } +/// +/// \brief KPerfWidget::~KPerfWidget nothing unusual - just delete the ui object +/// KPerfWidget::~KPerfWidget(){ delete ui; } +/// +/// \brief KPerfWidget::raw2Mode return hardware mode depending on setiings in \a sw_enable and \a hw_enable +/// \param sw_enable +/// \param hw_enable +/// \return the mode of operation for key-coloring and separate indicators. +/// \see KPerfWidget::mode2Raw for details. +/// KPerfWidget::HwMode KPerfWidget::raw2Mode(bool sw_enable, i_hw hw_enable){ if(sw_enable){ if(hw_enable == KbPerf::NORMAL) @@ -69,6 +83,20 @@ KPerfWidget::HwMode KPerfWidget::raw2Mode(bool sw_enable, i_hw hw_enable){ } } +/// +/// \brief KPerfWidget::mode2Raw Set values of sw_enable and hw_enable to hte value corresponding to input var mode +/// \param [IN] mode +/// \param [OUT] sw_enable +/// \param [OUT] hw_enable +/// mode determines how colors at a key and separate indicators should be handled: +/// Mode | sw | hw +/// _ | _ | _ +/// NORMAL | No color change at the key | use the separate indicator depending on key state +/// ALWAYS_ON | No color change at the key | switch on separate indicator +/// ALWAYS_OFF | No color change at the key | switch off separate indicator +/// RGB | use color change at the key depending on color sliders | switch off separate indicator +/// BOTH | use color change at the key depending on color sliders | use the separate indicator depending on key state +/// void KPerfWidget::mode2Raw(HwMode mode, bool& sw_enable, i_hw& hw_enable){ switch(mode){ case NORMAL: diff --git a/src/ckb/kperfwidget.h b/src/ckb/kperfwidget.h index a412aa3f3d..79e00355e4 100644 --- a/src/ckb/kperfwidget.h +++ b/src/ckb/kperfwidget.h @@ -43,6 +43,12 @@ class KPerfWidget : public QWidget BOTH }; HwMode raw2Mode(bool sw_enable, i_hw hw_enable); + + /// + /// \brief KPerfWidget::mode2Raw Set values of sw_enable and hw_enable to hte value corresponding to input var mode + /// \param [IN] mode + /// \param [OUT] sw_enable + /// \param [OUT] hw_enable void mode2Raw(HwMode mode, bool& sw_enable, i_hw& hw_enable); struct IndicatorUi { diff --git a/src/ckb/layoutdialog.ui b/src/ckb/layoutdialog.ui index f784233c72..1951db6821 100644 --- a/src/ckb/layoutdialog.ui +++ b/src/ckb/layoutdialog.ui @@ -6,8 +6,8 @@ 0 0 - 400 - 240 + 424 + 233 diff --git a/src/ckb/macroreader.cpp b/src/ckb/macroreader.cpp index a2f38d2bf6..d226cf0c23 100644 --- a/src/ckb/macroreader.cpp +++ b/src/ckb/macroreader.cpp @@ -1,5 +1,6 @@ #include #include "macroreader.h" +#include ////////// /// \class MacroReader @@ -24,7 +25,7 @@ void MacroReader::startWorkInAThread(int macroNumber, QString macroPath, QPlainT /// \class MacroReaderThread /// void MacroReaderThread::readMacro(QString line) { - /// \detail We want to see the keys as they appear in the macroText Widget. + /// \details We want to see the keys as they appear in the macroText Widget. /// /// Because it is possible to change the Focus via keyboard, /// we must set the focus on each call. @@ -37,8 +38,8 @@ void MacroReaderThread::readMacro(QString line) { ////////// /// \brief MacroReaderThread::run is the standard main function for a thread. -/// Tries to open a file several times -/// (in this case, it should be possible the first time. (The code was recycled from kb.cpp). +/// Tries to open a file \\ several times +/// (in this case, it should be possible the first time (the code was recycled from kb.cpp). /// /// While the file is open, read lines an signal them via metaObject() to the main thread. /// When the file is closed by the sender, close it as reader and terminate. @@ -63,9 +64,26 @@ void MacroReaderThread::run() { } } // Read data from notification node macroPath + // Count time between lines read from the interface QByteArray line; + timeval t; + gettimeofday(&t, NULL); + double tstart = t.tv_sec+(t.tv_usec/1000000.0); + bool firstline = true; + while(macroFile.isOpen() && (line = macroFile.readLine()).length() > 0){ QString text = QString::fromUtf8(line); + gettimeofday(&t, NULL); + double tnow = t.tv_sec+(t.tv_usec/1000000.0); + + // in the first line, there is only a delay "before start". Don't use it. + if (!firstline) { + text.prepend ("\n"); + text.prepend (QString::number ((tnow - tstart) * 1000000.0, 'f', 0)); + text.prepend ("="); + } else firstline = false; + tstart = tnow; + metaObject()->invokeMethod(this, "readMacro", Qt::QueuedConnection, Q_ARG(QString, text)); } qDebug() << "MacroReader::run() ends."; diff --git a/src/ckb/macroreader.h b/src/ckb/macroreader.h index 6b8299b2c5..5947591ed0 100644 --- a/src/ckb/macroreader.h +++ b/src/ckb/macroreader.h @@ -25,7 +25,7 @@ class MacroReaderThread : public QThread ////////// /// \brief macroNumber - /// Filenames of nofity channels have the structure /ckb1/notify + /// Filenames of nofity channels have the structure /ckb1/notify\ /// First part is hold in macroPath, the number is hold in macroNumber. /// macroNumber may range from 0 to 9. int macroNumber; diff --git a/src/ckb/mainwindow.ui b/src/ckb/mainwindow.ui index 4960a40de2..c84006886b 100644 --- a/src/ckb/mainwindow.ui +++ b/src/ckb/mainwindow.ui @@ -6,15 +6,15 @@ 0 0 - 1000 - 750 + 153 + 20 - - - 1000 - 750 - + + + 0 + 0 + ckb-next @@ -27,9 +27,21 @@ true + + + 0 + 0 + + + + + 0 + 0 + + -1 diff --git a/src/ckb/modeselectdialog.ui b/src/ckb/modeselectdialog.ui index 39e78b8935..f20535a272 100644 --- a/src/ckb/modeselectdialog.ui +++ b/src/ckb/modeselectdialog.ui @@ -6,8 +6,8 @@ 0 0 - 320 - 320 + 415 + 192 diff --git a/src/ckb/mperfwidget.ui b/src/ckb/mperfwidget.ui index 36aa0e08ac..313a867c7e 100644 --- a/src/ckb/mperfwidget.ui +++ b/src/ckb/mperfwidget.ui @@ -6,8 +6,8 @@ 0 0 - 932 - 637 + 773 + 802 @@ -1045,7 +1045,7 @@ ColorButton QPushButton -
colorbutton.h
+
colorbutton.h
diff --git a/src/ckb/rebindwidget.cpp b/src/ckb/rebindwidget.cpp index ee4e5f1206..7b4ed70230 100644 --- a/src/ckb/rebindwidget.cpp +++ b/src/ckb/rebindwidget.cpp @@ -192,6 +192,7 @@ void RebindWidget::setSelection(const QStringList& newSelection, bool applyPrevi ui->pteMacroBox->setPlainText(""); ui->pteMacroText->setPlainText(""); ui->pteMacroComment->setPlainText(""); + ui->txtBuffer->setText(""); // Fill in field and select tab according to action type bool mouse = act.isMouse(); if(mouse){ @@ -294,6 +295,12 @@ void RebindWidget::setSelection(const QStringList& newSelection, bool applyPrevi ui->pteMacroBox->setPlainText(act.macroContent()); ui->pteMacroText->setPlainText(act.macroLine()[1].replace("&das_IST_31N_col0n;", ":")); ui->pteMacroComment->setPlainText(act.macroLine()[2].replace("&das_IST_31N_col0n;", ":")); + // Set the invisible Buffer to the original timing information. + // For convenience / Migration from older versions: + // If the timing information is only "x", then ignore it by setting it to an empty QString. + ui->txtBuffer->setText(""); + if (act.macroTiming() != "x") ui->txtBuffer->setText(act.macroTiming()); + setCorrectRadioButton(act.macroContent()); } else { qDebug("RebindWidget::setSelection found invalid macro definition."); act.macroDisplay(); @@ -370,12 +377,25 @@ void RebindWidget::applyChanges(const QStringList& keys, bool doUnbind){ } bind->setAction(keys, KeyAction::programAction(ui->programKpBox->text(), ui->programKrBox->text(), kpStop | krStop)); } else if (ui->pteMacroBox->toPlainText().length() > 0) { - // G-key macro handling: - // Set the macro definiton for all keys selected (indeed, it may be multiple keys). - // First, concat the Macro Key Definion and the Macro plain text - // after escaping possible colos in the parts for Macro Text and Macro Comment. + /// G-key macro handling: + /// Set the macro definiton for all keys selected (indeed, it may be multiple keys). + /// First, concat the Macro Key Definion and the Macro plain text + /// after escaping possible colos in the parts for Macro Text and Macro Comment. + + /// But first, there is a special condition to handle: + /// You have recorded a macro with timing infos. + /// Afterwards you changed manually the timing infos in the pteMacroBox and press Apply. + /// In that case we must overwrite the txtBuffer to remember your changes. + if (ui->rb_delay_asTyped->isChecked()) ui->txtBuffer->setText(ui->pteMacroBox->toPlainText()); + + /// \todo There is still a bug in the state machine: + /// If you record a macro in asTyped-mode, switch to another mode + /// and change the vontent of the pteMacroBox manually, + /// then the changes are not saved in the timing buffer. + /// But anyhow, let's do more relevant things... QString mac; - mac = ui->pteMacroComment->toPlainText().replace(":", "&das_IST_31N_col0n;"); + mac = ui->txtBuffer->text(); + mac = ui->pteMacroComment->toPlainText().replace(":", "&das_IST_31N_col0n;") + ":" + mac; mac = ui->pteMacroText->toPlainText().replace(":", "&das_IST_31N_col0n;") + ":" + mac; mac = ui->pteMacroBox->toPlainText() + ":" + mac; bind->setAction(keys, KeyAction::macroAction(mac)); @@ -433,6 +453,7 @@ void RebindWidget::setBox(QWidget* box){ // Clear macro panel if (box != ui->pteMacroBox) { ui->pteMacroBox->setPlainText(""); + ui->txtBuffer->setText(""); helpStatus(1); } } @@ -711,13 +732,16 @@ void RebindWidget::on_btnStartMacro_clicked() { ui->unbindButton->setEnabled(false); ui->btnStartMacro->setEnabled(false); ui->btnStopMacro->setEnabled(true); + ui->rb_delay_asTyped->setEnabled(false); + ui->rb_delay_no->setEnabled(false); + ui->rb_delay_default->setEnabled(false); helpStatus(2); } } ////////// /// \brief RebindWidget::on_btnStopMacro_clicked ends the macro recording. -/// Notify channel ist closed, the ReaderThread is deleted if the notification is really down. +/// Notify channel ist closed, the ReaderThread is deleted when the notification is really down. /// /// Afterwards, the characters in the MacroBox are changed from KB-out format to cmd-in format. /// At last the UI changes to the new state. @@ -733,6 +757,9 @@ void RebindWidget::on_btnStopMacro_clicked() { ui->unbindButton->setEnabled(true); ui->btnStartMacro->setEnabled(true); ui->btnStopMacro->setEnabled(false); + ui->rb_delay_asTyped->setEnabled(true); + ui->rb_delay_no->setEnabled(true); + ui->rb_delay_default->setEnabled(true); helpStatus(3); } } @@ -741,6 +768,9 @@ void RebindWidget::on_btnStopMacro_clicked() { /// \brief RebindWidget::on_btnClearMacro_clicked changes the help info an the panel. /// The job of clearing the input panels is triggerd with signal/slot via the RebindWidget.ui file. /// +/// \todo I do not know what is the better solution with the delay-buttons in case of clicking clear: +/// Reset the button to the default value or do not touch it? Not clear is ignored. +/// void RebindWidget::on_btnClearMacro_clicked() { helpStatus(1); } @@ -768,16 +798,100 @@ void RebindWidget::helpStatus(int status) { ////////// /// \brief RebindWidget::convertMacroBox converts the macroBox content. /// The KB sends each keypress as "key [+|-]" +/// This is followed by timing information (delays between keystrokes). /// -/// the ckb-daemon needs a shorter format, only " [+|-]" +/// The ckb-daemon needs a shorter format, only " [+|-]=", +/// multiple entries are separated by comma. /// /// That function does the conversion. /// void RebindWidget::convertMacroBox() { QString in; - in = ui->pteMacroBox->toPlainText(); - in.replace (QRegExp("\n"), ","); - in.replace (QRegExp("key "), ""); + // Remember the original input stream before it is converted. + // In case of new choice of delay mode we have to restore it. + if (ui->txtBuffer->text() == "") { + ui->txtBuffer->setText(ui->pteMacroBox->toPlainText()); + in = ui->pteMacroBox->toPlainText(); + } else in = ui->txtBuffer->text(); + + in.replace (QRegExp("\n"), ","); // first join all in one line + in.replace (QRegExp("key "), ""); // then delete keyword "key" followed by space + in.replace (QRegExp(",="), "="); // at last join each keystroke with its delay parameter + + // How to deal with the delay params? + // Because the three radio buttons are mututally exclusive, + // we can run through the if-chain w/o conflicts. + // If rb_delay_asTyped is checked, do nothing, because that's the standard. + + if (ui->rb_delay_default->isChecked()) { + in.replace(QRegExp("=\\d+,"), ","); // Delete the timing infos, use default value + in.replace(QRegExp("=\\d+$"), ""); // The last entry is without comma + } + if (ui->rb_delay_no->isChecked()) { + in.replace(QRegExp("=\\d+,"), "=0,"); // Set timing infos to zero for no delay + in.replace(QRegExp("=\\d+$"), "=0"); // Again the last entry w/o comma + in.replace(QRegExp("([\\+\\-]\\w+),"), "\\1=0,"); // If no delay is given, force it to zero + in.replace(QRegExp("([\\+\\-]\\w+)$"), "\\1=0"); + } + + // Show the new format by replacing the older one. ui->pteMacroBox->setPlainText(in); } + +////////// +/// \brief RebindWidget::on_rb_delay_no_toggled +/// \param checked +/// The following slots are triggerd by changing the mutual exclusive radio buttons +/// when choosing the delay. +/// They are called, if the button ist enabled. +/// This first one should disable all delay. +/// +void RebindWidget::on_rb_delay_no_toggled(bool checked) +{ + convertMacroBox(); +} + +////////// +/// \brief RebindWidget::on_rb_delay_asTyped_toggled +/// \param checked +/// This button ist clicked to use the delay times, as they are recorded. +/// Returs a warning message, if we are not in the recording phase, +/// because then we don't have the delay times any more. +/// +void RebindWidget::on_rb_delay_asTyped_toggled(bool checked) +{ + convertMacroBox(); +} + +////////// +/// \brief RebindWidget::on_rb_delay_default_toggled +/// \param checked +/// This is as easy as the no-delay-button, because this means +/// take the default values. +/// +void RebindWidget::on_rb_delay_default_toggled(bool checked) +{ + convertMacroBox(); +} + +////////// +/// \brief RebindWidget::setCorrectRadioButton +/// \param macdef +/// Set the radiobutton for timing paramters according to +/// the context. +/// If no "=" followed by a number and comma can be found, it is the default button. +/// If "=" can be found and numbers with more than one digit (means: > 9), it is the "asTyped" button +/// Otherwise it is the "no" button. +/// +void RebindWidget::setCorrectRadioButton (QString macdef) { + if (!macdef.contains(QRegExp("=\\d+,"))) { + ui->rb_delay_default->setChecked(true); + return; + } + if (macdef.contains(QRegExp("=\\d\\d+,"))) { + ui->rb_delay_asTyped->setChecked(true); + return; + } + ui->rb_delay_no->setChecked(true); +} diff --git a/src/ckb/rebindwidget.h b/src/ckb/rebindwidget.h index 4fe8495356..ebb0fec411 100644 --- a/src/ckb/rebindwidget.h +++ b/src/ckb/rebindwidget.h @@ -1,5 +1,5 @@ -#ifndef BINDDIALOG_H -#define BINDDIALOG_H +#ifndef REBINDWIDGET_H +#define REBINDWIDGET_H #include #include "kbbind.h" @@ -70,6 +70,12 @@ private slots: void on_btnStopMacro_clicked(); void on_btnClearMacro_clicked(); + void on_rb_delay_no_toggled(bool checked); + + void on_rb_delay_asTyped_toggled(bool checked); + + void on_rb_delay_default_toggled(bool checked); + private: Ui::RebindWidget *ui; @@ -83,6 +89,8 @@ private slots: // Show some help info void helpStatus(int status); + void setCorrectRadioButton (QString macdef); + KbBind* bind; KbProfile* profile; QStringList selection; @@ -98,4 +106,4 @@ private slots: MacroReader* macReader; ///< \brief macReader holds the MacroReader when macro recording starts. }; -#endif // BINDDIALOG_H +#endif // REBINDWIDGET_H diff --git a/src/ckb/rebindwidget.ui b/src/ckb/rebindwidget.ui index f88c9663c1..a919673145 100644 --- a/src/ckb/rebindwidget.ui +++ b/src/ckb/rebindwidget.ui @@ -6,12 +6,12 @@ 0 0 - 687 - 342 + 958 + 477
- + 0 0 @@ -19,12 +19,21 @@ + + + 0 + 0 + + - 0 + 5 + + + true - + 0 0 @@ -34,7 +43,7 @@ - 0 + 6 @@ -97,31 +106,28 @@ - - + + - Qt::Vertical + Qt::Horizontal - 20 - 40 + 0 + 20 - - + + - Qt::Horizontal - - - QSizePolicy::Fixed + Qt::Vertical - 5 - 20 + 20 + 50 @@ -129,7 +135,7 @@ - Typing: + T&yping: false @@ -170,7 +176,7 @@ - Media: + &Media: false @@ -193,32 +199,6 @@ - - - - Qt::Horizontal - - - - 0 - 20 - - - - - - - - Qt::Horizontal - - - - 0 - 20 - - - - @@ -229,7 +209,7 @@ - Miscellaneous: + Misce&llaneous: false @@ -261,15 +241,15 @@ - + Qt::Horizontal - 40 - 20 + 0 + 0 @@ -277,6 +257,12 @@
+ + + 0 + 0 + + Mouse @@ -547,7 +533,7 @@ - Wheel: + Whee&l: @@ -613,6 +599,12 @@ + + + 0 + 0 + + Animation @@ -668,8 +660,8 @@ - 438 - 20 + 0 + 0 @@ -737,7 +729,7 @@ - + 0 0 @@ -747,7 +739,7 @@ - 0 + 6 @@ -969,35 +961,15 @@
- - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - + + + 0 + 0 + + Program @@ -1012,8 +984,8 @@ - 20 - 5 + 0 + 0 @@ -1024,18 +996,24 @@ Qt::Horizontal - QSizePolicy::Fixed + QSizePolicy::Preferred - 5 - 20 + 0 + 0
+ + + 0 + 0 + + Launch program on key press: @@ -1051,8 +1029,8 @@ - 40 - 20 + 0 + 0 @@ -1090,8 +1068,8 @@ - 40 - 20 + 0 + 0 @@ -1116,6 +1094,12 @@ + + + 0 + 0 + + Single instance @@ -1123,6 +1107,12 @@ + + + 0 + 0 + + Run indefinitely @@ -1147,8 +1137,8 @@ - 40 - 20 + 0 + 0 @@ -1176,6 +1166,12 @@ + + + 0 + 0 + + Single instance @@ -1183,6 +1179,12 @@ + + + 0 + 0 + + Run indefinitely @@ -1202,8 +1204,8 @@ - 40 - 20 + 0 + 0 @@ -1214,7 +1216,7 @@ - + 0 0 @@ -1230,6 +1232,12 @@ + + + 0 + 0 + + Macro @@ -1238,11 +1246,14 @@ 40 12 - 241 - 171 + 239 + 36 + + QLayout::SetNoConstraint + @@ -1250,16 +1261,6 @@ - - - - Qt::WheelFocus - - - true - - - @@ -1268,10 +1269,10 @@ - 420 + 450 20 311 - 31 + 41 @@ -1317,10 +1318,10 @@ - 420 - 60 + 450 + 70 311 - 61 + 121 @@ -1354,15 +1355,24 @@ - 300 + 310 20 - 119 - 81 + 131 + 101 + + QLayout::SetDefaultConstraint + + + + 0 + 0 + + Macro Comment @@ -1370,6 +1380,17 @@ + + + 0 + 0 + + + + + 8 + + Macro Text @@ -1381,40 +1402,64 @@ 300 - 160 - 436 - 30 + 230 + 461 + 33 - - - - Macro recording - - - false + + + 0 + 0 + + + + Stop + + + + + + + + 0 + 0 + + - Stop + Clear + + + 0 + 0 + + Start - - - - Clear + + + + + 0 + 0 + + + + Macro recording @@ -1423,22 +1468,175 @@ - 300 - 130 - 431 - 27 + 50 + 200 + 711 + 21 + + + 0 + 0 + + Comment label for help + + + + 40 + 50 + 239 + 101 + + + + + 0 + + + QLayout::SetMinimumSize + + + + + + false + + + + 40 + 30 + 0 + 0 + + + + + 0 + 0 + + + + + 0 + 0 + + + + Qt::NoContextMenu + + + + + + 0 + + + + + + + + + QFrame::Plain + + + 0 + + + + + + Qt::PlainText + + + 0 + + + Qt::NoTextInteraction + + + + + + 210 + 170 + 91 + 21 + + + + configure delays between keystrokes: Remember delays as they had been typed while the macro definition process + + + font: 10pt ; color: green; + + + as typed + + + true + + + + + + 129 + 170 + 71 + 21 + + + + configure delays between keystrokes: Set delay to default values: 20us up to 15 chars, 200us above + + + font: 10pt ; color: green; + + + default + + + false + + + true + + + + + + 45 + 170 + 74 + 21 + + + + configure delays between keystrokes: Disable delay + + + font: 10pt ; color: green; + + + No delay + + + txtBuffer pteMacroComment layoutWidget1 layoutWidget pteMacroText layoutWidget_2 lbl_macro + pteMacroBox + rb_delay_asTyped + rb_delay_default + rb_delay_no @@ -1449,16 +1647,25 @@ Qt::Horizontal + + QSizePolicy::Minimum + - 40 - 20 + 0 + 0 + + + 0 + 0 + + Unbind @@ -1466,6 +1673,12 @@ + + + 0 + 0 + + Reset to Default @@ -1473,6 +1686,12 @@ + + + 0 + 0 + + Cancel @@ -1480,6 +1699,12 @@ + + + 0 + 0 + + Apply @@ -1548,9 +1773,9 @@ - cancelButton + btnClearMacro clicked() - pteMacroBox + pteMacroText clear() @@ -1566,7 +1791,7 @@ btnClearMacro clicked() - pteMacroBox + pteMacroComment clear() @@ -1582,7 +1807,23 @@ btnClearMacro clicked() - pteMacroText + btnStopMacro + animateClick() + + + 711 + 238 + + + 618 + 238 + + + + + cancelButton + clicked() + txtBuffer clear() @@ -1598,7 +1839,39 @@ btnClearMacro clicked() - pteMacroComment + txtBuffer + clear() + + + 655 + 219 + + + 339 + 154 + + + + + btnStartMacro + clicked() + txtBuffer + clear() + + + 485 + 228 + + + 352 + 156 + + + + + unbindButton + clicked() + txtBuffer clear() @@ -1612,10 +1885,10 @@ - btnClearMacro + resetButton clicked() - btnStopMacro - animateClick() + txtBuffer + clear() 671