~ruther/qmk_firmware

7f7364c55912879baaff8fafca550d02f17b4d44 — Dasky 3 years ago 76a6732
[Core] Split support for pointing devices. (#15304)

* Draft implementation

* formatting

* fix combined buttons

* remove pimoroni throttle

* sync pointing on a throttle loop with checksum

* no longer used

* doh

Co-authored-by: Drashna Jaelre <drashna@live.com>

* switch pimoroni to a cpi equivalent

* add cpi support

* allow user modification of seperate mouse reports

* a little tidy up

* add *_RIGHT defines.

* docs

* doxygen comments

* basic changelog

* clean up pimoroni

* small doc fixes

* Update docs/feature_pointing_device.md

Co-authored-by: Drashna Jaelre <drashna@live.com>

* performance tweak if side has usb

* Don't run init funtions on wrong side

* renamed some variables for consistency

* fix pimoroni typos

* Clamp instead of OR

* Promote combined values to uint16_t

* Update pointing_device.c

Co-authored-by: Drashna Jaelre <drashna@live.com>
Co-authored-by: Nick Brassel <nick@tzarc.org>
A docs/ChangeLog/20220226/PR15304.md => docs/ChangeLog/20220226/PR15304.md +13 -0
@@ 0,0 1,13 @@
### Split Common core now supports Pointing Devices ([#15304](https://github.com/qmk/qmk_firmware/pull/15304))

Pointing devices can now be shared across a split keyboard with support for a single pointing device or a pointing device on each side.

This feature can be enabled with `#define SPLIT_POINTING_ENABLE` and one of the following options:

| Setting                   | Description                        |
|---------------------------|------------------------------------|
|`POINTING_DEVICE_LEFT`     | Pointing device on the left side   |
|`POINTING_DEVICE_RIGHT`    | Pointing device on the right side  |
|`POINTING_DEVICE_COMBINED` | Pointing device on both sides      |

See the [Pointing Device](../feature_pointing_device.md) documentation for further configuration options.

M docs/feature_pointing_device.md => docs/feature_pointing_device.md +105 -11
@@ 127,11 127,10 @@ The Pimoroni Trackball module is a I2C based breakout board with an RGB enable t
| Setting                             | Description                                                                        | Default |
|-------------------------------------|------------------------------------------------------------------------------------|---------|
|`PIMORONI_TRACKBALL_ADDRESS`         | (Required) Sets the I2C Address for the Pimoroni Trackball.                        | `0x0A`  |
|`PIMORONI_TRACKBALL_TIMEOUT`         | (Optional) The timeout for i2c communication with the trackpad in milliseconds.    | `100`   |
|`PIMORONI_TRACKBALL_INTERVAL_MS`     | (Optional) The update/read interval for the sensor in milliseconds.                | `8`     |
|`PIMORONI_TRACKBALL_TIMEOUT`         | (Optional) The timeout for i2c communication with the trackball in milliseconds.   | `100`   |
|`PIMORONI_TRACKBALL_SCALE`           | (Optional) The multiplier used to generate reports from the sensor.                | `5`     |
|`PIMORONI_TRACKBALL_DEBOUNCE_CYCLES` | (Optional) The number of scan cycles used for debouncing on the ball press.        | `20`    |
|`PIMORONI_TRACKBALL_ERROR_COUNT`     | (Optional) Specifies the number of read/write errors until the sensor is disabled. | `10`  |
|`PIMORONI_TRACKBALL_ERROR_COUNT`     | (Optional) Specifies the number of read/write errors until the sensor is disabled. | `10`    |

### PMW 3360 Sensor



@@ 171,14 170,35 @@ void           pointing_device_driver_set_cpi(uint16_t cpi) {}

## Common Configuration

| Setting                       | Description                                                           | Default       |
|-------------------------------|-----------------------------------------------------------------------|---------------|
|`POINTING_DEVICE_ROTATION_90`  | (Optional) Rotates the X and Y data by  90 degrees.                   | _not defined_ |
|`POINTING_DEVICE_ROTATION_180` | (Optional) Rotates the X and Y data by 180 degrees.                   | _not defined_ |
|`POINTING_DEVICE_ROTATION_270` | (Optional) Rotates the X and Y data by 270 degrees.                   | _not defined_ |
|`POINTING_DEVICE_INVERT_X`     | (Optional) Inverts the X axis report.                                 | _not defined_ |
|`POINTING_DEVICE_INVERT_Y`     | (Optional) Inverts the Y axis report.                                 | _not defined_ |
|`POINTING_DEVICE_MOTION_PIN`   | (Optional) If supported, will only read from sensor if pin is active. | _not defined_ |
| Setting                          | Description                                                           | Default           |
|----------------------------------|-----------------------------------------------------------------------|-------------------|
|`POINTING_DEVICE_ROTATION_90`     | (Optional) Rotates the X and Y data by  90 degrees.                   | _not defined_     |
|`POINTING_DEVICE_ROTATION_180`    | (Optional) Rotates the X and Y data by 180 degrees.                   | _not defined_     |
|`POINTING_DEVICE_ROTATION_270`    | (Optional) Rotates the X and Y data by 270 degrees.                   | _not defined_     |
|`POINTING_DEVICE_INVERT_X`        | (Optional) Inverts the X axis report.                                 | _not defined_     |
|`POINTING_DEVICE_INVERT_Y`        | (Optional) Inverts the Y axis report.                                 | _not defined_     |
|`POINTING_DEVICE_MOTION_PIN`      | (Optional) If supported, will only read from sensor if pin is active. | _not defined_     |
|`POINTING_DEVICE_TASK_THROTTLE_MS`      | (Optional) Limits the frequency that the sensor is polled for motion. | _not defined_     |

!> When using `SPLIT_POINTING_ENABLE` the `POINTING_DEVICE_MOTION_PIN` functionality is not supported and would recommend `POINTING_DEVICE_TASK_THROTTLE_MS` be set to `1`. Increasing this value will increase transport performance at the cost of possible mouse responsiveness.


## Split Keyboard Configuration

The following configuration options are only available when using `SPLIT_POINTING_ENABLE` see [data sync options](feature_split_keyboard.md?id=data-sync-options). The rotation and invert `*_RIGHT` options are only used with `POINTING_DEVICE_COMBINED`. If using `POINTING_DEVICE_LEFT` or `POINTING_DEVICE_RIGHT` use the common configuration above to configure your pointing device.

| Setting                                | Description                                                           | Default       |
|----------------------------------------|-----------------------------------------------------------------------|---------------|
|`POINTING_DEVICE_LEFT`                  | Pointing device on the left side (Required - pick one only)           | _not defined_ |
|`POINTING_DEVICE_RIGHT`                 | Pointing device on the right side (Required - pick one only)          | _not defined_ |
|`POINTING_DEVICE_COMBINED`              | Pointing device on both sides (Required - pick one only)              | _not defined_ |
|`POINTING_DEVICE_ROTATION_90_RIGHT`     | (Optional) Rotates the X and Y data by  90 degrees.                   | _not defined_ |
|`POINTING_DEVICE_ROTATION_180_RIGHT`    | (Optional) Rotates the X and Y data by 180 degrees.                   | _not defined_ |
|`POINTING_DEVICE_ROTATION_270_RIGHT`    | (Optional) Rotates the X and Y data by 270 degrees.                   | _not defined_ |
|`POINTING_DEVICE_INVERT_X_RIGHT`        | (Optional) Inverts the X axis report.                                 | _not defined_ |
|`POINTING_DEVICE_INVERT_Y_RIGHT`        | (Optional) Inverts the Y axis report.                                 | _not defined_ |

!> If there is a `_RIGHT` configuration option or callback, the [common configuration](feature_pointing_device.md?id=common-configuration) option will work for the left. For correct left/right detection you should setup a [handedness option](feature_split_keyboard?id=setting-handedness), `EE_HANDS` is usually a good option for an existing board that doesn't do handedness by hardware.


## Callbacks and Functions 


@@ 196,6 216,21 @@ void           pointing_device_driver_set_cpi(uint16_t cpi) {}
| `pointing_device_set_report(mouse_report)`                 | Sets the mouse report to the assigned `mouse_report_t` data structured passed to the function.                | 
| `pointing_device_send(void)`                               | Sends the current mouse report to the host system.  Function can be replaced.                                 | 
| `has_mouse_report_changed(old, new)`                       | Compares the old and new `mouse_report_t` data and returns true only if it has changed.                       |
| `pointing_device_adjust_by_defines(mouse_report)`          | Applies rotations and invert configurations to a raw mouse report.                                             |


## Split Keyboard Callbacks and Functions

The combined functions below are only available when using `SPLIT_POINTING_ENABLE` and `POINTING_DEVICE_COMBINED`. The 2 callbacks `pointing_device_task_combined_*` replace the single sided equivalents above. See the [combined pointing devices example](feature_pointing_device.md?id=combined-pointing-devices)

| Function                                                        | Description                                                                                                              |
|-----------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------|
| `pointing_device_set_shared_report(mouse_report)`               | Sets the shared mouse report to the assigned `mouse_report_t` data structured passed to the function.                    |
| `pointing_device_set_cpi_on_side(bool, uint16_t)`               | Sets the CPI/DPI of one side, if supported. Passing `true` will set the left and `false` the right`                      |
| `pointing_device_combine_reports(left_report, right_report)`    | Returns a combined mouse_report of left_report and right_report (as a `mouse_report_t` data structure)                   |
| `pointing_device_task_combined_kb(left_report, right_report)`   | Callback, so keyboard code can intercept and modify the data. Returns a combined mouse report.                           |
| `pointing_device_task_combined_user(left_report, right_report)` | Callback, so user code can intercept and modify. Returns a combined mouse report using `pointing_device_combine_reports` |
| `pointing_device_adjust_by_defines_right(mouse_report)`         | Applies right side rotations and invert configurations to a raw mouse report.                                            |


# Manipulating Mouse Reports


@@ 242,3 277,62 @@ case MS_SPECIAL:
```

Recall that the mouse report is set to zero (except the buttons) whenever it is sent, so the scrolling would only occur once in each case.

## Split Examples

The following examples make use the `SPLIT_POINTING_ENABLE` functionality and show how to manipulate the mouse report for a scrolling mode.

### Single Pointing Device

The following example will work with either `POINTING_DEVICE_LEFT` or `POINTING_DEVICE_RIGHT` and enables scrolling mode while on a particular layer.

```c

static bool scrolling_mode = false;

layer_state_t layer_state_set_user(layer_state_t state) {
    switch (get_highest_layer(state)) {
        case _RAISE:  // If we're on the _RAISE layer enable scrolling mode
            scrolling_mode = true;
            pointing_device_set_cpi(2000);
            break;
        default:
            if (scrolling_mode) {  // check if we were scrolling before and set disable if so
                scrolling_mode = false;
                pointing_device_set_cpi(8000);
            }
            break;
    }
    return state;
}

report_mouse_t pointing_device_task_user(report_mouse_t mouse_report) {
    if (scrolling_mode) {
        mouse_report.h = mouse_report.x;
        mouse_report.v = mouse_report.y;
        mouse_report.x = 0;
        mouse_report.y = 0;
    }
    return mouse_report;
}

```

### Combined Pointing Devices

The following example requires `POINTING_DEVICE_COMBINED` and sets the left side pointing device to scroll only.

```c
void keyboard_post_init_user(void) {
    pointing_device_set_cpi_on_side(true, 1000); //Set cpi on left side to a low value for slower scrolling.
    pointing_device_set_cpi_on_side(false, 8000); //Set cpi on right side to a reasonable value for mousing.
}

report_mouse_t pointing_device_task_combined_user(report_mouse_t left_report, report_mouse_t right_report) {
    left_report.h = left_report.x;
    left_report.v = left_report.y;
    left_report.x = 0;
    left_report.y = 0;
    return pointing_device_combine_reports(left_report, right_report);
}
```

M docs/feature_split_keyboard.md => docs/feature_split_keyboard.md +8 -0
@@ 266,6 266,14 @@ This enables transmitting the current OLED on/off status to the slave side of th

This enables transmitting the current ST7565 on/off status to the slave side of the split keyboard. The purpose of this feature is to support state (on/off state only) syncing.

```c
#define SPLIT_POINTING_ENABLE
```

This enables transmitting the pointing device status to the master side of the split keyboard. The purpose of this feature is to enable use pointing devices on the slave side. 

!> There is additional required configuration for `SPLIT_POINTING_ENABLE` outlined in the [pointing device documentation](feature_pointing_device.md?id=split-keyboard-configuration).

### Custom data sync between sides :id=custom-data-sync

QMK's split transport allows for arbitrary data transactions at both the keyboard and user levels. This is modelled on a remote procedure call, with the master invoking a function on the slave side, with the ability to send data from master to slave, process it slave side, and send data back from slave to master.

M drivers/sensors/pimoroni_trackball.c => drivers/sensors/pimoroni_trackball.c +19 -3
@@ 33,8 33,24 @@

static uint16_t precision = 128;

float pimoroni_trackball_get_precision(void) { return ((float)precision / 128); }
void  pimoroni_trackball_set_precision(float floatprecision) { precision = (floatprecision * 128); }
uint16_t pimoroni_trackball_get_cpi(void) { return (precision * 125); }
/**
 * @brief Sets the scaling value for pimoroni trackball
 *
 * Sets a scaling value for pimoroni trackball to allow runtime adjustment. This isn't used by the sensor and is an
 * approximation so the functions are consistent across drivers.
 *
 * NOTE: This rounds down to the nearest number divisable by 125 that's a positive integer, values below 125 are clamped to 125.
 *
 * @param cpi uint16_t
 */
void pimoroni_trackball_set_cpi(uint16_t cpi) {
    if (cpi < 249) {
        precision = 1;
    } else {
        precision = (cpi - (cpi % 125)) / 125;
    }
}

void pimoroni_trackball_set_rgbw(uint8_t r, uint8_t g, uint8_t b, uint8_t w) {
    uint8_t                              data[4] = {r, g, b, w};


@@ 60,7 76,7 @@ i2c_status_t read_pimoroni_trackball(pimoroni_data_t* data) {
    return status;
}

__attribute__((weak)) void pimironi_trackball_device_init(void) {
__attribute__((weak)) void pimoroni_trackball_device_init(void) {
    i2c_init();
    pimoroni_trackball_set_rgbw(0x00, 0x00, 0x00, 0x00);
}

M drivers/sensors/pimoroni_trackball.h => drivers/sensors/pimoroni_trackball.h +3 -6
@@ 23,9 23,6 @@
#ifndef PIMORONI_TRACKBALL_ADDRESS
#    define PIMORONI_TRACKBALL_ADDRESS 0x0A
#endif
#ifndef PIMORONI_TRACKBALL_INTERVAL_MS
#    define PIMORONI_TRACKBALL_INTERVAL_MS 8
#endif
#ifndef PIMORONI_TRACKBALL_SCALE
#    define PIMORONI_TRACKBALL_SCALE 5
#endif


@@ 52,10 49,10 @@ typedef struct {
    uint8_t click;
} pimoroni_data_t;

void         pimironi_trackball_device_init(void);
void         pimoroni_trackball_device_init(void);
void         pimoroni_trackball_set_rgbw(uint8_t red, uint8_t green, uint8_t blue, uint8_t white);
int16_t      pimoroni_trackball_get_offsets(uint8_t negative_dir, uint8_t positive_dir, uint8_t scale);
void         pimoroni_trackball_adapt_values(int8_t* mouse, int16_t* offset);
float        pimoroni_trackball_get_precision(void);
void         pimoroni_trackball_set_precision(float precision);
uint16_t     pimoroni_trackball_get_cpi(void);
void         pimoroni_trackball_set_cpi(uint16_t cpi);
i2c_status_t read_pimoroni_trackball(pimoroni_data_t* data);

M quantum/pointing_device.c => quantum/pointing_device.c +356 -33
@@ 18,24 18,105 @@

#include "pointing_device.h"
#include <string.h>
#include "timer.h"
#ifdef MOUSEKEY_ENABLE
#    include "mousekey.h"
#endif
#if (defined(POINTING_DEVICE_ROTATION_90) + defined(POINTING_DEVICE_ROTATION_180) + defined(POINTING_DEVICE_ROTATION_270)) > 1
#    error More than one rotation selected.  This is not supported.
#endif
#if defined(SPLIT_POINTING_ENABLE)
#    include "transactions.h"
#    include "keyboard.h"

static report_mouse_t mouseReport = {};
report_mouse_t shared_mouse_report = {};
uint16_t       shared_cpi          = 0;

/**
 * @brief Sets the shared mouse report used be pointing device task
 *
 * NOTE : Only available when using SPLIT_POINTING_ENABLE
 *
 * @param[in] new_mouse_report report_mouse_t
 */
void pointing_device_set_shared_report(report_mouse_t new_mouse_report) { shared_mouse_report = new_mouse_report; }

/**
 * @brief Gets current pointing device CPI if supported
 *
 * Gets current cpi of the shared report and returns it as uint16_t
 *
 * NOTE : Only available when using SPLIT_POINTING_ENABLE
 *
 * @return cpi value as uint16_t
 */
uint16_t pointing_device_get_shared_cpi(void) { return shared_cpi; }

#    if defined(POINTING_DEVICE_LEFT)
#        define POINTING_DEVICE_THIS_SIDE is_keyboard_left()
#    elif defined(POINTING_DEVICE_RIGHT)
#        define POINTING_DEVICE_THIS_SIDE !is_keyboard_left()
#    elif defined(POINTING_DEVICE_COMBINED)
#        define POINTING_DEVICE_THIS_SIDE true
#    endif

#endif  // defined(SPLIT_POINTING_ENABLE)

static report_mouse_t local_mouse_report = {};

extern const pointing_device_driver_t pointing_device_driver;

/**
 * @brief Compares 2 mouse reports for difference and returns result
 *
 * @param[in] new report_mouse_t
 * @param[in] old report_mouse_t
 * @return bool result
 */
__attribute__((weak)) bool has_mouse_report_changed(report_mouse_t new, report_mouse_t old) { return memcmp(&new, &old, sizeof(new)); }

__attribute__((weak)) void           pointing_device_init_kb(void) {}
__attribute__((weak)) void           pointing_device_init_user(void) {}
/**
 * @brief Keyboard level code pointing device initialisation
 *
 */
__attribute__((weak)) void pointing_device_init_kb(void) {}

/**
 * @brief User level code pointing device initialisation
 *
 */
__attribute__((weak)) void pointing_device_init_user(void) {}

/**
 * @brief Weak function allowing for keyboard level mouse report modification
 *
 * Takes report_mouse_t struct allowing modification at keyboard level then returns report_mouse_t.
 *
 * @param[in] mouse_report report_mouse_t
 * @return report_mouse_t
 */
__attribute__((weak)) report_mouse_t pointing_device_task_kb(report_mouse_t mouse_report) { return pointing_device_task_user(mouse_report); }

/**
 * @brief Weak function allowing for user level mouse report modification
 *
 * Takes report_mouse_t struct allowing modification at user level then returns report_mouse_t.
 *
 * @param[in] mouse_report report_mouse_t
 * @return report_mouse_t
 */
__attribute__((weak)) report_mouse_t pointing_device_task_user(report_mouse_t mouse_report) { return mouse_report; }

/**
 * @brief Handles pointing device buttons
 *
 * Returns modified button bitmask using bool pressed and selected pointing_device_buttons_t button in uint8_t buttons bitmask.
 *
 * @param buttons[in] uint8_t bitmask
 * @param pressed[in] bool
 * @param button[in] pointing_device_buttons_t value
 * @return Modified uint8_t bitmask buttons
 */
__attribute__((weak)) uint8_t pointing_device_handle_buttons(uint8_t buttons, bool pressed, pointing_device_buttons_t button) {
    if (pressed) {
        buttons |= 1 << (button);


@@ 45,7 126,17 @@ __attribute__((weak)) uint8_t pointing_device_handle_buttons(uint8_t buttons, bo
    return buttons;
}

/**
 * @brief Initialises pointing device
 *
 * Initialises pointing device, perform driver init and optional keyboard/user level code.
 */
__attribute__((weak)) void pointing_device_init(void) {
#if defined(SPLIT_POINTING_ENABLE)
    if (!(POINTING_DEVICE_THIS_SIDE)) {
        return;
    }
#endif
    pointing_device_driver.init();
#ifdef POINTING_DEVICE_MOTION_PIN
    setPinInputHigh(POINTING_DEVICE_MOTION_PIN);


@@ 54,67 145,299 @@ __attribute__((weak)) void pointing_device_init(void) {
    pointing_device_init_user();
}

/**
 * @brief Sends processed mouse report to host
 *
 * This sends the mouse report generated by pointing_device_task if changed since the last report. Once send zeros mouse report except buttons.
 *
 */
__attribute__((weak)) void pointing_device_send(void) {
    static report_mouse_t old_report = {};

    // If you need to do other things, like debugging, this is the place to do it.
    if (has_mouse_report_changed(mouseReport, old_report)) {
        host_mouse_send(&mouseReport);
    if (has_mouse_report_changed(local_mouse_report, old_report)) {
        host_mouse_send(&local_mouse_report);
    }
    // send it and 0 it out except for buttons, so those stay until they are explicity over-ridden using update_pointing_device
    mouseReport.x = 0;
    mouseReport.y = 0;
    mouseReport.v = 0;
    mouseReport.h = 0;
    local_mouse_report.x = 0;
    local_mouse_report.y = 0;
    local_mouse_report.v = 0;
    local_mouse_report.h = 0;

    memcpy(&old_report, &mouseReport, sizeof(mouseReport));
    memcpy(&old_report, &local_mouse_report, sizeof(local_mouse_report));
}

__attribute__((weak)) void pointing_device_task(void) {
    // Gather report info
#ifdef POINTING_DEVICE_MOTION_PIN
    if (!readPin(POINTING_DEVICE_MOTION_PIN))
#endif
        mouseReport = pointing_device_driver.get_report(mouseReport);

        // Support rotation of the sensor data
/**
 * @brief Adjust mouse report by any optional common pointing configuration defines
 *
 * This applies rotation or inversion to the mouse report as selected by the pointing device common configuration defines.
 *
 * @param mouse_report[in] takes a report_mouse_t to be adjusted
 * @return report_mouse_t with adjusted values
 */
report_mouse_t pointing_device_adjust_by_defines(report_mouse_t mouse_report) {
    // Support rotation of the sensor data
#if defined(POINTING_DEVICE_ROTATION_90) || defined(POINTING_DEVICE_ROTATION_180) || defined(POINTING_DEVICE_ROTATION_270)
    int8_t x = mouseReport.x, y = mouseReport.y;
    int8_t x = mouse_report.x, y = mouse_report.y;
#    if defined(POINTING_DEVICE_ROTATION_90)
    mouseReport.x = y;
    mouseReport.y = -x;
    mouse_report.x = y;
    mouse_report.y = -x;
#    elif defined(POINTING_DEVICE_ROTATION_180)
    mouseReport.x = -x;
    mouseReport.y = -y;
    mouse_report.x = -x;
    mouse_report.y = -y;
#    elif defined(POINTING_DEVICE_ROTATION_270)
    mouseReport.x = -y;
    mouseReport.y = x;
    mouse_report.x = -y;
    mouse_report.y = x;
#    else
#        error "How the heck did you get here?!"
#    endif
#endif
    // Support Inverting the X and Y Axises
#if defined(POINTING_DEVICE_INVERT_X)
    mouseReport.x = -mouseReport.x;
    mouse_report.x = -mouse_report.x;
#endif
#if defined(POINTING_DEVICE_INVERT_Y)
    mouseReport.y = -mouseReport.y;
    mouse_report.y = -mouse_report.y;
#endif
    return mouse_report;
}

/**
 * @brief Retrieves and processes pointing device data.
 *
 * This function is part of the keyboard loop and retrieves the mouse report from the pointing device driver.
 * It applies any optional configuration e.g. rotation or axis inversion and then initiates a send.
 *
 */
__attribute__((weak)) void pointing_device_task(void) {
#if defined(SPLIT_POINTING_ENABLE)
    // Don't poll the target side pointing device.
    if (!is_keyboard_master()) {
        return;
    };
#endif

#if defined(POINTING_DEVICE_TASK_THROTTLE_MS)
    static uint32_t last_exec = 0;
    if (timer_elapsed32(last_exec) < POINTING_DEVICE_TASK_THROTTLE_MS) {
        return;
    }
    last_exec = timer_read32();
#else
#    if defined(SPLIT_POINTING_ENABLE)
#        pragma message("It's recommended you enable a throttle when sharing pointing devices.")
#    endif
#endif

    // Gather report info
#ifdef POINTING_DEVICE_MOTION_PIN
#    if defined(SPLIT_POINTING_ENABLE)
#        error POINTING_DEVICE_MOTION_PIN not supported when sharing the pointing device report between sides.
#    endif
    if (!readPin(POINTING_DEVICE_MOTION_PIN))
#endif

#if defined(SPLIT_POINTING_ENABLE)
#    if defined(POINTING_DEVICE_COMBINED)
        static uint8_t old_buttons = 0;
    local_mouse_report.buttons = old_buttons;
    local_mouse_report         = pointing_device_driver.get_report(local_mouse_report);
    old_buttons                = local_mouse_report.buttons;
#    elif defined(POINTING_DEVICE_LEFT) || defined(POINTING_DEVICE_RIGHT)
        local_mouse_report = POINTING_DEVICE_THIS_SIDE ? pointing_device_driver.get_report(local_mouse_report) : shared_mouse_report;
#    else
#        error "You need to define the side(s) the pointing device is on. POINTING_DEVICE_COMBINED / POINTING_DEVICE_LEFT / POINTING_DEVICE_RIGHT"
#    endif
#else
    local_mouse_report = pointing_device_driver.get_report(local_mouse_report);
#endif  // defined(SPLIT_POINTING_ENABLE)

    // allow kb to intercept and modify report
    mouseReport = pointing_device_task_kb(mouseReport);
#if defined(SPLIT_POINTING_ENABLE) && defined(POINTING_DEVICE_COMBINED)
    if (is_keyboard_left()) {
        local_mouse_report  = pointing_device_adjust_by_defines(local_mouse_report);
        shared_mouse_report = pointing_device_adjust_by_defines_right(shared_mouse_report);
    } else {
        local_mouse_report  = pointing_device_adjust_by_defines_right(local_mouse_report);
        shared_mouse_report = pointing_device_adjust_by_defines(shared_mouse_report);
    }
    local_mouse_report = is_keyboard_left() ? pointing_device_task_combined_kb(local_mouse_report, shared_mouse_report) : pointing_device_task_combined_kb(shared_mouse_report, local_mouse_report);
#else
    local_mouse_report = pointing_device_adjust_by_defines(local_mouse_report);
    local_mouse_report = pointing_device_task_kb(local_mouse_report);
#endif
    // combine with mouse report to ensure that the combined is sent correctly
#ifdef MOUSEKEY_ENABLE
    report_mouse_t mousekey_report = mousekey_get_report();
    mouseReport.buttons            = mouseReport.buttons | mousekey_report.buttons;
    local_mouse_report.buttons     = local_mouse_report.buttons | mousekey_report.buttons;
#endif
    pointing_device_send();
}

report_mouse_t pointing_device_get_report(void) { return mouseReport; }
/**
 * @brief Gets current mouse report used by pointing device task
 *
 * @return report_mouse_t
 */
report_mouse_t pointing_device_get_report(void) { return local_mouse_report; }

/**
 * @brief Sets mouse report used be pointing device task
 *
 * @param[in] new_mouse_report
 */
void pointing_device_set_report(report_mouse_t new_mouse_report) { local_mouse_report = new_mouse_report; }

/**
 * @brief Gets current pointing device CPI if supported
 *
 * Gets current cpi from pointing device driver if supported and returns it as uint16_t
 *
 * @return cpi value as uint16_t
 */
uint16_t pointing_device_get_cpi(void) {
#if defined(SPLIT_POINTING_ENABLE)
    return POINTING_DEVICE_THIS_SIDE ? pointing_device_driver.get_cpi() : shared_cpi;
#else
    return pointing_device_driver.get_cpi();
#endif
}

void pointing_device_set_report(report_mouse_t newMouseReport) { mouseReport = newMouseReport; }
/**
 * @brief Set pointing device CPI if supported
 *
 * Takes a uint16_t value to set pointing device cpi if supported by driver.
 *
 * @param[in] cpi uint16_t value.
 */
void pointing_device_set_cpi(uint16_t cpi) {
#if defined(SPLIT_POINTING_ENABLE)
    if (POINTING_DEVICE_THIS_SIDE) {
        pointing_device_driver.set_cpi(cpi);
    } else {
        shared_cpi = cpi;
    }
#else
    pointing_device_driver.set_cpi(cpi);
#endif
}

uint16_t pointing_device_get_cpi(void) { return pointing_device_driver.get_cpi(); }
#if defined(SPLIT_POINTING_ENABLE) && defined(POINTING_DEVICE_COMBINED)
/**
 * @brief Set pointing device CPI if supported
 *
 * Takes a bool and uint16_t and allows setting cpi for a single side when using 2 pointing devices with a split keyboard.
 *
 * NOTE: Only available when using SPLIT_POINTING_ENABLE and POINTING_DEVICE_COMBINED
 *
 * @param[in] left true = left, false = right.
 * @param[in] cpi uint16_t value.
 */
void pointing_device_set_cpi_on_side(bool left, uint16_t cpi) {
    bool local = (is_keyboard_left() & left) ? true : false;
    if (local) {
        pointing_device_driver.set_cpi(cpi);
    } else {
        shared_cpi = cpi;
    }
}

void pointing_device_set_cpi(uint16_t cpi) { pointing_device_driver.set_cpi(cpi); }
/**
 * @brief clamps int16_t to int8_t
 *
 * @param[in] int16_t value
 * @return int8_t clamped value
 */
static inline int8_t pointing_device_movement_clamp(int16_t value) {
    if (value < INT8_MIN) {
        return INT8_MIN;
    } else if (value > INT8_MAX) {
        return INT8_MAX;
    } else {
        return value;
    }
}

/**
 * @brief combines 2 mouse reports and returns 2
 *
 * Combines 2 report_mouse_t structs, clamping movement values to int8_t and ignores report_id then returns the resulting report_mouse_t struct.
 *
 * NOTE: Only available when using SPLIT_POINTING_ENABLE and POINTING_DEVICE_COMBINED
 *
 * @param[in] left_report left report_mouse_t
 * @param[in] right_report right report_mouse_t
 * @return combined report_mouse_t of left_report and right_report
 */
report_mouse_t pointing_device_combine_reports(report_mouse_t left_report, report_mouse_t right_report) {
    left_report.x = pointing_device_movement_clamp((int16_t)left_report.x + right_report.x);
    left_report.y = pointing_device_movement_clamp((int16_t)left_report.y + right_report.y);
    left_report.h = pointing_device_movement_clamp((int16_t)left_report.h + right_report.h);
    left_report.v = pointing_device_movement_clamp((int16_t)left_report.v + right_report.v);
    left_report.buttons |= right_report.buttons;
    return left_report;
}

/**
 * @brief Adjust mouse report by any optional right pointing configuration defines
 *
 * This applies rotation or inversion to the mouse report as selected by the pointing device common configuration defines.
 *
 * NOTE: Only available when using SPLIT_POINTING_ENABLE and POINTING_DEVICE_COMBINED
 *
 * @param[in] mouse_report report_mouse_t to be adjusted
 * @return report_mouse_t with adjusted values
 */
report_mouse_t pointing_device_adjust_by_defines_right(report_mouse_t mouse_report) {
    // Support rotation of the sensor data
#    if defined(POINTING_DEVICE_ROTATION_90_RIGHT) || defined(POINTING_DEVICE_ROTATION_RIGHT) || defined(POINTING_DEVICE_ROTATION_RIGHT)
    int8_t x = mouse_report.x, y = mouse_report.y;
#        if defined(POINTING_DEVICE_ROTATION_90_RIGHT)
    mouse_report.x = y;
    mouse_report.y = -x;
#        elif defined(POINTING_DEVICE_ROTATION_180_RIGHT)
    mouse_report.x = -x;
    mouse_report.y = -y;
#        elif defined(POINTING_DEVICE_ROTATION_270_RIGHT)
    mouse_report.x = -y;
    mouse_report.y = x;
#        else
#            error "How the heck did you get here?!"
#        endif
#    endif
    // Support Inverting the X and Y Axises
#    if defined(POINTING_DEVICE_INVERT_X_RIGHT)
    mouse_report.x = -mouse_report.x;
#    endif
#    if defined(POINTING_DEVICE_INVERT_Y_RIGHT)
    mouse_report.y = -mouse_report.y;
#    endif
    return mouse_report;
}

/**
 * @brief Weak function allowing for keyboard level mouse report modification
 *
 * Takes 2 report_mouse_t structs allowing individual modification of sides at keyboard level then returns pointing_device_task_combined_user.
 *
 * NOTE: Only available when using SPLIT_POINTING_ENABLE and POINTING_DEVICE_COMBINED
 *
 * @param[in] left_report report_mouse_t
 * @param[in] right_report report_mouse_t
 * @return pointing_device_task_combined_user(left_report, right_report) by default
 */
__attribute__((weak)) report_mouse_t pointing_device_task_combined_kb(report_mouse_t left_report, report_mouse_t right_report) { return pointing_device_task_combined_user(left_report, right_report); }

/**
 * @brief Weak function allowing for user level mouse report modification
 *
 * Takes 2 report_mouse_t structs allowing individual modification of sides at user level then returns pointing_device_combine_reports.
 *
 * NOTE: Only available when using SPLIT_POINTING_ENABLE and POINTING_DEVICE_COMBINED
 *
 * @param[in] left_report report_mouse_t
 * @param[in] right_report report_mouse_t
 * @return pointing_device_combine_reports(left_report, right_report) by default
 */
__attribute__((weak)) report_mouse_t pointing_device_task_combined_user(report_mouse_t left_report, report_mouse_t right_report) { return pointing_device_combine_reports(left_report, right_report); }
#endif

M quantum/pointing_device.h => quantum/pointing_device.h +13 -0
@@ 86,3 86,16 @@ void           pointing_device_init_user(void);
report_mouse_t pointing_device_task_kb(report_mouse_t mouse_report);
report_mouse_t pointing_device_task_user(report_mouse_t mouse_report);
uint8_t        pointing_device_handle_buttons(uint8_t buttons, bool pressed, pointing_device_buttons_t button);
report_mouse_t pointing_device_adjust_by_defines(report_mouse_t mouse_report);

#if defined(SPLIT_POINTING_ENABLE)
void     pointing_device_set_shared_report(report_mouse_t report);
uint16_t pointing_device_get_shared_cpi(void);
#    if defined(POINTING_DEVICE_COMBINED)
void           pointing_device_set_cpi_on_side(bool left, uint16_t cpi);
report_mouse_t pointing_device_combine_reports(report_mouse_t left_report, report_mouse_t right_report);
report_mouse_t pointing_device_task_combined_kb(report_mouse_t left_report, report_mouse_t right_report);
report_mouse_t pointing_device_task_combined_user(report_mouse_t left_report, report_mouse_t right_report);
report_mouse_t pointing_device_adjust_by_defines_right(report_mouse_t mouse_report);
#    endif //defined(POINTING_DEVICE_COMBINED)
#endif //defined(SPLIT_POINTING_ENABLE)

M quantum/pointing_device_drivers.c => quantum/pointing_device_drivers.c +11 -13
@@ 165,14 165,13 @@ const pointing_device_driver_t pointing_device_driver = {
// clang-format on

#elif defined(POINTING_DEVICE_DRIVER_pimoroni_trackball)
report_mouse_t pimorono_trackball_get_report(report_mouse_t mouse_report) {
    static fast_timer_t throttle      = 0;
    static uint16_t     debounce      = 0;
    static uint8_t      error_count   = 0;
    pimoroni_data_t     pimoroni_data = {0};
    static int16_t      x_offset = 0, y_offset = 0;

    if (error_count < PIMORONI_TRACKBALL_ERROR_COUNT && timer_elapsed_fast(throttle) >= PIMORONI_TRACKBALL_INTERVAL_MS) {
report_mouse_t pimoroni_trackball_get_report(report_mouse_t mouse_report) {
    static uint16_t debounce      = 0;
    static uint8_t  error_count   = 0;
    pimoroni_data_t pimoroni_data = {0};
    static int16_t  x_offset = 0, y_offset = 0;

    if (error_count < PIMORONI_TRACKBALL_ERROR_COUNT) {
        i2c_status_t status = read_pimoroni_trackball(&pimoroni_data);

        if (status == I2C_STATUS_SUCCESS) {


@@ 195,17 194,16 @@ report_mouse_t pimorono_trackball_get_report(report_mouse_t mouse_report) {
        } else {
            error_count++;
        }
        throttle = timer_read_fast();
    }
    return mouse_report;
}

// clang-format off
const pointing_device_driver_t pointing_device_driver = {
    .init       = pimironi_trackball_device_init,
    .get_report = pimorono_trackball_get_report,
    .set_cpi    = NULL,
    .get_cpi    = NULL
    .init       = pimoroni_trackball_device_init,
    .get_report = pimoroni_trackball_get_report,
    .set_cpi    = pimoroni_trackball_set_cpi,
    .get_cpi    = pimoroni_trackball_get_cpi
};
// clang-format on
#elif defined(POINTING_DEVICE_DRIVER_pmw3360)

M quantum/split_common/transaction_id_define.h => quantum/split_common/transaction_id_define.h +6 -0
@@ 78,6 78,12 @@ enum serial_transaction_id {
    PUT_ST7565,
#endif  // defined(ST7565_ENABLE) && defined(SPLIT_ST7565_ENABLE)

#if defined(POINTING_DEVICE_ENABLE) && defined(SPLIT_POINTING_ENABLE)
    GET_POINTING_CHECKSUM,
    GET_POINTING_DATA,
    PUT_POINTING_CPI,
#endif  // defined(POINTING_DEVICE_ENABLE) && defined(SPLIT_POINTING_ENABLE)

#if defined(SPLIT_TRANSACTION_IDS_KB) || defined(SPLIT_TRANSACTION_IDS_USER)
    PUT_RPC_INFO,
    PUT_RPC_REQ_DATA,

M quantum/split_common/transactions.c => quantum/split_common/transactions.c +79 -0
@@ 579,6 579,82 @@ static void st7565_handlers_slave(matrix_row_t master_matrix[], matrix_row_t sla
#endif  // defined(ST7565_ENABLE) && defined(SPLIT_ST7565_ENABLE)

////////////////////////////////////////////////////
// POINTING

#if defined(POINTING_DEVICE_ENABLE) && defined(SPLIT_POINTING_ENABLE)

static bool pointing_handlers_master(matrix_row_t master_matrix[], matrix_row_t slave_matrix[]) {
#    if defined(POINTING_DEVICE_LEFT)
    if (is_keyboard_left()) {
        return true;
    }
#    elif defined(POINTING_DEVICE_RIGHT)
    if (!is_keyboard_left()) {
        return true;
    }
#    endif
    static uint32_t last_update = 0;
    static uint16_t last_cpi    = 0;
    report_mouse_t  temp_state;
    uint16_t        temp_cpi;
    bool            okay = read_if_checksum_mismatch(GET_POINTING_CHECKSUM, GET_POINTING_DATA, &last_update, &temp_state, &split_shmem->pointing.report, sizeof(temp_state));
    if (okay) pointing_device_set_shared_report(temp_state);
    temp_cpi = pointing_device_get_shared_cpi();
    if (temp_cpi && memcmp(&last_cpi, &temp_cpi, sizeof(temp_cpi)) != 0) {
        memcpy(&split_shmem->pointing.cpi, &temp_cpi, sizeof(temp_cpi));
        okay = transport_write(PUT_POINTING_CPI, &split_shmem->pointing.cpi, sizeof(split_shmem->pointing.cpi));
        if (okay) {
            last_cpi = temp_cpi;
        }
    }
    return okay;
}

extern const pointing_device_driver_t pointing_device_driver;

static void pointing_handlers_slave(matrix_row_t master_matrix[], matrix_row_t slave_matrix[]) {
#    if defined(POINTING_DEVICE_LEFT)
    if (!is_keyboard_left()) {
        return;
    }
#    elif defined(POINTING_DEVICE_RIGHT)
    if (is_keyboard_left()) {
        return;
    }
#    endif
    report_mouse_t temp_report;
    uint16_t       temp_cpi;
#    ifdef POINTING_DEVICE_TASK_THROTTLE_MS
    static uint32_t last_exec = 0;
    if (timer_elapsed32(last_exec) < POINTING_DEVICE_TASK_THROTTLE_MS) {
        return;
    }
    last_exec = timer_read32();
#    endif
    temp_cpi = pointing_device_driver.get_cpi();
    if (split_shmem->pointing.cpi && memcmp(&split_shmem->pointing.cpi, &temp_cpi, sizeof(temp_cpi)) != 0) {
        pointing_device_driver.set_cpi(split_shmem->pointing.cpi);
    }
    memset(&temp_report, 0, sizeof(temp_report));
    temp_report = pointing_device_driver.get_report(temp_report);
    memcpy(&split_shmem->pointing.report, &temp_report, sizeof(temp_report));
    // Now update the checksum given that the pointing has been written to
    split_shmem->pointing.checksum = crc8(&temp_report, sizeof(temp_report));
}

#    define TRANSACTIONS_POINTING_MASTER() TRANSACTION_HANDLER_MASTER(pointing)
#    define TRANSACTIONS_POINTING_SLAVE() TRANSACTION_HANDLER_SLAVE(pointing)
#    define TRANSACTIONS_POINTING_REGISTRATIONS [GET_POINTING_CHECKSUM] = trans_target2initiator_initializer(pointing.checksum), [GET_POINTING_DATA] = trans_target2initiator_initializer(pointing.report), [PUT_POINTING_CPI] = trans_initiator2target_initializer(pointing.cpi),

#else  // defined(POINTING_DEVICE_ENABLE) && defined(SPLIT_POINTING_ENABLE)

#    define TRANSACTIONS_POINTING_MASTER()
#    define TRANSACTIONS_POINTING_SLAVE()
#    define TRANSACTIONS_POINTING_REGISTRATIONS

#endif  // defined(POINTING_DEVICE_ENABLE) && defined(SPLIT_POINTING_ENABLE)

////////////////////////////////////////////////////

uint8_t                  dummy;
split_transaction_desc_t split_transaction_table[NUM_TOTAL_TRANSACTIONS] = {


@@ 604,6 680,7 @@ split_transaction_desc_t split_transaction_table[NUM_TOTAL_TRANSACTIONS] = {
    TRANSACTIONS_WPM_REGISTRATIONS
    TRANSACTIONS_OLED_REGISTRATIONS
    TRANSACTIONS_ST7565_REGISTRATIONS
    TRANSACTIONS_POINTING_REGISTRATIONS
// clang-format on

#if defined(SPLIT_TRANSACTION_IDS_KB) || defined(SPLIT_TRANSACTION_IDS_USER)


@@ 629,6 706,7 @@ bool transactions_master(matrix_row_t master_matrix[], matrix_row_t slave_matrix
    TRANSACTIONS_WPM_MASTER();
    TRANSACTIONS_OLED_MASTER();
    TRANSACTIONS_ST7565_MASTER();
    TRANSACTIONS_POINTING_MASTER();
    return true;
}



@@ 647,6 725,7 @@ void transactions_slave(matrix_row_t master_matrix[], matrix_row_t slave_matrix[
    TRANSACTIONS_WPM_SLAVE();
    TRANSACTIONS_OLED_SLAVE();
    TRANSACTIONS_ST7565_SLAVE();
    TRANSACTIONS_POINTING_SLAVE();
}

#if defined(SPLIT_TRANSACTION_IDS_KB) || defined(SPLIT_TRANSACTION_IDS_USER)

M quantum/split_common/transport.h => quantum/split_common/transport.h +13 -0
@@ 106,6 106,15 @@ typedef struct _split_mods_sync_t {
} split_mods_sync_t;
#endif  // SPLIT_MODS_ENABLE

#if defined(POINTING_DEVICE_ENABLE) && defined(SPLIT_POINTING_ENABLE)
#    include "pointing_device.h"
typedef struct _split_slave_pointing_sync_t {
    uint8_t        checksum;
    report_mouse_t report;
    uint16_t       cpi;
} split_slave_pointing_sync_t;
#endif  // defined(POINTING_DEVICE_ENABLE) && defined(SPLIT_POINTING_ENABLE)

#if defined(SPLIT_TRANSACTION_IDS_KB) || defined(SPLIT_TRANSACTION_IDS_USER)
typedef struct _rpc_sync_info_t {
    int8_t  transaction_id;


@@ 173,6 182,10 @@ typedef struct _split_shared_memory_t {
    uint8_t current_st7565_state;
#endif  // ST7565_ENABLE(OLED_ENABLE) && defined(SPLIT_ST7565_ENABLE)

#if defined(POINTING_DEVICE_ENABLE) && defined(SPLIT_POINTING_ENABLE)
    split_slave_pointing_sync_t pointing;
#endif  // defined(POINTING_DEVICE_ENABLE) && defined(SPLIT_POINTING_ENABLE)

#if defined(SPLIT_TRANSACTION_IDS_KB) || defined(SPLIT_TRANSACTION_IDS_USER)
    rpc_sync_info_t rpc_info;
    uint8_t         rpc_m2s_buffer[RPC_M2S_BUFFER_SIZE];