diff --git a/qmk_firmware/keyboards/keyball/keyball61/config.h b/qmk_firmware/keyboards/keyball/keyball61/config.h
index 2e7d6965f..6573594eb 100644
--- a/qmk_firmware/keyboards/keyball/keyball61/config.h
+++ b/qmk_firmware/keyboards/keyball/keyball61/config.h
@@ -22,7 +22,7 @@ along with this program. If not, see .
// USB Device descriptor parameters
#define VENDOR_ID 0x5957 // "YW" = Yowkees
-#define PRODUCT_ID 0x0100 // dummy ID
+#define PRODUCT_ID 0x0100
#define DEVICE_VER 0x0001
#define MANUFACTURER Yowkees
#define PRODUCT Keyball61
diff --git a/qmk_firmware/keyboards/keyball/keyball61/keyball61.c b/qmk_firmware/keyboards/keyball/keyball61/keyball61.c
index a2ac770d4..1c90f5945 100644
--- a/qmk_firmware/keyboards/keyball/keyball61/keyball61.c
+++ b/qmk_firmware/keyboards/keyball/keyball61/keyball61.c
@@ -18,68 +18,7 @@ along with this program. If not, see .
#include QMK_KEYBOARD_H
-#include
-
-#include "quantum.h"
-
-#include "transactions.h"
-#include "drivers/pmw3360/pmw3360.h"
-
-//////////////////////////////////////////////////////////////////////////////
-
-#define KEYBALL_CPI_DEFAULT 4 // 4: 500 CPI
-#define KEYBALL_SCROLL_DIV_DEFAULT 4
-
-//////////////////////////////////////////////////////////////////////////////
-
-#define TX_GETINFO_INTERVAL 500
-#define TX_GETINFO_MAXTRY 10
-#define TX_GETMOTION_INTERVAL 5
-
-//////////////////////////////////////////////////////////////////////////////
-
-typedef union {
- uint32_t raw;
- struct {
- uint8_t cpi : 7;
- uint8_t sdiv : 3; // scroll divider
- };
-} keyball_config_t;
-
-typedef struct {
- uint16_t vid;
- uint16_t pid;
- uint8_t ballcnt; // count of balls: support only 0 or 1, for now
-} keyball_info_t;
-
-typedef uint8_t keyball_motion_id_t;
-
-typedef struct {
- int16_t x;
- int16_t y;
-} keyball_motion_t;
-
-typedef uint8_t keyball_cpi_t;
-
-//////////////////////////////////////////////////////////////////////////////
-
-static bool this_have_ball = false;
-static bool that_enable = false;
-static bool that_have_ball = false;
-
-static uint8_t cpi_value = KEYBALL_CPI_DEFAULT;
-static bool cpi_changed = false;
-
-static keyball_motion_t this_motion = {0};
-static keyball_motion_t that_motion = {0};
-
-static bool scroll_mode = false;
-static uint8_t scroll_div = 0;
-
-static uint16_t last_keycode;
-static uint8_t last_row;
-static uint8_t last_col;
-static report_mouse_t last_mouse = {};
+#include "lib/keyball/keyball.h"
//////////////////////////////////////////////////////////////////////////////
@@ -98,476 +37,12 @@ matrix_row_t matrix_mask[MATRIX_ROWS] = {
};
// clang-format on
-static void adjust_rgblight_ranges(void) {
+void keyball_on_adjust_layout(keyball_adjust_t v) {
#ifdef RGBLIGHT_ENABLE
// adjust RGBLIGHT's clipping and effect ranges
- uint8_t lednum_this = this_have_ball ? 34 : 37;
- uint8_t lednum_that = !that_enable ? 0 : that_have_ball ? 34 : 37;
+ uint8_t lednum_this = keyball.this_have_ball ? 34 : 37;
+ uint8_t lednum_that = !keyball.that_enable ? 0 : keyball.that_have_ball ? 34 : 37;
rgblight_set_clipping_range(is_keyboard_left() ? 0 : lednum_that, lednum_this);
rgblight_set_effect_range(0, lednum_this + lednum_that);
#endif
}
-
-static void adjust_board_as_this(void) {
- adjust_rgblight_ranges();
-
- keyball_config_t c;
- c.raw = eeconfig_read_kb();
- if (c.cpi != 0) {
- pointing_device_set_cpi(c.cpi);
- }
- scroll_div = c.sdiv;
-}
-
-static void adjust_board_on_primary(void) {
- adjust_rgblight_ranges();
-
-#ifdef VIA_ENABLE
- // adjust layout options value according to current combination.
- bool left = is_keyboard_left();
- uint8_t layouts = (this_have_ball ? (left ? 0x02 : 0x01) : 0x00) | (that_have_ball ? (left ? 0x01 : 0x02) : 0x00);
- uint32_t curr = via_get_layout_options();
- uint32_t next = (curr & ~0x3) | layouts;
- if (next != curr) {
- via_set_layout_options(next);
- }
-#endif
-}
-
-static void adjust_board_on_secondary(void) { adjust_rgblight_ranges(); }
-
-//////////////////////////////////////////////////////////////////////////////
-
-// add16 adds two int16_t with clipping.
-static int16_t add16(int16_t a, int16_t b) {
- int16_t r = a + b;
- if (a >= 0 && b >= 0 && r < 0) {
- r = 32767;
- } else if (a < 0 && b < 0 && r >= 0) {
- r = -32768;
- }
- return r;
-}
-
-// incU8 increments a uint8_t with clipping.
-static inline uint8_t incU8(uint8_t a) { return a < 0xff ? a + 1 : 0xff; }
-
-// clip2int8 clips an integer fit into int8_t.
-static inline int8_t clip2int8(int16_t v) { return (v) < -127 ? -127 : (v) > 127 ? 127 : (int8_t)v; }
-
-static void motion_to_mouse_move(keyball_motion_t *m, report_mouse_t *r, bool is_left) {
- r->x = clip2int8(m->y);
- r->y = clip2int8(m->x);
- if (is_left) {
- r->x = -r->x;
- r->y = -r->y;
- }
- // clear motion
- m->x = 0;
- m->y = 0;
-}
-
-static void motion_to_mouse_scroll(keyball_motion_t *m, report_mouse_t *r, bool is_left) {
- int16_t x = m->x >> scroll_div;
- m->x -= x << scroll_div;
- int16_t y = m->y >> scroll_div;
- m->y -= y << scroll_div;
- r->h = clip2int8(y);
- r->v = clip2int8(x);
- if (!is_left) {
- r->h = -r->h;
- r->v = -r->v;
- }
-}
-
-static void motion_to_mouse(keyball_motion_t *m, report_mouse_t *r, bool is_left, bool as_scroll) {
- if (as_scroll) {
- motion_to_mouse_scroll(m, r, is_left);
- } else {
- motion_to_mouse_move(m, r, is_left);
- }
-}
-
-void pointing_device_driver_init(void) {
- this_have_ball = pmw3360_init();
- if (this_have_ball) {
- pmw3360_reg_write(pmw3360_Config1, KEYBALL_CPI_DEFAULT);
- pmw3360_reg_write(pmw3360_Motion_Burst, 0);
- }
-}
-
-report_mouse_t pointing_device_driver_get_report(report_mouse_t rep) {
- // fetch from optical sensor.
- if (this_have_ball) {
- pmw3360_motion_t d = {0};
- if (pmw3360_motion_burst(&d)) {
- ATOMIC_BLOCK_FORCEON {
- this_motion.x = add16(this_motion.x, d.x);
- this_motion.y = add16(this_motion.y, d.y);
- }
- }
- }
- // report mouse event, if keyboard is primary.
- if (is_keyboard_master()) {
- bool is_left = is_keyboard_left();
- if (this_have_ball) {
- motion_to_mouse(&this_motion, &rep, is_left, scroll_mode);
- }
- if (that_have_ball) {
- motion_to_mouse(&that_motion, &rep, !is_left, scroll_mode ^ this_have_ball);
- }
- }
- return rep;
-}
-
-uint16_t pointing_device_driver_get_cpi(void) { return cpi_value; }
-
-void pointing_device_driver_set_cpi(uint16_t cpi) {
- if (this_have_ball) {
- pmw3360_cpi_set(cpi);
- pmw3360_reg_write(pmw3360_Motion_Burst, 0);
- }
- cpi_value = cpi;
- cpi_changed = true;
-}
-
-static void add_cpi(int8_t delta) {
- int16_t v = cpi_value + delta;
- if (v < 0) {
- v = 0;
- } else if (v > pmw3360_MAXCPI) {
- v = pmw3360_MAXCPI;
- }
- pointing_device_set_cpi(v);
-}
-
-//////////////////////////////////////////////////////////////////////////////
-
-static void keyball_get_info_handler(uint8_t in_buflen, const void *in_data, uint8_t out_buflen, void *out_data) {
- keyball_info_t *that = (keyball_info_t *)in_data;
- if (that->vid == VENDOR_ID && that->pid == PRODUCT_ID) {
- that_enable = true;
- that_have_ball = that->ballcnt > 0;
- }
- keyball_info_t info = {
- .vid = VENDOR_ID,
- .pid = PRODUCT_ID,
- .ballcnt = this_have_ball ? 1 : 0,
- };
- memcpy(out_data, &info, sizeof(info));
- adjust_board_on_secondary();
-}
-
-static void keyball_get_info_invoke(void) {
- static bool negotiated = false;
- static uint32_t last_sync = 0;
- static int round = 0;
- if (negotiated || timer_elapsed32(last_sync) < TX_GETINFO_INTERVAL) {
- return;
- }
- last_sync = timer_read32();
- round++;
- keyball_info_t req = {
- .vid = VENDOR_ID,
- .pid = PRODUCT_ID,
- .ballcnt = this_have_ball ? 1 : 0,
- };
- keyball_info_t recv = {0};
- if (!transaction_rpc_exec(KEYBALL_GET_INFO, sizeof(req), &req, sizeof(recv), &recv)) {
- if (round < TX_GETINFO_MAXTRY) {
- dprintf("keyball_get_info_invoke: missed #%d\n", round);
- return;
- }
- }
- negotiated = true;
- if (recv.vid == VENDOR_ID && recv.pid == PRODUCT_ID) {
- that_enable = true;
- that_have_ball = recv.ballcnt > 0;
- }
- dprintf("keyball_get_info_invoke: negotiated #%d %d\n", round, that_have_ball);
- if (is_keyboard_master()) {
- adjust_board_on_primary();
- } else {
- adjust_board_on_secondary();
- }
-}
-
-static void keyball_get_motion_handler(uint8_t in_buflen, const void *in_data, uint8_t out_buflen, void *out_data) {
- if (this_have_ball && *((keyball_motion_id_t *)in_data) == 0) {
- *(keyball_motion_t *)out_data = this_motion;
- // clear motion
- this_motion.x = 0;
- this_motion.y = 0;
- }
-}
-
-static void keyball_get_motion_invoke(void) {
- static uint32_t last_sync = 0;
- if (!that_have_ball || timer_elapsed32(last_sync) < TX_GETMOTION_INTERVAL) {
- return;
- }
- keyball_motion_id_t req = 0;
- keyball_motion_t recv = {0};
- if (transaction_rpc_exec(KEYBALL_GET_MOTION, sizeof(req), &req, sizeof(recv), &recv)) {
- ATOMIC_BLOCK_FORCEON {
- that_motion.x = add16(that_motion.x, recv.x);
- that_motion.y = add16(that_motion.y, recv.y);
- }
- } else {
- dprintf("keyball_get_motion_invoke: failed");
- }
- last_sync = timer_read32();
- return;
-}
-
-static void keyball_set_cpi_handler(uint8_t in_buflen, const void *in_data, uint8_t out_buflen, void *out_data) {
- if (this_have_ball) {
- pmw3360_cpi_set(*(keyball_cpi_t *)in_data);
- pmw3360_reg_write(pmw3360_Motion_Burst, 0);
- }
-}
-
-static void keyball_set_cpi_invoke(void) {
- if (!that_have_ball || !cpi_changed) {
- return;
- }
- keyball_cpi_t req = cpi_value;
- if (!transaction_rpc_send(KEYBALL_SET_CPI, sizeof(req), &req)) {
- return;
- }
- cpi_changed = false;
-}
-
-//////////////////////////////////////////////////////////////////////////////
-
-bool keyball_get_scroll_mode(void) { return scroll_mode; }
-
-void keyball_set_scroll_mode(bool mode) {
- scroll_mode = mode;
-}
-
-#ifdef OLED_ENABLE
-
-static const char *format_4d(int8_t d) {
- static char buf[5] = {0}; // max width (4) + NUL (1)
- char lead = ' ';
- if (d < 0) {
- d = -d;
- lead = '-';
- }
- buf[3] = (d % 10) + '0';
- d /= 10;
- if (d == 0) {
- buf[2] = lead;
- lead = ' ';
- } else {
- buf[2] = (d % 10) + '0';
- d /= 10;
- }
- if (d == 0) {
- buf[1] = lead;
- lead = ' ';
- } else {
- buf[1] = (d % 10) + '0';
- d /= 10;
- }
- buf[0] = lead;
- return buf;
-}
-
-static char to_1x(uint8_t x) {
- x &= 0x0f;
- return x < 10 ? x + '0' : x + 'a' - 10;
-}
-
-#endif
-
-void keyball_oled_render_ballinfo(void) {
-#ifdef OLED_ENABLE
- // Format: `Ball:{mouse x}{mouse y}{mouse h}{mouse v}`
- // ` CPI{CPI} S{SCROLL_MODE} D{SCROLL_DIV}`
- //
- // Output example:
- //
- // Ball: -12 34 0 0
- //
- oled_write_P(PSTR("Ball:"), false);
- oled_write(format_4d(last_mouse.x), false);
- oled_write(format_4d(last_mouse.y), false);
- oled_write(format_4d(last_mouse.h), false);
- oled_write(format_4d(last_mouse.v), false);
- // CPI
- oled_write_P(PSTR(" CPI"), false);
- oled_write(format_4d(cpi_value + 1) + 1, false);
- oled_write_P(PSTR("00 S"), false);
- oled_write_char(scroll_mode ? '1' : '0', false);
- oled_write_P(PSTR(" D"), false);
- oled_write_char('0' + scroll_div, false);
-#endif
-}
-
-#ifdef OLED_ENABLE
-// clang-format off
-const char PROGMEM code_to_name[] = {
- 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
- 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
- 'u', 'v', 'w', 'x', 'y', 'z', '1', '2', '3', '4',
- '5', '6', '7', '8', '9', '0', 'R', 'E', 'B', 'T',
- '_', '-', '=', '[', ']', '\\', '#', ';', '\'', '`',
- ',', '.', '/',
-};
-// clang-format on
-#endif
-
-void keyball_oled_render_keyinfo(void) {
-#ifdef OLED_ENABLE
- // Format: `Key : R{row} C{col} K{kc} '{name}`
- //
- // Where `kc` is lower 8 bit of keycode.
- // Where `name` is readable label for `kc`, valid between 4 and 56.
- //
- // It is aligned to fit with output of keyball_oled_render_ballinfo().
- // For example:
- //
- // Key : R2 C3 K06 'c
- // Ball: 0 0 0 0
- //
- uint8_t keycode = last_keycode;
-
- oled_write_P(PSTR("Key : R"), false);
- oled_write_char(to_1x(last_row), false);
- oled_write_P(PSTR(" C"), false);
- oled_write_char(to_1x(last_col), false);
- if (keycode) {
- oled_write_P(PSTR(" K"), false);
- oled_write_char(to_1x(keycode >> 4), false);
- oled_write_char(to_1x(keycode), false);
- }
- if (keycode >= 4 && keycode < 57) {
- oled_write_P(PSTR(" '"), false);
- char name = pgm_read_byte(code_to_name + keycode - 4);
- oled_write_char(name, false);
- } else {
- oled_advance_page(true);
- }
-#endif
-}
-
-//////////////////////////////////////////////////////////////////////////////
-
-void keyboard_post_init_kb(void) {
- // register transaction handlers on secondary.
- if (!is_keyboard_master()) {
- transaction_register_rpc(KEYBALL_GET_INFO, keyball_get_info_handler);
- transaction_register_rpc(KEYBALL_GET_MOTION, keyball_get_motion_handler);
- transaction_register_rpc(KEYBALL_SET_CPI, keyball_set_cpi_handler);
- }
- adjust_board_as_this();
- keyboard_post_init_user();
-}
-
-void housekeeping_task_kb(void) {
- if (is_keyboard_master()) {
- keyball_get_info_invoke();
- keyball_get_motion_invoke();
- keyball_set_cpi_invoke();
- }
-}
-
-bool process_record_kb(uint16_t keycode, keyrecord_t *record) {
- // store last keycode, row, and col for OLED
- last_keycode = keycode;
- last_row = record->event.key.row;
- last_col = record->event.key.col;
-
- if (!process_record_user(keycode, record)) {
- return false;
- }
-
- switch (keycode) {
- // process KC_MS_BTN1~8 by myself
- // See process_action() in quantum/action.c for details.
-#ifndef MOUSEKEY_ENABLE
- case KC_MS_BTN1 ... KC_MS_BTN8: {
- extern void register_button(bool, enum mouse_buttons);
- register_button(record->event.pressed, MOUSE_BTN_MASK(keycode - KC_MS_BTN1));
- break;
- }
-#endif
-
- case KBC_RST:
- if (record->event.pressed) {
- pointing_device_set_cpi(KEYBALL_CPI_DEFAULT);
- scroll_div = KEYBALL_SCROLL_DIV_DEFAULT;
- }
- break;
- case KBC_SAVE:
- if (record->event.pressed) {
- keyball_config_t c = {
- .cpi = cpi_value,
- .sdiv = scroll_div,
- };
- eeconfig_update_kb(c.raw);
- }
- break;
-
- case CPI_I100:
- if (record->event.pressed) {
- add_cpi(1);
- }
- break;
- case CPI_D100:
- if (record->event.pressed) {
- add_cpi(-1);
- }
- break;
- case CPI_I1K:
- if (record->event.pressed) {
- add_cpi(10);
- }
- break;
- case CPI_D1K:
- if (record->event.pressed) {
- add_cpi(-10);
- }
- break;
-
- case SCRL_TO:
- if (record->event.pressed) {
- scroll_mode = !scroll_mode;
- }
- break;
- case SCRL_MO:
- scroll_mode = record->event.pressed;
- break;
- case SCRL_DVI:
- if (record->event.pressed && scroll_div < 7) {
- scroll_div++;
- }
- break;
- case SCRL_DVD:
- if (record->event.pressed && scroll_div > 0) {
- scroll_div--;
- }
- break;
-
- default:
- return true;
- }
- return false;
-}
-
-report_mouse_t pointing_device_task_kb(report_mouse_t mouse_report) {
- // store mouse report for OLED.
- last_mouse = pointing_device_task_user(mouse_report);
- return last_mouse;
-}
-
-void eeconfig_init_kb(void) {
- keyball_config_t c = {
- .cpi = 0,
- .sdiv = KEYBALL_SCROLL_DIV_DEFAULT,
- };
- eeconfig_update_kb(c.raw);
- eeconfig_init_user();
-}
diff --git a/qmk_firmware/keyboards/keyball/keyball61/keyball61.h b/qmk_firmware/keyboards/keyball/keyball61/keyball61.h
index ceb7c5906..857d83fda 100644
--- a/qmk_firmware/keyboards/keyball/keyball61/keyball61.h
+++ b/qmk_firmware/keyboards/keyball/keyball61/keyball61.h
@@ -19,6 +19,7 @@ along with this program. If not, see .
#pragma once
#include "quantum.h"
+#include "lib/keyball/keyball.h"
// clang-format off
@@ -106,40 +107,3 @@ along with this program. If not, see .
#define LAYOUT LAYOUT_right_ball
#define LAYOUT_universal LAYOUT_no_ball
-
-//////////////////////////////////////////////////////////////////////////////
-
-enum keyball_keycodes {
- KBC_RST = SAFE_RANGE, // Keyball configuration: reset to default
- KBC_SAVE, // Keyball configuration: save to EEPROM
-
- CPI_I100, // CPI +100 CPI
- CPI_D100, // CPI -100 CPI
- CPI_I1K, // CPI +1000 CPI
- CPI_D1K, // CPI -1000 CPI
-
- // In scroll mode, motion from primary trackball is treated as scroll
- // wheel.
- SCRL_TO, // Toggle scroll mode
- SCRL_MO, // Momentary scroll mode
- SCRL_DVI, // Increment scroll divider
- SCRL_DVD, // Decrement scroll divider
-};
-
-//////////////////////////////////////////////////////////////////////////////
-// Experimental API
-
-// keyball_get_scroll_mode returns current scroll ode of trackball.
-bool keyball_get_scroll_mode(void);
-
-// keyball_set_scroll_mode enables/disables scroll mode of trackball.
-// When scroll mode enabled, rotating trackball reports scrolling events.
-void keyball_set_scroll_mode(bool mode);
-
-// keyball_oled_render_ballinfo renders ball information to OLED.
-// It uses just 21 columns to show the info.
-void keyball_oled_render_ballinfo(void);
-
-// keyball_oled_render_keyinfo renders last processed key information to OLED.
-// It shows column, row, key code, and key name (if available).
-void keyball_oled_render_keyinfo(void);
diff --git a/qmk_firmware/keyboards/keyball/keyball61/rules.mk b/qmk_firmware/keyboards/keyball/keyball61/rules.mk
index 78f904752..a6d44e8c1 100644
--- a/qmk_firmware/keyboards/keyball/keyball61/rules.mk
+++ b/qmk_firmware/keyboards/keyball/keyball61/rules.mk
@@ -46,6 +46,9 @@ SLEEP_LED_ENABLE = no # Breathing sleep LED during USB suspend
OLED_ENABLE = no # Please Enable this in each keymaps.
SRC += lib/oledkit/oledkit.c # OLED utility for Keyball series.
+# Include common library
+SRC += lib/keyball/keyball.c
+
# Disable other features to squeeze firmware size
SPACE_CADET_ENABLE = no
MAGIC_ENABLE = no
diff --git a/qmk_firmware/keyboards/keyball/lib/keyball/keyball.c b/qmk_firmware/keyboards/keyball/lib/keyball/keyball.c
new file mode 100644
index 000000000..66d5e0d6e
--- /dev/null
+++ b/qmk_firmware/keyboards/keyball/lib/keyball/keyball.c
@@ -0,0 +1,500 @@
+/*
+Copyright 2022 MURAOKA Taro (aka KoRoN, @kaoriya)
+
+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 .
+*/
+
+#include "quantum.h"
+#include "transactions.h"
+
+#include "keyball.h"
+#include "drivers/pmw3360/pmw3360.h"
+
+const uint8_t CPI_DEFAULT = KEYBALL_CPI_DEFAULT / 100;
+const uint8_t CPI_MAX = pmw3360_MAXCPI + 1;
+const uint8_t SCROLL_DIV_MAX = 7;
+
+keyball_t keyball = {
+ .this_have_ball = false,
+ .that_enable = false,
+ .that_have_ball = false,
+
+ .this_motion = {0},
+ .that_motion = {0},
+
+ .cpi_value = 0,
+ .cpi_changed = false,
+
+ .scroll_mode = false,
+ .scroll_div = 0,
+};
+
+//////////////////////////////////////////////////////////////////////////////
+// Hook points
+
+__attribute__((weak)) void keyball_on_adjust_layout(keyball_adjust_t v) {}
+
+//////////////////////////////////////////////////////////////////////////////
+// Static utilities
+
+// add16 adds two int16_t with clipping.
+static int16_t add16(int16_t a, int16_t b) {
+ int16_t r = a + b;
+ if (a >= 0 && b >= 0 && r < 0) {
+ r = 32767;
+ } else if (a < 0 && b < 0 && r >= 0) {
+ r = -32768;
+ }
+ return r;
+}
+
+// clip2int8 clips an integer fit into int8_t.
+static inline int8_t clip2int8(int16_t v) { return (v) < -127 ? -127 : (v) > 127 ? 127 : (int8_t)v; }
+
+static const char *format_4d(int8_t d) {
+ static char buf[5] = {0}; // max width (4) + NUL (1)
+ char lead = ' ';
+ if (d < 0) {
+ d = -d;
+ lead = '-';
+ }
+ buf[3] = (d % 10) + '0';
+ d /= 10;
+ if (d == 0) {
+ buf[2] = lead;
+ lead = ' ';
+ } else {
+ buf[2] = (d % 10) + '0';
+ d /= 10;
+ }
+ if (d == 0) {
+ buf[1] = lead;
+ lead = ' ';
+ } else {
+ buf[1] = (d % 10) + '0';
+ d /= 10;
+ }
+ buf[0] = lead;
+ return buf;
+}
+
+static char to_1x(uint8_t x) {
+ x &= 0x0f;
+ return x < 10 ? x + '0' : x + 'a' - 10;
+}
+
+static void add_cpi(int8_t delta) {
+ int16_t v = keyball_get_cpi() + delta;
+ keyball_set_cpi(v < 1 ? 1 : v);
+}
+
+static void add_scroll_div(int8_t delta) {
+ int8_t v = keyball_get_scroll_div() + delta;
+ keyball_set_scroll_div(v < 1 ? 1 : v);
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Pointing device driver
+
+void pointing_device_driver_init(void) {
+ keyball.this_have_ball = pmw3360_init();
+ if (keyball.this_have_ball) {
+ pmw3360_cpi_set(CPI_DEFAULT - 1);
+ pmw3360_reg_write(pmw3360_Motion_Burst, 0);
+ }
+}
+
+uint16_t pointing_device_driver_get_cpi(void) { return keyball_get_cpi(); }
+
+void pointing_device_driver_set_cpi(uint16_t cpi) { keyball_set_cpi(cpi); }
+
+static void motion_to_mouse_move(keyball_motion_t *m, report_mouse_t *r, bool is_left) {
+ r->x = clip2int8(m->y);
+ r->y = clip2int8(m->x);
+ if (is_left) {
+ r->x = -r->x;
+ r->y = -r->y;
+ }
+ // clear motion
+ m->x = 0;
+ m->y = 0;
+}
+
+static void motion_to_mouse_scroll(keyball_motion_t *m, report_mouse_t *r, bool is_left) {
+ uint8_t div = keyball_get_scroll_div() - 1;
+ int16_t x = m->x >> div;
+ m->x -= x << div;
+ int16_t y = m->y >> div;
+ m->y -= y << div;
+ r->h = clip2int8(y);
+ r->v = clip2int8(x);
+ if (!is_left) {
+ r->h = -r->h;
+ r->v = -r->v;
+ }
+}
+
+static void motion_to_mouse(keyball_motion_t *m, report_mouse_t *r, bool is_left, bool as_scroll) {
+ if (as_scroll) {
+ motion_to_mouse_scroll(m, r, is_left);
+ } else {
+ motion_to_mouse_move(m, r, is_left);
+ }
+}
+
+report_mouse_t pointing_device_driver_get_report(report_mouse_t rep) {
+ // fetch from optical sensor.
+ if (keyball.this_have_ball) {
+ pmw3360_motion_t d = {0};
+ if (pmw3360_motion_burst(&d)) {
+ ATOMIC_BLOCK_FORCEON {
+ keyball.this_motion.x = add16(keyball.this_motion.x, d.x);
+ keyball.this_motion.y = add16(keyball.this_motion.y, d.y);
+ }
+ }
+ }
+ // report mouse event, if keyboard is primary.
+ if (is_keyboard_master()) {
+ if (keyball.this_have_ball) {
+ motion_to_mouse(&keyball.this_motion, &rep, is_keyboard_left(), keyball.scroll_mode);
+ }
+ if (keyball.that_have_ball) {
+ motion_to_mouse(&keyball.that_motion, &rep, !is_keyboard_left(), keyball.scroll_mode ^ keyball.this_have_ball);
+ }
+ }
+ return rep;
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Split RPC
+
+static void rpc_get_info_handler(uint8_t in_buflen, const void *in_data, uint8_t out_buflen, void *out_data) {
+ keyball_info_t *that = (keyball_info_t *)in_data;
+ if (that->vid == VENDOR_ID && that->pid == PRODUCT_ID) {
+ keyball.that_enable = true;
+ keyball.that_have_ball = that->ballcnt > 0;
+ }
+ keyball_info_t info = {
+ .vid = VENDOR_ID,
+ .pid = PRODUCT_ID,
+ .ballcnt = keyball.this_have_ball ? 1 : 0,
+ };
+ *(keyball_info_t *)out_data = info;
+ keyball_on_adjust_layout(KEYBALL_ADJUST_SECONDARY);
+}
+
+static void rpc_get_info_invoke(void) {
+ static bool negotiated = false;
+ static uint32_t last_sync = 0;
+ static int round = 0;
+ if (negotiated || timer_elapsed32(last_sync) < KEYBALL_TX_GETINFO_INTERVAL) {
+ return;
+ }
+ last_sync = timer_read32();
+ round++;
+ keyball_info_t req = {
+ .vid = VENDOR_ID,
+ .pid = PRODUCT_ID,
+ .ballcnt = keyball.this_have_ball ? 1 : 0,
+ };
+ keyball_info_t recv = {0};
+ if (!transaction_rpc_exec(KEYBALL_GET_INFO, sizeof(req), &req, sizeof(recv), &recv)) {
+ if (round < KEYBALL_TX_GETINFO_MAXTRY) {
+ dprintf("keyball:rpc_get_info_invoke: missed #%d\n", round);
+ return;
+ }
+ }
+ negotiated = true;
+ if (recv.vid == VENDOR_ID && recv.pid == PRODUCT_ID) {
+ keyball.that_enable = true;
+ keyball.that_have_ball = recv.ballcnt > 0;
+ }
+ dprintf("keyball:rpc_get_info_invoke: negotiated #%d %d\n", round, keyball.that_have_ball);
+
+ // split keyboard negotiation completed.
+
+#ifdef VIA_ENABLE
+ // adjust VIA layout options according to current combination.
+ uint8_t layouts = (keyball.this_have_ball ? (is_keyboard_left() ? 0x02 : 0x01) : 0x00) | (keyball.that_have_ball ? (is_keyboard_left() ? 0x01 : 0x02) : 0x00);
+ uint32_t curr = via_get_layout_options();
+ uint32_t next = (curr & ~0x3) | layouts;
+ if (next != curr) {
+ via_set_layout_options(next);
+ }
+#endif
+
+ keyball_on_adjust_layout(KEYBALL_ADJUST_PRIMARY);
+}
+
+static void rpc_get_motion_handler(uint8_t in_buflen, const void *in_data, uint8_t out_buflen, void *out_data) {
+ if (keyball.this_have_ball && *((keyball_motion_id_t *)in_data) == 0) {
+ *(keyball_motion_t *)out_data = keyball.this_motion;
+ // clear motion
+ keyball.this_motion.x = 0;
+ keyball.this_motion.y = 0;
+ }
+}
+
+static void rpc_get_motion_invoke(void) {
+ static uint32_t last_sync = 0;
+ if (!keyball.that_have_ball || timer_elapsed32(last_sync) < KEYBALL_TX_GETMOTION_INTERVAL) {
+ return;
+ }
+ keyball_motion_id_t req = 0;
+ keyball_motion_t recv = {0};
+ if (transaction_rpc_exec(KEYBALL_GET_MOTION, sizeof(req), &req, sizeof(recv), &recv)) {
+ ATOMIC_BLOCK_FORCEON {
+ keyball.that_motion.x = add16(keyball.that_motion.x, recv.x);
+ keyball.that_motion.y = add16(keyball.that_motion.y, recv.y);
+ }
+ } else {
+ dprintf("keyball:rpc_get_motion_invoke: failed");
+ }
+ last_sync = timer_read32();
+ return;
+}
+
+static void rpc_set_cpi_handler(uint8_t in_buflen, const void *in_data, uint8_t out_buflen, void *out_data) {
+ if (keyball.this_have_ball) {
+ keyball_set_cpi(*(keyball_cpi_t *)in_data);
+ }
+}
+
+static void rpc_set_cpi_invoke(void) {
+ if (!keyball.that_have_ball || !keyball.cpi_changed) {
+ return;
+ }
+ keyball_cpi_t req = keyball.cpi_value;
+ if (!transaction_rpc_send(KEYBALL_SET_CPI, sizeof(req), &req)) {
+ return;
+ }
+ keyball.cpi_changed = false;
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// OLED utility
+
+#ifdef OLED_ENABLE
+// clang-format off
+const char PROGMEM code_to_name[] = {
+ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
+ 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
+ 'u', 'v', 'w', 'x', 'y', 'z', '1', '2', '3', '4',
+ '5', '6', '7', '8', '9', '0', 'R', 'E', 'B', 'T',
+ '_', '-', '=', '[', ']', '\\', '#', ';', '\'', '`',
+ ',', '.', '/',
+};
+// clang-format on
+#endif
+
+void keyball_oled_render_ballinfo(void) {
+#ifdef OLED_ENABLE
+ // Format: `Ball:{mouse x}{mouse y}{mouse h}{mouse v}`
+ // ` CPI{CPI} S{SCROLL_MODE} D{SCROLL_DIV}`
+ //
+ // Output example:
+ //
+ // Ball: -12 34 0 0
+ //
+ oled_write_P(PSTR("Ball:"), false);
+ oled_write(format_4d(keyball.last_mouse.x), false);
+ oled_write(format_4d(keyball.last_mouse.y), false);
+ oled_write(format_4d(keyball.last_mouse.h), false);
+ oled_write(format_4d(keyball.last_mouse.v), false);
+ // CPI
+ oled_write_P(PSTR(" CPI"), false);
+ oled_write(format_4d(keyball_get_cpi()) + 1, false);
+ oled_write_P(PSTR("00 S"), false);
+ oled_write_char(keyball.scroll_mode ? '1' : '0', false);
+ oled_write_P(PSTR(" D"), false);
+ oled_write_char('0' + keyball_get_scroll_div(), false);
+#endif
+}
+
+void keyball_oled_render_keyinfo(void) {
+#ifdef OLED_ENABLE
+ // Format: `Key : R{row} C{col} K{kc} '{name}`
+ //
+ // Where `kc` is lower 8 bit of keycode.
+ // Where `name` is readable label for `kc`, valid between 4 and 56.
+ //
+ // It is aligned to fit with output of keyball_oled_render_ballinfo().
+ // For example:
+ //
+ // Key : R2 C3 K06 'c
+ // Ball: 0 0 0 0
+ //
+ uint8_t keycode = keyball.last_kc;
+
+ oled_write_P(PSTR("Key : R"), false);
+ oled_write_char(to_1x(keyball.last_pos.row), false);
+ oled_write_P(PSTR(" C"), false);
+ oled_write_char(to_1x(keyball.last_pos.col), false);
+ if (keycode) {
+ oled_write_P(PSTR(" K"), false);
+ oled_write_char(to_1x(keycode >> 4), false);
+ oled_write_char(to_1x(keycode), false);
+ }
+ if (keycode >= 4 && keycode < 57) {
+ oled_write_P(PSTR(" '"), false);
+ char name = pgm_read_byte(code_to_name + keycode - 4);
+ oled_write_char(name, false);
+ } else {
+ oled_advance_page(true);
+ }
+#endif
+}
+
+//////////////////////////////////////////////////////////////////////////////
+
+bool keyball_get_scroll_mode(void) { return keyball.scroll_mode; }
+
+void keyball_set_scroll_mode(bool mode) { keyball.scroll_mode = mode; }
+
+uint8_t keyball_get_scroll_div(void) { return keyball.scroll_div == 0 ? KEYBALL_SCROLL_DIV_DEFAULT : keyball.scroll_div; }
+
+void keyball_set_scroll_div(uint8_t div) { keyball.scroll_div = div > SCROLL_DIV_MAX ? SCROLL_DIV_MAX : div; }
+
+uint8_t keyball_get_cpi(void) { return keyball.cpi_value == 0 ? CPI_DEFAULT : keyball.cpi_value; }
+
+void keyball_set_cpi(uint8_t cpi) {
+ if (cpi > CPI_MAX) {
+ cpi = CPI_MAX;
+ }
+ keyball.cpi_value = cpi;
+ keyball.cpi_changed = true;
+ if (keyball.this_have_ball) {
+ pmw3360_cpi_set(cpi == 0 ? CPI_DEFAULT - 1 : cpi - 1);
+ pmw3360_reg_write(pmw3360_Motion_Burst, 0);
+ }
+}
+
+//////////////////////////////////////////////////////////////////////////////
+
+void keyboard_post_init_kb(void) {
+ // register transaction handlers on secondary.
+ if (!is_keyboard_master()) {
+ transaction_register_rpc(KEYBALL_GET_INFO, rpc_get_info_handler);
+ transaction_register_rpc(KEYBALL_GET_MOTION, rpc_get_motion_handler);
+ transaction_register_rpc(KEYBALL_SET_CPI, rpc_set_cpi_handler);
+ }
+
+ // read keyball configuration from EEPROM
+ if (eeconfig_is_enabled()) {
+ keyball_config_t c = {.raw = eeconfig_read_kb()};
+ if (c.cpi != 0) {
+ keyball_set_cpi(c.cpi);
+ }
+ keyball_set_scroll_div(c.sdiv);
+ }
+
+ keyball_on_adjust_layout(KEYBALL_ADJUST_PENDING);
+ keyboard_post_init_user();
+}
+
+void housekeeping_task_kb(void) {
+ if (is_keyboard_master()) {
+ rpc_get_info_invoke();
+ rpc_get_motion_invoke();
+ rpc_set_cpi_invoke();
+ }
+}
+
+report_mouse_t pointing_device_task_kb(report_mouse_t mouse_report) {
+ // store mouse report for OLED.
+ keyball.last_mouse = pointing_device_task_user(mouse_report);
+ return keyball.last_mouse;
+}
+
+bool process_record_kb(uint16_t keycode, keyrecord_t *record) {
+ // store last keycode, row, and col for OLED
+ keyball.last_kc = keycode;
+ keyball.last_pos = record->event.key;
+
+ if (!process_record_user(keycode, record)) {
+ return false;
+ }
+
+ switch (keycode) {
+ // process KC_MS_BTN1~8 by myself
+ // See process_action() in quantum/action.c for details.
+#ifndef MOUSEKEY_ENABLE
+ case KC_MS_BTN1 ... KC_MS_BTN8: {
+ extern void register_button(bool, enum mouse_buttons);
+ register_button(record->event.pressed, MOUSE_BTN_MASK(keycode - KC_MS_BTN1));
+ break;
+ }
+#endif
+
+ case KBC_RST:
+ if (record->event.pressed) {
+ keyball_set_cpi(0);
+ keyball_set_scroll_div(0);
+ }
+ break;
+ case KBC_SAVE:
+ if (record->event.pressed) {
+ keyball_config_t c = {
+ .cpi = keyball.cpi_value,
+ .sdiv = keyball.scroll_div,
+ };
+ eeconfig_update_kb(c.raw);
+ }
+ break;
+
+ case CPI_I100:
+ if (record->event.pressed) {
+ add_cpi(1);
+ }
+ break;
+ case CPI_D100:
+ if (record->event.pressed) {
+ add_cpi(-1);
+ }
+ break;
+ case CPI_I1K:
+ if (record->event.pressed) {
+ add_cpi(10);
+ }
+ break;
+ case CPI_D1K:
+ if (record->event.pressed) {
+ add_cpi(-10);
+ }
+ break;
+
+ case SCRL_TO:
+ if (record->event.pressed) {
+ keyball.scroll_mode = !keyball.scroll_mode;
+ }
+ break;
+ case SCRL_MO:
+ keyball.scroll_mode = record->event.pressed;
+ break;
+ case SCRL_DVI:
+ if (record->event.pressed) {
+ add_scroll_div(1);
+ }
+ break;
+ case SCRL_DVD:
+ if (record->event.pressed) {
+ add_scroll_div(-1);
+ }
+ break;
+
+ default:
+ return true;
+ }
+ return false;
+}
diff --git a/qmk_firmware/keyboards/keyball/lib/keyball/keyball.h b/qmk_firmware/keyboards/keyball/lib/keyball/keyball.h
new file mode 100644
index 000000000..bf3479f08
--- /dev/null
+++ b/qmk_firmware/keyboards/keyball/lib/keyball/keyball.h
@@ -0,0 +1,135 @@
+/*
+Copyright 2022 MURAOKA Taro (aka KoRoN, @kaoriya)
+
+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 .
+*/
+
+#pragma once
+
+//////////////////////////////////////////////////////////////////////////////
+
+#ifndef KEYBALL_CPI_DEFAULT
+# define KEYBALL_CPI_DEFAULT 500
+#endif
+
+#ifndef KEYBALL_SCROLL_DIV_DEFAULT
+# define KEYBALL_SCROLL_DIV_DEFAULT 4 // 4: 1/8 (1/2^(n-1))
+#endif
+
+//////////////////////////////////////////////////////////////////////////////
+
+#define KEYBALL_TX_GETINFO_INTERVAL 500
+#define KEYBALL_TX_GETINFO_MAXTRY 10
+#define KEYBALL_TX_GETMOTION_INTERVAL 5
+
+//////////////////////////////////////////////////////////////////////////////
+
+enum keyball_keycodes {
+ KBC_RST = SAFE_RANGE, // Keyball configuration: reset to default
+ KBC_SAVE, // Keyball configuration: save to EEPROM
+
+ CPI_I100, // CPI +100 CPI
+ CPI_D100, // CPI -100 CPI
+ CPI_I1K, // CPI +1000 CPI
+ CPI_D1K, // CPI -1000 CPI
+
+ // In scroll mode, motion from primary trackball is treated as scroll
+ // wheel.
+ SCRL_TO, // Toggle scroll mode
+ SCRL_MO, // Momentary scroll mode
+ SCRL_DVI, // Increment scroll divider
+ SCRL_DVD, // Decrement scroll divider
+
+ KEYBALL_SAFE_RANGE,
+};
+
+typedef union {
+ uint32_t raw;
+ struct {
+ uint8_t cpi : 7;
+ uint8_t sdiv : 3; // scroll divider
+ };
+} keyball_config_t;
+
+typedef struct {
+ uint16_t vid;
+ uint16_t pid;
+ uint8_t ballcnt; // count of balls: support only 0 or 1, for now
+} keyball_info_t;
+
+typedef uint8_t keyball_motion_id_t;
+
+typedef struct {
+ int16_t x;
+ int16_t y;
+} keyball_motion_t;
+
+typedef uint8_t keyball_cpi_t;
+
+typedef struct {
+ bool this_have_ball;
+ bool that_enable;
+ bool that_have_ball;
+
+ keyball_motion_t this_motion;
+ keyball_motion_t that_motion;
+
+ uint8_t cpi_value;
+ bool cpi_changed;
+
+ bool scroll_mode;
+ uint8_t scroll_div;
+
+ uint16_t last_kc;
+ keypos_t last_pos;
+ report_mouse_t last_mouse;
+} keyball_t;
+
+typedef enum {
+ KEYBALL_ADJUST_PENDING = 0,
+ KEYBALL_ADJUST_PRIMARY = 1,
+ KEYBALL_ADJUST_SECONDARY = 2,
+} keyball_adjust_t;
+
+//////////////////////////////////////////////////////////////////////////////
+
+extern keyball_t keyball;
+
+//////////////////////////////////////////////////////////////////////////////
+
+// keyball_oled_render_ballinfo renders ball information to OLED.
+// It uses just 21 columns to show the info.
+void keyball_oled_render_ballinfo(void);
+
+// keyball_oled_render_keyinfo renders last processed key information to OLED.
+// It shows column, row, key code, and key name (if available).
+void keyball_oled_render_keyinfo(void);
+
+// TODO: document
+bool keyball_get_scroll_mode(void);
+
+// TODO: document
+void keyball_set_scroll_mode(bool mode);
+
+// TODO: document
+uint8_t keyball_get_scroll_div(void);
+
+// TODO: document
+void keyball_set_scroll_div(uint8_t div);
+
+// TODO: document
+uint8_t keyball_get_cpi(void);
+
+// TODO: document
+void keyball_set_cpi(uint8_t cpi);