Ploopy Classic Trackball

ploopy trackball firmware mod with qmk

Ploopy Classic Trackball
Ploopy Classic with Custom Paint Job, BTU Mod and Custom firmware

In this post I'm going to walk through what I did to customize the firmware running on the ploopy classic trackball. I've been using a trackball for at least decade starting with the Logitech series of thumb operated trackballs moving through to finger operated trackballs. The Ploopy classic trackball is an open-source design and fairly easy to modify the firmware if you're familiar with QMK.

I like the standard buttons, left, right and middle click with a scroll wheel as is with buttons 4 and 5 allow for clicking back and forward. The scroll wheel on the ploopy is average and not notched and requires thumb operation. It's not my favorite part of the device. To counter this you might be familiar with drag scroll, which allows you to hold a button and use the ball to scroll vertically and horizontally rather than the scroll wheel. Once you get the hang of it becomes second nature and for me becomes a must have.

On Linux you can setup drag scroll via software, but ideally it would be baked into the device so it works on windows and mac too. In addition to that, I wanted to toggle through the DPI settings (make the pointer more or less sensitive) using firmware, which avoids having to configure something in software which is specific to each computer you connect to. Furthermore, it's useful to have a precision mode when gaming or doing something which requires a high degree of accuracy in CAD.

With QMK you can use "tap dance", which allows you to customize the behavior when a button is held or double tapped which works around using a single button for each use. I wanted the firmware to do the following with just five buttons:

  • Use the buttons like a normal mouse or Microsoft Trackball Explorer.
  • Hold button 5 and tap right click, to cycle through DPI.
  • Hold button 4 for momentary drag scoll to quickly scroll through something.
  • Hold button 5 and tap left click, to toggle drag scroll for long viewing sessions where you want to scroll through a long thread or wide spreadsheet without having to hold down a button.
  • Double click button 4 to enter precision mode, double clicking again to exit.
  • Hold button 4 and double click button 5 to swap to swap right click and back buttons, this is sometimes useful for gaming, I find right click challenging using my thumb for gaming.

First, we need to enable tap dance in QMK for the ploopy, add the following to your rules.mk

TAP_DANCE_ENABLE = yes
MOUSEKEY_ENABLE = yes

Next we create directory under keymaps with a file named config.h

trackball/keymaps/custom/config.h

#pragma once

// Define DPIs 
#define PLOOPY_DPI_OPTIONS { 400, 600, 800, 1000, 1200, 1600, 2400 }
#define PLOOPY_DPI_DEFAULT 1
#define PLOOPY_DRAGSCROLL_FIXED 1
#define PLOOPY_DRAGSCROLL_INVERT 1
#define PLOOPY_DRAGSCROLL_DPI 100  // Fixed-DPI Drag Scroll

PLOOPY_DPI_OPTIONS defines the DPI modes we want to cycle through.

PLOOPY_DPI_DEFAULT Sets the default DPI mode.

PLOOPY_DRAGSCROLL_FIXED 1 Sets the dragscroll to fixed DPI for when we're using the trackball like a scroll wheel.

PLOOPY_DRAGSCROLL_INVERT  1 Inverts the drag scroll direction so it works like the scroll wheel on a mouse, rolling the finger towards the palm scrolls up and vice versa.

PLOOPY_DRAGSCROLL_DPI 100 Sets the DPI to 100, you can't go lower than this, if you do the DPI seems to default to a very high number making it difficult to control. I found 100 was still too high for me, more on that later.

Next we need to define keymap.c this goes into the directory you just created trackball/keymaps/custom/keymap.c

#include QMK_KEYBOARD_H

// 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_DRAG     // Hold mse btn4 toggles dragscroll
};

// Define a state for when we're holding down button 4
// this enters precison mode but also allows us to switch to another layer
bool btn4_held = false;
bool precision_mode = false;

// 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_state_t;

// Create a global instance of the tapdance state type
static td_state_t td_state; 

// 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);
const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
    [0] = LAYOUT( /* Base */
        KC_BTN1, KC_BTN3, KC_BTN2,
          TD(MSE_BTN4_DRAG), TD(MSE_BTN5_LAYR_1) 
    ),
    [1] = LAYOUT(
        DRAG_SCROLL, _______, DPI_CONFIG,
          _______, _______
    ),
    [2] = LAYOUT(
        KC_BTN1, KC_BTN3, TD(MSE_BTN4_DRAG),
          KC_BTN2, TD(MSE_BTN5_LAYR_1)
    )
};


// Determine the tapdance state to return
td_state_t cur_dance(qk_tap_dance_state_t *state) {
    if (state->count == 1) {
        if (state->interrupted || !state->pressed) return TD_SINGLE_TAP;
        else return TD_SINGLE_HOLD;
    }

    if (state->count == 2) return TD_DOUBLE_SINGLE_TAP;
    else 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_TAP:
            register_code16(KC_BTN4);
            break;
        case TD_SINGLE_HOLD:
            is_drag_scroll = true;
            btn4_held = true;
            break;
        case TD_NONE:
        case TD_UNKNOWN:
            break;
        case TD_DOUBLE_SINGLE_TAP:
            // If we've double tapped and not in precision mode then enter by dividing the dpi
            if (!precision_mode) {
                pmw33xx_set_cpi(0, (dpi_array[keyboard_config.dpi_config] / 2) );
                precision_mode = true;
            }
            else {
                pmw33xx_set_cpi(0, dpi_array[keyboard_config.dpi_config] );
                precision_mode = false;
            }
            break;
    }
}

void mseBtn4_reset(qk_tap_dance_state_t *state, void *user_data) {
    switch (td_state) {
        case TD_SINGLE_TAP:
            unregister_code16(KC_BTN4);
            break;
        case TD_SINGLE_HOLD:
            is_drag_scroll = false;
            btn4_held = false;
            break;
        case TD_NONE:
        case TD_UNKNOWN:
            break;
        case TD_DOUBLE_SINGLE_TAP:
            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_TAP:
            register_code16(KC_BTN5);
            break;
        case TD_SINGLE_HOLD:
            layer_on(1);
            break;
	case TD_NONE:
	case TD_UNKNOWN:
	    break;
	case TD_DOUBLE_SINGLE_TAP:
	    break;
    }
}

void mseBtn5_reset(qk_tap_dance_state_t *state, void *user_data) {
    switch (td_state) {
        case TD_SINGLE_TAP:
            unregister_code16(KC_BTN5);
            break;
        case TD_SINGLE_HOLD:
            layer_off(1);
            break;
	case TD_NONE:
	case TD_UNKNOWN:
	case TD_DOUBLE_SINGLE_TAP:
	    uprintf(" button 5 double tapped\n");
	    if (btn4_held) {
	        // If button 4 is held we're in drag scroll, so come out of that mode
            is_drag_scroll = false;
            uprintf(" button 4 is held\n");
            if (layer_state_is(0))  {
                uprintf(" layer state was 0 switching to 2\n");
                layer_on(2);
            }
            else {
                uprintf(" layer state was 2 switching to 0\n");
                layer_clear();
            }
	    }
	    // Nothing to do
	    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)
};

First, we define three layers, the default layer provides our standard mouse button support, buttons 4 and buttons 5 use TD macros to customize their behavior when they are held or double tapped, but when clicked work as back and forward buttons.

The second layer is enabled when button 5 is held, much like holding a shift key, when button 5 is held button 1 toggles drag scroll when clicked, and right click (button 2) cycles through the DPI settings.

The third layer flips button 4 with button 2 (right click).

The rest of file is mostly the logic to enable tap dance to understand our intentions as we hold or double click the buttons 4 and 5.

Taking a closer look at the following function:

void mseBtn4_finished(qk_tap_dance_state_t *state, void *user_data) {
    td_state = cur_dance(state);
    switch (td_state) {
        case TD_SINGLE_TAP:
            register_code16(KC_BTN4);
            break;
        case TD_SINGLE_HOLD:
            is_drag_scroll = true;
            btn4_held = true;
            break;
        case TD_NONE:
        case TD_UNKNOWN:
            break;
        case TD_DOUBLE_SINGLE_TAP:
            // If we've double tapped and not in precision mode then enter by dividing the dpi
            if (!precision_mode) {
                pmw33xx_set_cpi(0, (dpi_array[keyboard_config.dpi_config] / 2) );
                precision_mode = true;
            }
            else {
                pmw33xx_set_cpi(0, dpi_array[keyboard_config.dpi_config] );
                precision_mode = false;
            }
            break;
    }
}

A single tap of button 4 passes through button 4 (normally the back button).

When holding button 4, we've exposed is_drag_scroll from the trackball.c file to tap dance so we momentarily enable it here.

When double tapping button 4 we toggle on a precision_mode dividing the DPI setting by 2, if we're already in precision mode we exit it.

The following method handles exiting tap dance, here we just need to send the button 4 release or exit drag scroll if we were holding it.

void mseBtn4_reset(qk_tap_dance_state_t *state, void *user_data) {
    switch (td_state) {
        case TD_SINGLE_TAP:
            unregister_code16(KC_BTN4);
            break;
        case TD_SINGLE_HOLD:
            is_drag_scroll = false;
            btn4_held = false;
            break;
        case TD_NONE:
        case TD_UNKNOWN:
            break;
        case TD_DOUBLE_SINGLE_TAP:
            break;
    }
}

To expose is_drag_scroll we need to add a definition to trackball.h

extern bool              is_drag_scroll;

As previously mentioned, the fixed DPI setting of 100 was still too sensitive for me. Searching around this code was taken from another reddit user u/Yuler, which I adapted for the classic:

In trackball/trackball.c add the following lines just under the trackball state definitions to hold accumulator variables.

#define PLOOPY_DRAGSCROLL_DENOMINATOR 10;
static int _dragscroll_accumulator_x = 0;
static int _dragscroll_accumulator_y = 0;

Then replace the following method with what's below, which will make drag scroll less sensitive.

report_mouse_t pointing_device_task_kb(report_mouse_t mouse_report) {
    process_wheel();

    if (is_drag_scroll) {
#ifdef PLOOPY_DRAGSCROLL_H_INVERT
        // Invert horizontal scroll direction
        _dragscroll_accumulator_x += -mouse_report.x;
#else
        _dragscroll_accumulator_x += mouse_report.x;
#endif
#ifdef PLOOPY_DRAGSCROLL_INVERT
        // Invert vertical scroll direction
        _dragscroll_accumulator_y += -mouse_report.y;
#else
        _dragscroll_accumulator_y += mouse_report.y;
#endif
        int div_x = _dragscroll_accumulator_x / PLOOPY_DRAGSCROLL_DENOMINATOR;
        int div_y = _dragscroll_accumulator_y / PLOOPY_DRAGSCROLL_DENOMINATOR;

        if (div_x != 0) {
            mouse_report.h += div_x;
            _dragscroll_accumulator_x -= div_x * PLOOPY_DRAGSCROLL_DENOMINATOR;
        }

        if (div_y != 0) {
            mouse_report.v += div_y;
            _dragscroll_accumulator_y -= div_y * PLOOPY_DRAGSCROLL_DENOMINATOR;
        }
        mouse_report.x = 0;
        mouse_report.y = 0;
    }

    return pointing_device_task_user(mouse_report);
}

After making the changes it's a case of compiling the source code, I do this with the following command:

qmk compile -kb ploopco/trackball -km custom

Once compiled you can flash the hex file to your ploopy, to do that unplug the ploopy and with the QMK toolbox installed and running, hold down button 4 (that's the one just right of ball) and plug it in. Then flash the new firmware.

In case this is all too much, I've included the firmware hex, use at your own risk. Ensure that you ploopy classic is compatible before flashing, I have rev 1.005