Orbital Mouse
Pascal Getreuer, 2023-12-31
Overview
QMK has a Mouse Keys feature that controls the mouse using keyboard keys. You can then avoid switching hands between keyboard and mouse, in theory a substantial ergonomic advantage. In reality, this interface is pretty awkward.
This post describes an experiment: Orbital Mouse. I find it is easier to control than QMK’s Mouse Keys, or at least it is more fun. To be clear, it still pales in comparison to real mouse or trackball hardware.
What is Orbital Mouse
Orbital Mouse is a userspace library that replaces QMK Mouse Keys. The pointer moves according to a heading direction. Two keys move forward and backward along that direction while another two keys steer.
Figures have been enlarged and annotated in blue for sake of visualizing the control scheme. In real use, sadly, there are no annotations.
Orbital Mouse is essentially tank controls, like the player movement in earlier Resident Evil and Tomb Raiders games, but no monsters chasing your cursor.
When steering, the cursor rotates (orbits!) around a circle to indicate change in heading direction. This rotation effect is additionally useful as a means of fine-scale control, good for hitting small targets like menu items and toolbar buttons.
Add Orbital Mouse to your keymap
Step 1: Use the Orbital Mouse keycodes in your layout in keymap.c. A description of the keycodes is in the next section. A suggested right-handed layout is:
, OM_BTNS, OM_U , OM_BTN2, OM_SEL1,
OM_DBLS, OM_L , OM_D , OM_R , OM_SEL2,
OM_HLDS, OM_W_D , OM_W_U , OM_BTN3, OM_SEL3, OM_RELS
Step 2: Include the orbital_mouse header by adding near the top of keymap.c:
#include "features/orbital_mouse.h"`
Step 3: Still in keymap.c, handle Orbital Mouse by
defining (or adding to) process_record_user()
and
matrix_scan_user()
as
bool process_record_user(uint16_t keycode, keyrecord_t* record) {
if (!process_orbital_mouse(keycode, record)) { return false; }
// Your macros ...
return true;
}
void matrix_scan_user(void) {
();
orbital_mouse_task
// Other tasks ...
}
Step 4: In your rules.mk
file, add
SRC += features/orbital_mouse.c
MOUSE_ENABLE = yes
Optionally, if Mouse Keys is enabled, you may disable it with
“MOUSEKEYS_ENABLE = no
” to save some firmware space.
Step 5: In the directory containing keymap.c, create
a features
subdirectory and copy orbital_mouse.h
and orbital_mouse.c
there.
Keycodes
Orbital Mouse is controlled with the following keycodes.
Keycode | Description |
---|---|
OM_U |
Move forward. |
OM_D |
Move backward. |
OM_L |
Steer left (counter-clockwise). |
OM_R |
Steer right (clockwise). |
OM_BTNS |
Press the selected mouse button. |
OM_HLDS |
Hold the selected mouse button. |
OM_RELS |
Release the selected mouse button. |
OM_DBLS |
Double click the selected mouse button. |
OM_SEL n |
Select mouse button n, for n = 1, …, 8. |
OM_BTN n |
Press mouse button n, for n = 1, …, 8. |
OM_W_U |
Mouse wheel up. |
OM_W_D |
Mouse wheel down. |
OM_W_L |
Mouse wheel left. |
OM_W_R |
Mouse wheel right. |
Of course, you don’t have to use all of them in your layout, just use what is useful. There are many ways these keys could be arranged in a layout. I leave it to you to explore.
Selected mouse button
Keycode OM_BTNS
and its companions OM_HLDS
,
OM_RELS
, OM_DBLS
make use of the “selected”
mouse button. Initially, mouse button 1 is selected. Keycodes
OM_SEL1
… OM_SEL8
set the selected mouse
button.
OM_HLDS
and OM_RELS
are useful when a mouse
button must be held for an extended duration, like click and drag
inputs. Tapping OM_HLDS
holds down the selected mouse
button. The button is held until OM_RELS
is tapped to
release it.
Mouse wheel controls
Keycodes OM_W_U
, OM_W_D
,
OM_W_L
, OM_W_R
scroll the mouse wheel. Unlike
cursor movement, mouse wheel navigation follows usual Cartesian
up/down/left/right controls.
Configuration
Speed curve
Mouse keys must facilitate both large motions across the screen as well as precise fine motions for selecting menu items and such. An idea from the X Windows System is that holding a mouse key should begin moving the cursor at a slow speed and accelerate to faster speed. A “speed curve” defines how this speed changes as a function of time.
ORBITAL_MOUSE_SPEED_CURVE
defines the speed curve as a
table of 16 values, representing speed in pixels per 16-ms interval. The
nth table entry is the movement speed after holding the key for
0.256n seconds. The entries are piecewise linearly interpolated
at times between these points. Table entries are uint8_t
values in the range 0–255, larger values meaning faster movement.
The default curve is a bilevel scheme, beginning at a lower speed of 24, for fine-scale motions, then transitions after a second to a higher speed of 66, for large-scale motions:
#define ORBITAL_MOUSE_SPEED_CURVE \
{24, 24, 24, 32, 58, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66}
// | | | | |
// t = 0.000 1.024 2.048 3.072 3.840 s
Define ORBITAL_MOUSE_SPEED_CURVE
in your config.h to use
a different speed curve. You can represent just about any speed curve
you like in this manner.
In QMK Mouse Keys, the default Accelerated mode is based on X Window System MouseKeysAccel. Here is a family of curves following that design:
\[ s(t) = s_0 + (s_T - s_0) (t / T)^p. \]
#define ORBITAL_MOUSE_SPEED_CURVE \
{24, 28, 36, 47, 59, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66}
// | | | | |
// t = 0.000 1.024 2.048 3.072 3.840 s
Time (s)
Options
Orbital Mouse can be further customized by tuning the following options in config.h.
#define ORBITAL_MOUSE_RADIUS 36
#define ORBITAL_MOUSE_WHEEL_SPEED 0.2
#define ORBITAL_MOUSE_DBL_DELAY_MS 50
ORBITAL_MOUSE_RADIUS
is the radius in pixels of the
circle that the cursor rotates or “orbits” around when steering. Must be
in [0, 63]. Default is 36
.
ORBITAL_MOUSE_WHEEL_SPEED
is the mouse wheel speed in
units per 16-ms interval, specified as a double value. Must be in [0,
3.99]. Default is 0.2
.
ORBITAL_MOUSE_DBL_DELAY_MS
is the delay in ms between
clicks with OM_DBLS
double clicking. If this is too low,
the computer might debounce the double click. Default is
50
.
Functions
set_orbital_mouse_speed_curve(speed_curve)
sets the speed curve at run time, wherespeed_curve
points to auint8_t
array of 16 values. This enables dynamically switching between multiple speed curves. Calling this function withNULL
resets to the speed curve defined byORBITAL_MOUSE_SPEED_CURVE
.get_orbital_mouse_angle()
gets the heading direction as a value in the range 0–63. Value 0 represents up, and values increase in counter-clockwise direction.set_orbital_mouse_angle(angle)
sets the heading direction. The function wraps the givenangle
to the 0–63 range.
Explanation
If you are interested in the technical details, here are some notes on how Orbital Mouse is implemented.
While Orbital Mouse controls are actively being used, it runs a task function once every 16 ms. This function updates the state, moving and turning according to which keys are currently held, then sends a mouse report to the host.
Details:
The mouse report is created by filling a
report_mouse_t
struct, then passing this struct tohost_mouse_send()
.Orbital Mouse needs 28 keycodes. While keycodes for userspace features are conventionally allocated in the user-defined keycode range, that range is limited (32 keycodes). It would be unreasonable to allocate Orbital Mouse’s keys there. Being a Mouse Keys replacement, we repurpose the Mouse Keys keycodes (
KC_MS_U
,KC_BTN1
, etc.) for the analogous functions in Orbital Mouse. We also repurpose the block of keycodesUC(0x41)
toUC(0x49)
. These keycode represent Unicode input of ASCII characters, which seems unlikely to be missed.The Orbital Mouse implementation uses no floating point arithmetic at run time. Instead, fixed point arithmetic is used to represent fractional values. The internal functions
scaled_sin()
andscaled_cos()
evaluate sine and cosine for a desired phase and amplitude, returning the result as a Q6.8 value.