Now we arrive at the actual definition of the device controls:
(configure-device ... #:controls (list (list ak1:left remap-button #:event-code key-home) (list ak1:middle (make-remap-button-toggle) #:event-code key-space) (list ak1:right remap-button #:event-code key-end) (list ak1:knob (make-abs-knob-to-button) #:neg-event-code key-leftbrace #:pos-event-code key-rightbrace #:neg-mod-keys (list key-leftshift) #:pos-mod-keys (list key-leftshift) #:knob-max (ak1:knob-max) #:invert #t)) ...)
The #:controls option takes a list of controls. Each control is, in turn, a list consisting of the control definition, the name of a callback function to be called when the control’s value changes (button pressed, knob twiddled, etc.), and a number of optional arguments to be passed to the callback function. Optional arguments can simply be omitted if you’re happy with the default behavior.
The AK1’s controls have all already been defined in ‘(librekontrol devices ni-ak1)’ and are bound to the symbols ak1:left, ak1:middle, ak1:right and ak1:knob. Each control is defined as having an associated input event and, optionally, an ALSA control (usually an LED). If you are configuring a new device, you will need to define the controls yourself via the make-control procedure.
Each control is connected to the listed callback function. When Librekontrol detects an input event originating from that control, it runs the callback function. Using the high-level configuration, currently only one callback function can be associated with an input event. If you want to call a series of procedures when an event occurs, you must use the low-level API to add event hooks.
(configure-device ... #:controls (list (list ak1:left remap-button #:event-code key-home) ... (list ak1:right remap-button #:event-code key-end) ...) ...)
We see a few different callbacks here. ak1:left and ak1:right are both connected to the remap-button callback. This callback simply generates a new input event that sends a different event code as specified by the #:event-code option (HOME and END, respectively). When the button is pressed in, the associated LED turns on, and when it is released, the LED turns off.
(configure-device ... #:controls (list ... (list ak1:middle (make-remap-button-toggle) #:event-code key-space) ...) ...)
ak1:middle is connected to something that looks fairly similar, however it’s wrapped in parentheses: ‘(make-remap-button-toggle)’. What’s going on here? Because it’s wrapped in parentheses, Guile interprets this as a call to a procedure, which it is. The make-remap-button-toggle procedure actually returns a new callback function. We have to do this because we want the middle button to behave like a toggle switch: press it once to send the SPC event and turn on its LED; press it a second time to send the SPC event again and turn off its LED. We need a way to remember from one press to another whether the toggle is “on” or “off”. One way to do this would be to require the user to maintain a variable that stores the current state and to pass it to the procedure as an argument. Another way is to take advantage of something called a lexical closure (this is beyond the scope of this tutorial but see (guile)About Closure), but this requires us to create a new function for each control whose state we want to remember.
(configure-device ... #:controls (list ... (list ak1:knob (make-abs-knob-to-button) #:neg-event-code key-leftbrace #:pos-event-code key-rightbrace #:neg-mod-keys (list key-leftshift) #:pos-mod-keys (list key-leftshift) #:knob-max (ak1:knob-max) #:invert #t)) ...)
Finally, ak1:knob is connected to ‘(make-abs-knob-to-button)’. Once again, this is a call to a procedure that returns a callback function. This one converts an absolute-position control like a knob and remaps it to keypresses. Here the state that we need to remember is related to determining whether the knob’s position is increasing or decreasing. We then associate a keypress event to negative events (rotating counter-clockwise; #:neg-event-code) and one to positive events (clockwise; #:pos-event-code). We also can specify a list of modifier keys (Shift, Ctrl, etc.) that we want to apply to the new keypress events (#:neg-mod-keys/#:pos-mod-keys; note that we could have done the same for any of our earlier remap-button callbacks).
We must specify the maximum value that the original input event can generate, which is stored in a parameter ak1:knob-max, defined in ‘(librekontrol devices ni-ak1)’. In general, such maximum values are defined as parameters, the values of which can be retrieved by calling them like procedures (e.g. ‘(ak1:knob-max)’). See (guile)Parameters for more information on how you might take advantage of that. For now, though, we just want to take the defined maximum value (which happens to be 999, if you’re curious).
The last option to ak1:knob is ‘#:invert #t’. This sets the invert option to “true”. This is necessary because the AK1’s behavior is not what one would intuitively expect: rotating the knob clockwise decreases the absolute position. Setting #:invert makes the position increase with clockwise rotation as expected.