Ploopy QMK Firmware Customized for the Trackball Mini

Note: Updated with a bugfix where the two tap dance events could interfere with each other, this post has been revised with a fix along with the updated firmware to address this.

I have three ploopy trackballs which I find a great alternative to regular pointing devices. I like trackballs as well as split keyboards I find they really help with RSI and long term comfort. You can learn more about ploopy trackballs here.

While the ploopy trackballs work with the stock firmware there are some customizations, which I think warrant running custom firmware:

  • Setting a precision mode to allow fine adjustments, I find this useful for CAD and the occasional FPS game.
  • Being able to toggle through different DPI settings to increase or decrease the sensitivity.
  • Using a "drag scroll", which allows the ball to be used like a scroll wheel, useful for scrolling through a long thread or document.

I have my ploopy classic setup just like this, but to setup the ploopy mini trackball took a little more work, so I thought I'd document it.

While QMK is geared more towards keyboards, and I'll make another post on that later, it also offers some of the same customization capabilities for mice and other input devices like trackballs.

Given we only have 5 mouse buttons available and in normal operation we want to use all 5 buttons plus the other capabilities we can use two key features of QMK, layers and Tap Dance to add more functionality than just the 5 buttons alone would allow.

I have the trackball setup with the usual left, middle, right, forward and back buttons. We can use layers to take care of the other features we want. If you're not familiar with layers, a layer works a little like a shift key or function key on a laptop. In this setup, holding the forward button or mouse button 5 enables the second layer so when that button is held then left click or button 1 toggles drag scroll and right click or button 2 rotates through the DPI settings.

Rather than a layer we'll use tap dance so when the back button or button 4 is held we'll switch to a precision mode, when tapped it will work as the back button.

First off, to use a custom QMK firmware you'll need a working QMK environment, I won't cover that in this blog, but this link should take you through it.

Once that is setup, we need to make a couple of changes to rules.mk, add or change the following rules:

TAP_DANCE_ENABLE = yes
MOUSEKEY_ENABLE = yes

We need to enable tap dance, ordinarily we would use mod-tap but in this instance the mouse keys are not in the basic keycode set.

Currently, the kc argument of MT() is limited to the Basic Keycode set, meaning you can't use keycodes like LCTL(), KC_TILD, or anything greater than 0xFF. This is because QMK uses 16-bit keycodes, of which 3 bits are used for the function identifier, 1 bit for selecting right or left mods, and 4 bits to tell which mods are used, leaving only 8 bits for the keycode. Additionally, if at least one right-handed modifier is specified in a Mod-Tap, it will cause all modifiers specified to become right-handed, so it is not possible to mix and match the two - for example, Left Control and Right Shift would become Right Control and Right Shift.

MOUSEKEY_ENABLED allows us to reference the mouse keys in our keymap.c to register mouse button 4 and mouse button 5 when tapped, but when held as explained above, something entirely different.

First off, modify trackball_mini.c, we're going to add a few dpi resolutions and also provide a precision equivalent, so when the precision button is held we drop to roughly half the dpi, it's important that the array sizes are the same:


#define PLOOPY_DPI_OPTIONS { CPI500, CPI750, CPI1000, CPI1375 }
#define PLOOPY_DPI_PREC_OPTIONS { CPI250, CPI375, CPI500, CPI625 }
#define PLOOPY_DPI_DEFAULT 3

In addition, we set a default DPI, I find 1000 works well for me.

We need to define the precision array in trackball_mini.c, the existing options should already be in place:

uint16_t dpi_prec_array[] = PLOOPY_DPI_PREC_OPTIONS;

The CPI* definitions are in adns5050.h if you want to add more DPI options to the two arrays above just make sure the PLOOPY_DPI_OPTIONS and the PLOOPY_DPI_PREC_OPTIONS are the same size, as defined above, both hold 4 entries.

// CPI values
#define CPI125 0x11
#define CPI250 0x12
#define CPI375 0x13
#define CPI500 0x14
#define CPI625 0x15
#define CPI750 0x16
#define CPI875 0x17
#define CPI1000 0x18
#define CPI1125 0x19
#define CPI1250 0x1a
#define CPI1375 0x1b

Next we need to modify trackball_mini.h and make the dpi_array and dpi_prec_array available to our keymap.c file.

extern uint16_t dpi_array[];
extern uint16_t dpi_prec_array[];

Create a new keymap entry by creating a directory under your qmk_firmware:

mkdir ~/qmk_firmware/keyboards/ploopyco/trackball_mini/keymaps/custom

Then create a new keymaps.c file in the directory you just created:

/* Copyright 2020 Christopher Courtney, aka Drashna Jael're  (@drashna) <drashna@live.com>
 * Copyright 2019 Sunjun Kim
 * Copyright 2020 Ploopy Corporation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
#include QMK_KEYBOARD_H
#define PLOOPY_DRAGSCROLL_INVERT 1
#define TAPPING_TERM 175


// Tap Dance keycodes
enum td_keycodes {
    MSE_BTN5_LAYR_1, // Our example key: Forward mouse button when held, switch to layer 1,  forward mouse button when tapped.
    MSE_BTN4_PRCSN   // Hold mse btn4 toggles precision mode, tapped send back.
};

// Define a type containing as many tapdance states as you need
typedef enum {
    TD_NONE,
    TD_UNKNOWN,
    TD_SINGLE_TAP,
    TD_SINGLE_HOLD,
    TD_DOUBLE_SINGLE_TAP,
    TD_DOUBLE_HOLD,
    TD_DOUBLE_TAP
} td_state_t;

// Create a global instance of the tapdance state type
static td_state_t td_state; 
bool td_precision_state = false;

// Declare your tapdance functions:

// Function to determine the current tapdance state
td_state_t cur_dance(qk_tap_dance_state_t *state);

// `finished` and `reset` functions for each tapdance keycode
void mseBtn4_finished(qk_tap_dance_state_t *state, void *user_data);
void mseBtn4_reset(qk_tap_dance_state_t *state, void *user_data);
void mseBtn5_finished(qk_tap_dance_state_t *state, void *user_data);
void mseBtn5_reset(qk_tap_dance_state_t *state, void *user_data);

// Define two layers
const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
    [0] = LAYOUT( /* Base */
        KC_BTN1, KC_BTN3, KC_BTN2,
        TD(MSE_BTN4_PRCSN), TD(MSE_BTN5_LAYR_1) 
    ),
    [1] = LAYOUT(
        DRAG_SCROLL, _______, DPI_CONFIG,
          _______, _______
    )
};


// Determine the tapdance state to return
td_state_t cur_dance(qk_tap_dance_state_t *state) {
    if (state->count == 1) {
        // Interrupted means some other button was pressed in the tapping term
        if (state->interrupted || !state->pressed) {
            xprintf("TD_SINGLE_TAP\n");
            return TD_SINGLE_TAP;
        } else {
            xprintf("TD_SINGLE_HOLD\n");
            return TD_SINGLE_HOLD;
        }
    }

    if (state->count == 2) {
        if (state->interrupted) {
            xprintf("TD_DOUBLE_SINGLE_TAP\n");
            return TD_DOUBLE_SINGLE_TAP;
        } else if (state->pressed) {
            xprintf("TD_DOUBLE_HOLD\n");
            return TD_DOUBLE_HOLD;
        } else {
            xprintf("TD_DOUBLE_TAP\n");
            return TD_DOUBLE_TAP;
        }

    } else {
        xprintf("TD_UNKNOWN\n");
        return TD_UNKNOWN; // Any number higher than the maximum state value you return above
    }
}

void mseBtn4_finished(qk_tap_dance_state_t *state, void *user_data) {
    td_state = cur_dance(state);
    switch (td_state) {
        case TD_SINGLE_HOLD:
            xprintf("Hold for button 4 finished\n");
            is_drag_scroll = true;
            btn4_held = true;
        break;
        case TD_DOUBLE_TAP:
            if (!precision_mode) {
                pointing_device_set_cpi(dpi_prec_array[keyboard_config.dpi_config]);
                precision_mode = true;
            } else {
                pointing_device_set_cpi(dpi_array[keyboard_config.dpi_config]);
                precision_mode = false;
            }
        break;
        default:
        break;
    }
}

void mseBtn4_reset(qk_tap_dance_state_t *state, void *user_data) {
    switch (td_state) {
        case TD_SINGLE_TAP:
            xprintf("reset button 4 sending tap code\n");
            tap_code16(KC_BTN4);
        break;
        case TD_SINGLE_HOLD:
            xprintf("Hold for button 4 reset\n");
            is_drag_scroll = false;
            btn4_held = false;
        break;
        default:
        break;
    }
}

// Handle the possible states for each tapdance keycode you define:
void mseBtn5_finished(qk_tap_dance_state_t *state, void *user_data) {
    td_state = cur_dance(state);
    switch (td_state) {
        case TD_SINGLE_HOLD:
            xprintf("Turning on layer 1 for button 5\n");
            layer_on(1);
        break;
        default:
        break;
    }
}

void mseBtn5_reset(qk_tap_dance_state_t *state, void *user_data) {
    switch (td_state) {
        case TD_SINGLE_TAP:
            // xprintf("UN-Registering button 5\n");
            // unregister_code16(KC_BTN5);
            xprintf("reset button 5 sending tap code\n");
            tap_code16(KC_BTN5);
        break;
        case TD_SINGLE_HOLD:
            xprintf("Turning off layer 1 for button 5\n");
            layer_off(1);
        break;
        default:
        break;
    }
}

// Define `ACTION_TAP_DANCE_FN_ADVANCED()` for each tapdance keycode, passing in `finished` and `reset` functions
qk_tap_dance_action_t tap_dance_actions[] = {
    [MSE_BTN5_LAYR_1] = ACTION_TAP_DANCE_FN_ADVANCED(NULL, mseBtn5_finished, mseBtn5_reset),
    [MSE_BTN4_DRAG]  = ACTION_TAP_DANCE_FN_ADVANCED(NULL, mseBtn4_finished, mseBtn4_reset)
};

There's alot to unpack here, but the highlights are defining two layers:

const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
    [0] = LAYOUT( /* Base */
        KC_BTN1, KC_BTN3, KC_BTN2,
        TD(MSE_BTN4_PRCSN), TD(MSE_BTN5_LAYR_1) 
    ),
    [1] = LAYOUT(
        DRAG_SCROLL, _______, DPI_CONFIG,
          _______, _______
    )
};

You can see switching to layer 1 makes button 1 now toggle drag scroll and pressing button 3 to cycle through the DPI modes.

Tap dance is added to button 4 which when held toggles a precision mode but when tapped we send a button 4 press.

void mseBtn4_finished(qk_tap_dance_state_t *state, void *user_data) {
    td_state = cur_dance(state);
    switch (td_state) {
        case TD_SINGLE_HOLD:
            xprintf("Hold for button 4 finished\n");
            is_drag_scroll = true;
            btn4_held = true;
        break;
        case TD_DOUBLE_TAP:
            if (!precision_mode) {
                pointing_device_set_cpi(dpi_prec_array[keyboard_config.dpi_config]);
                precision_mode = true;
            } else {
                pointing_device_set_cpi(dpi_array[keyboard_config.dpi_config]);
                precision_mode = false;
            }
        break;
        default:
        break;
    }
}

Once the tap dance for button 4 has completed we need to either send the back button event BTN4 or if holding button 4 and releasing exit the drag scroll mode.

void mseBtn4_reset(qk_tap_dance_state_t *state, void *user_data) {
    switch (td_state) {
        case TD_SINGLE_TAP:
            xprintf("reset button 4 sending tap code\n");
            tap_code16(KC_BTN4);
        break;
        case TD_SINGLE_HOLD:
            xprintf("Hold for button 4 reset\n");
            is_drag_scroll = false;
            btn4_held = false;
        break;
        default:
        break;
    }
}

For button 5, when held we turn on layer 1 which changes the function assigned to button 1 or button 3.

// Handle the possible states for each tapdance keycode you define:
void mseBtn5_finished(qk_tap_dance_state_t *state, void *user_data) {
    td_state = cur_dance(state);
    switch (td_state) {
        case TD_SINGLE_HOLD:
            xprintf("Turning on layer 1 for button 5\n");
            layer_on(1);
        break;
        default:
        break;
    }
}

And when we release button 5 send the button 5 press ore turn off layer 1.

void mseBtn5_reset(qk_tap_dance_state_t *state, void *user_data) {
    switch (td_state) {
        case TD_SINGLE_TAP:
            // xprintf("UN-Registering button 5\n");
            // unregister_code16(KC_BTN5);
            xprintf("reset button 5 sending tap code\n");
            tap_code16(KC_BTN5);
        break;
        case TD_SINGLE_HOLD:
            xprintf("Turning off layer 1 for button 5\n");
            layer_off(1);
        break;
        default:
        break;
    }
}

Once we have our changes in place we can build the firmware using:
qmk compile -kb ploopyco/trackball_mini/ -km custom

To flash the firmware to the ploopy mini I used QMK toolbox

To put the ploopy mini in bootloader mode, hold down button 4, that's the button immediately to the right of the ball, plug in and flash the firmware you just compiled.