feat(trackpad): first Windows Precision inplementation

This commit is contained in:
Florian Didron
2025-12-10 11:16:13 +07:00
parent c2cd9f4ff5
commit 378fc09e35
17 changed files with 1327 additions and 1316 deletions

View File

@@ -163,6 +163,11 @@ ifeq ($(strip $(POINTING_DEVICE_ENABLE)), yes)
else ifeq ($(strip $(POINTING_DEVICE_DRIVER)), navigator_trackpad)
I2C_DRIVER_REQUIRED = yes
SRC += drivers/sensors/navigator.c
SRC += drivers/sensors/navigator_trackpad_common.c
SRC += drivers/sensors/navigator_trackpad.c
SRC += drivers/sensors/navigator_trackpad_mouse.c
# Define PTP mode for mouse mode (uses absolute coordinates internally)
OPT_DEFS += -DNAVIGATOR_TRACKPAD_PTP_MODE
else ifneq ($(filter $(strip $(POINTING_DEVICE_DRIVER)),pmw3360 pmw3389),)
SPI_DRIVER_REQUIRED = yes
SRC += drivers/sensors/pmw33xx_common.c
@@ -171,9 +176,35 @@ ifeq ($(strip $(POINTING_DEVICE_ENABLE)), yes)
endif
PRECISION_TRACKPAD_ENABLE ?= no
# Valid precision trackpad driver types
VALID_PRECISION_TRACKPAD_DRIVER_TYPES := navigator_trackpad custom
ifeq ($(strip $(PRECISION_TRACKPAD_ENABLE)), yes)
OPT_DEFS += -DPRECISION_TRACKPAD_ENABLE
SRC += $(QUANTUM_DIR)/precision_trackpad.c
# Validate driver type
ifeq ($(filter $(PRECISION_TRACKPAD_DRIVER),$(VALID_PRECISION_TRACKPAD_DRIVER_TYPES)),)
$(call CATASTROPHIC_ERROR,Invalid PRECISION_TRACKPAD_DRIVER,\
PRECISION_TRACKPAD_DRIVER="$(PRECISION_TRACKPAD_DRIVER)" is not a valid PTP driver)
else
OPT_DEFS += -DPRECISION_TRACKPAD_ENABLE
SRC += $(QUANTUM_DIR)/precision_trackpad.c
# Include driver source (unless custom)
ifneq ($(strip $(PRECISION_TRACKPAD_DRIVER)), custom)
# Add common code
SRC += drivers/sensors/navigator_trackpad_common.c
SRC += drivers/sensors/navigator_trackpad_ptp.c
SRC += drivers/sensors/navigator.c
I2C_DRIVER_REQUIRED = yes
# Define PTP mode for precision trackpad
OPT_DEFS += -DNAVIGATOR_TRACKPAD_PTP_MODE
# Set driver name macro (used by PRECISION_TRACKPAD_DRIVER macro)
OPT_DEFS += -DPRECISION_TRACKPAD_DRIVER_NAME=$(strip $(PRECISION_TRACKPAD_DRIVER))
OPT_DEFS += -DPRECISION_TRACKPAD_DRIVER_$(strip $(shell echo $(PRECISION_TRACKPAD_DRIVER) | tr '[:lower:]' '[:upper:]'))
endif
endif
endif
QUANTUM_PAINTER_ENABLE ?= no

View File

@@ -1,888 +1,14 @@
// Copyright 2025 ZSA Technology Labs, Inc <contact@zsa.io>
// SPDX-License-Identifier: GPL-2.0-or-later
// THIS IS A WORK IN PROGRESS, AS THE TRACKPAD IC's FIRMWARE IS STILL IN DEVELOPMENT
// DO NOT USE THIS CODE IN PRODUCTION
// Dispatcher/compatibility layer for Navigator trackpad
// Provides backward compatibility for existing code
#include <stdint.h>
#include <sys/types.h>
#include <math.h>
#include "navigator_trackpad.h"
#include "i2c_master.h"
#include "quantum.h"
#include "timer.h"
#include "navigator_trackpad_common.h"
#ifdef PROTOCOL_LUFA
# error "LUFA is not supported yet"
#endif
#ifdef POINTING_DEVICE_ENABLE
const pointing_device_driver_t navigator_trackpad_pointing_device_driver = {.init = navigator_trackpad_device_init, .get_report = navigator_trackpad_get_report, .get_cpi = navigator_trackpad_get_cpi, .set_cpi = navigator_trackpad_set_cpi};
#endif
deferred_token callback_token = 0;
uint16_t current_cpi = DEFAULT_CPI_TICK;
uint8_t has_motion = 0;
extern bool set_scrolling;
bool trackpad_init;
#ifdef NAVIGATOR_TRACKPAD_MACOS_SCROLLING
float macos_scroll_accumulated_h = 0;
float macos_scroll_accumulated_v = 0;
#endif
#if defined(NAVIGATOR_TRACKPAD_PTP_MODE)
cgen6_report_t ptp_report;
trackpad_gesture_t gesture = {0};
# ifdef NAVIGATOR_TRACKPAD_SCROLL_INERTIA_ENABLE
scroll_inertia_t scroll_inertia = {0};
# endif
#endif
// Helper functions to parse ptp reports
#if defined(NAVIGATOR_TRACKPAD_PTP_MODE)
uint8_t finger_count(cgen6_report_t *report) {
uint8_t fingers = 0;
if (report->fingers[0].tip) {
fingers++;
}
if (report->fingers[1].tip) {
fingers++;
}
return fingers;
}
#endif
i2c_status_t cirque_gen6_read_report(uint8_t *data, uint16_t cnt) {
i2c_status_t res = i2c_receive(NAVIGATOR_TRACKPAD_ADDRESS, data, cnt, NAVIGATOR_TRACKPAD_TIMEOUT);
if (res != I2C_STATUS_SUCCESS) {
return res;
}
wait_us(cnt * 15);
return res;
}
void cirque_gen6_clear(void) {
uint8_t buf[CGEN6_MAX_PACKET_SIZE];
for (uint8_t i = 0; i < 5; i++) {
wait_ms(1);
if (cirque_gen6_read_report(buf, CGEN6_MAX_PACKET_SIZE) != I2C_STATUS_SUCCESS) {
break;
}
}
}
uint8_t cirque_gen6_read_memory(uint32_t addr, uint8_t *data, uint16_t cnt, bool fast_read) {
uint8_t cksum = 0;
uint8_t res = CGEN6_SUCCESS;
uint8_t len[2];
uint16_t read = 0;
uint8_t preamble[8] = {0x01, 0x09, (uint8_t)(addr & (uint32_t)0x000000FF), (uint8_t)((addr & 0x0000FF00) >> 8), (uint8_t)((addr & 0x00FF0000) >> 16), (uint8_t)((addr & 0xFF000000) >> 24), (uint8_t)(cnt & 0x00FF), (uint8_t)((cnt & 0xFF00) >> 8)};
// Read the length of the data + 3 bytes (first 2 bytes for the length and the last byte for the checksum)
// Create a buffer to store the data
uint8_t buf[cnt + 3];
if (i2c_transmit_and_receive(NAVIGATOR_TRACKPAD_ADDRESS, preamble, 8, buf, cnt + 3, NAVIGATOR_TRACKPAD_TIMEOUT) != I2C_STATUS_SUCCESS) {
res |= CGEN6_I2C_FAILED;
trackpad_init = false;
}
// Read the data length
for (uint8_t i = 0; i < 2; i++) {
cksum += len[i] = buf[i];
read++;
}
// Populate the data buffer
for (uint16_t i = 2; i < cnt + 2; i++) {
cksum += data[i - 2] = buf[i];
read++;
}
if (!fast_read) {
// Check the checksum
if (cksum != buf[read]) {
res |= CGEN6_CKSUM_FAILED;
}
// Check the length (incremented first to account for the checksum)
if (++read != (len[0] | (len[1] << 8))) {
res |= CGEN6_LEN_MISMATCH;
}
wait_ms(1);
} else {
wait_us(250);
}
return res;
}
uint8_t cirque_gen6_write_memory(uint32_t addr, uint8_t *data, uint16_t cnt) {
uint8_t res = CGEN6_SUCCESS;
uint8_t cksum = 0, i = 0;
uint8_t preamble[8] = {0x00, 0x09, (uint8_t)(addr & 0x000000FF), (uint8_t)((addr & 0x0000FF00) >> 8), (uint8_t)((addr & 0x00FF0000) >> 16), (uint8_t)((addr & 0xFF000000) >> 24), (uint8_t)(cnt & 0x00FF), (uint8_t)((cnt & 0xFF00) >> 8)};
uint8_t buf[cnt + 9];
// Calculate the checksum
for (; i < 8; i++) {
cksum += buf[i] = preamble[i];
}
for (i = 0; i < cnt; i++) {
cksum += buf[i + 8] = data[i];
}
buf[cnt + 8] = cksum;
if (i2c_transmit(NAVIGATOR_TRACKPAD_ADDRESS, buf, cnt + 9, NAVIGATOR_TRACKPAD_TIMEOUT) != I2C_STATUS_SUCCESS) {
res |= CGEN6_I2C_FAILED;
trackpad_init = false;
}
wait_ms(1);
return res;
}
uint8_t cirque_gen6_read_reg(uint32_t addr, bool fast_read) {
uint8_t data;
uint8_t res = cirque_gen6_read_memory(addr, &data, 1, fast_read);
if (res != CGEN6_SUCCESS) {
printf("Failed to read 8bits from register at address 0x%08X with error 0x%02X\n", (u_int)addr, res);
return 0;
}
return data;
}
uint16_t cirque_gen6_read_reg_16(uint32_t addr) {
uint8_t buf[2];
uint8_t res = cirque_gen6_read_memory(addr, buf, 2, false);
if (res != CGEN6_SUCCESS) {
printf("Failed to read 16bits from register at address 0x%08X with error 0x%02X\n", (u_int)addr, res);
return 0;
}
return (buf[1] << 8) | buf[0];
}
uint32_t cirque_gen6_read_reg_32(uint32_t addr) {
uint8_t buf[4];
uint8_t res = cirque_gen6_read_memory(addr, buf, 4, false);
if (res != CGEN6_SUCCESS) {
printf("Failed to read 32bits from register at address 0x%08X with error 0x%02X\n", (u_int)addr, res);
return 0;
}
return (buf[3] << 24) | (buf[2] << 16) | (buf[1] << 8) | buf[0];
}
uint8_t cirque_gen6_write_reg(uint32_t addr, uint8_t data) {
return cirque_gen6_write_memory(addr, &data, 1);
}
uint8_t cirque_gen6_write_reg_16(uint32_t addr, uint16_t data) {
uint8_t buf[2] = {data & 0xFF, (data >> 8) & 0xFF};
return cirque_gen6_write_memory(addr, buf, 2);
}
uint8_t cirque_gen6_write_reg_32(uint32_t addr, uint32_t data) {
uint8_t buf[4] = {data & 0xFF, (data >> 8) & 0xFF, (data >> 16) & 0xFF, (data >> 24) & 0xFF};
return cirque_gen6_write_memory(addr, buf, 4);
}
uint8_t cirque_gen6_set_relative_mode(void) {
uint8_t feed_config4 = cirque_gen6_read_reg(CGEN6_FEED_CONFIG4, false);
feed_config4 &= 0xF3;
return cirque_gen6_write_reg(CGEN6_FEED_CONFIG4, feed_config4);
}
uint8_t cirque_gen6_set_ptp_mode(void) {
uint8_t feed_config4 = cirque_gen6_read_reg(CGEN6_FEED_CONFIG4, false);
feed_config4 &= 0xF7;
feed_config4 |= 0x04;
return cirque_gen6_write_reg(CGEN6_FEED_CONFIG4, feed_config4);
}
uint8_t cirque_gen6_swap_xy(bool set) {
uint8_t xy_config = cirque_gen6_read_reg(CGEN6_XY_CONFIG, false);
if (set) {
xy_config |= 0x04;
} else {
xy_config &= ~0x04;
}
return cirque_gen6_write_reg(CGEN6_XY_CONFIG, xy_config);
}
uint8_t cirque_gen6_invert_y(bool set) {
uint8_t xy_config = cirque_gen6_read_reg(CGEN6_XY_CONFIG, false);
if (set) {
xy_config |= 0x02;
} else {
xy_config &= ~0x02;
}
return cirque_gen6_write_reg(CGEN6_XY_CONFIG, xy_config);
}
uint8_t cirque_gen6_invert_x(bool set) {
uint8_t xy_config = cirque_gen6_read_reg(CGEN6_XY_CONFIG, false);
if (set) {
xy_config |= 0x01;
} else {
xy_config &= ~0x01;
}
return cirque_gen6_write_reg(CGEN6_XY_CONFIG, xy_config);
}
uint8_t cirque_gen6_enable_logical_scaling(bool set) {
uint8_t xy_config = cirque_gen6_read_reg(CGEN6_XY_CONFIG, false);
if (set) {
xy_config &= ~0x08;
} else {
xy_config |= 0x08;
}
return cirque_gen6_write_reg(CGEN6_XY_CONFIG, xy_config);
}
void cirque_gen_6_read_report(void) {
uint8_t packet[CGEN6_MAX_PACKET_SIZE];
if (cirque_gen6_read_report(packet, CGEN6_MAX_PACKET_SIZE) != I2C_STATUS_SUCCESS) {
return;
}
uint8_t report_id = packet[2];
#ifdef CONSOLE_ENABLE
static uint16_t report_id_counter = 0;
if (++report_id_counter % 100 == 0) {
printf("Cirque report ID: 0x%02x (expecting 0x01 for PTP)\n", report_id);
}
#endif
#if defined(NAVIGATOR_TRACKPAD_PTP_MODE)
if (report_id == CGEN6_PTP_REPORT_ID) {
ptp_report.fingers[0].tip = (packet[3] & 0x02) >> 1;
ptp_report.fingers[0].x = packet[5] << 8 | packet[4];
ptp_report.fingers[0].y = packet[7] << 8 | packet[6];
ptp_report.fingers[1].tip = (packet[8] & 0x02) >> 1;
ptp_report.fingers[1].x = packet[10] << 8 | packet[9];
ptp_report.fingers[1].y = packet[12] << 8 | packet[11];
ptp_report.buttons = packet[16];
}
#endif
#if defined(NAVIGATOR_TRACKPAD_RELATIVE_MODE)
if (report_id == CGEN6_MOUSE_REPORT_ID) {
ptp_report.buttons = packet[3];
ptp_report.xDelta = packet[4];
ptp_report.yDelta = packet[5];
ptp_report.scrollDelta = packet[6];
ptp_report.panDelta = packet[7];
has_motion = 1;
}
#endif
}
// Check if the DR pin is asserted, if it is there is motion data to sample.
uint8_t cirque_gen6_has_motion(void) {
return cirque_gen6_read_reg(CGEN6_I2C_DR, true);
}
uint32_t cirque_gen6_read_callback(uint32_t trigger_time, void *cb_arg) {
if (!trackpad_init) {
navigator_trackpad_device_init();
return NAVIGATOR_TRACKPAD_PROBE;
}
if (cirque_gen6_has_motion()) {
has_motion = 1;
cirque_gen_6_read_report();
}
return NAVIGATOR_TRACKPAD_READ;
}
void navigator_trackpad_device_init(void) {
i2c_init();
i2c_status_t status = i2c_ping_address(NAVIGATOR_TRACKPAD_ADDRESS, NAVIGATOR_TRACKPAD_TIMEOUT);
if (status != I2C_STATUS_SUCCESS) {
trackpad_init = false;
return;
}
cirque_gen6_clear();
wait_ms(50);
uint8_t res = CGEN6_SUCCESS;
#if defined(NAVIGATOR_TRACKPAD_PTP_MODE)
res = cirque_gen6_set_ptp_mode();
#elif defined(NAVIGATOR_TRACKPAD_RELATIVE_MODE)
res = cirque_gen6_set_relative_mode();
#endif
if (res != CGEN6_SUCCESS) {
return;
}
// Reset to the default alignment
cirque_gen6_swap_xy(false);
cirque_gen6_invert_x(false);
cirque_gen6_invert_y(false);
cirque_gen6_swap_xy(true);
cirque_gen6_invert_x(true);
cirque_gen6_invert_y(true);
cirque_gen6_enable_logical_scaling(true);
trackpad_init = true;
// Only register the callback for the first time
if (!callback_token) {
callback_token = defer_exec(NAVIGATOR_TRACKPAD_READ, cirque_gen6_read_callback, NULL);
}
}
#ifdef POINTING_DEVICE_ENABLE
report_mouse_t navigator_trackpad_get_report(report_mouse_t mouse_report) {
#ifdef PRECISION_TRACKPAD_ENABLE
// When PTP is enabled, don't send mouse reports - PTP task handles everything
return mouse_report;
#include "navigator_trackpad_ptp.h"
#else
// Mouse mode - process gestures and send mouse reports
#if defined(NAVIGATOR_TRACKPAD_PTP_MODE)
// Handle pending click release from previous cycle
if (gesture.pending_click) {
gesture.pending_click = false;
mouse_report.buttons = 0;
return mouse_report;
}
# ifdef NAVIGATOR_TRACKPAD_SCROLL_INERTIA_ENABLE
// Process scroll inertia when active
if (scroll_inertia.active && timer_elapsed(scroll_inertia.timer) >= NAVIGATOR_TRACKPAD_SCROLL_INERTIA_INTERVAL) {
scroll_inertia.timer = timer_read();
// Apply friction to velocity (Q8 fixed point math)
// Friction reduces velocity towards zero
int16_t friction_x = (scroll_inertia.vx * NAVIGATOR_TRACKPAD_SCROLL_INERTIA_FRICTION) / 256;
int16_t friction_y = (scroll_inertia.vy * NAVIGATOR_TRACKPAD_SCROLL_INERTIA_FRICTION) / 256;
// Ensure we always reduce by at least 1 if not zero
if (scroll_inertia.vx > 0 && friction_x < 1) friction_x = 1;
if (scroll_inertia.vx < 0 && friction_x > -1) friction_x = -1;
if (scroll_inertia.vy > 0 && friction_y < 1) friction_y = 1;
if (scroll_inertia.vy < 0 && friction_y > -1) friction_y = -1;
scroll_inertia.vx -= friction_x;
scroll_inertia.vy -= friction_y;
// Convert Q8 velocity to scroll value
# ifdef NAVIGATOR_TRACKPAD_MACOS_SCROLLING
// macOS mode: send raw velocity deltas (descriptor tells macOS the resolution)
int16_t scroll_x = scroll_inertia.vx / 256;
int16_t scroll_y = scroll_inertia.vy / 256;
# else
// Hi-res mode: apply multiplier for Windows/Linux
int16_t scroll_x = (scroll_inertia.vx * NAVIGATOR_TRACKPAD_SCROLL_MULTIPLIER) / 256;
int16_t scroll_y = (scroll_inertia.vy * NAVIGATOR_TRACKPAD_SCROLL_MULTIPLIER) / 256;
# endif
// Clamp to int8_t range
scroll_x = (scroll_x > 127) ? 127 : ((scroll_x < -127) ? -127 : scroll_x);
scroll_y = (scroll_y > 127) ? 127 : ((scroll_y < -127) ? -127 : scroll_y);
// Check if velocity is too low to continue
int16_t abs_vx = scroll_inertia.vx < 0 ? -scroll_inertia.vx : scroll_inertia.vx;
int16_t abs_vy = scroll_inertia.vy < 0 ? -scroll_inertia.vy : scroll_inertia.vy;
# ifdef NAVIGATOR_TRACKPAD_MACOS_SCROLLING
// In macOS mode, track consecutive frames with no output to detect janky tail
if (scroll_x == 0 && scroll_y == 0) {
scroll_inertia.no_output_count++;
} else {
scroll_inertia.no_output_count = 0;
}
// Stop if: velocity very low OR too many consecutive frames without output
bool velocity_too_low = (abs_vx < 64 && abs_vy < 64); // Same as hi-res mode
bool stalled = (scroll_inertia.no_output_count > 5); // 5 frames (~35ms) with no output
if (velocity_too_low || stalled) {
scroll_inertia.active = false;
} else
# else
if (abs_vx < 64 && abs_vy < 64) { // Threshold in Q8 (0.25 in real units)
scroll_inertia.active = false;
} else
# endif
{
// Apply scroll inversion if configured
# ifdef NAVIGATOR_SCROLL_INVERT_X
mouse_report.h = -scroll_x;
# else
mouse_report.h = scroll_x;
# endif
# ifdef NAVIGATOR_SCROLL_INVERT_Y
mouse_report.v = scroll_y;
# else
mouse_report.v = -scroll_y;
# endif
return mouse_report;
}
}
# endif
#endif // End scroll inertia block
#if defined(NAVIGATOR_TRACKPAD_RELATIVE_MODE)
if (!has_motion || !trackpad_init) {
return mouse_report;
}
mouse_report.x = ptp_report.xDelta;
mouse_report.y = ptp_report.yDelta;
mouse_report.v = ptp_report.scrollDelta;
mouse_report.h = ptp_report.panDelta;
mouse_report.buttons = ptp_report.buttons;
#elif defined(NAVIGATOR_TRACKPAD_PTP_MODE)
if (!has_motion || !trackpad_init) {
return mouse_report;
}
// Create local snapshot to avoid race condition with callback updating ptp_report
cgen6_report_t local_report = ptp_report;
uint8_t raw_fingers = finger_count(&local_report);
bool is_touching = local_report.fingers[0].tip;
bool was_idle = (gesture.state == TP_IDLE);
uint8_t fingers = raw_fingers;
// Handle finger down - record start position (regardless of current state)
if (is_touching && was_idle) {
gesture.touch_start_time = timer_read();
gesture.prev_x = local_report.fingers[0].x;
gesture.prev_y = local_report.fingers[0].y;
gesture.settled_x = 0; // Will be set after settle time
gesture.settled_y = 0;
gesture.settled = false;
gesture.max_finger_count = fingers;
gesture.state = TP_MOVING;
# ifdef NAVIGATOR_TRACKPAD_SCROLL_INERTIA_ENABLE
// Stop any ongoing scroll inertia when finger touches
scroll_inertia.active = false;
scroll_inertia.smooth_vx = 0;
scroll_inertia.smooth_vy = 0;
# ifdef NAVIGATOR_TRACKPAD_MACOS_SCROLLING
scroll_inertia.no_output_count = 0;
# endif
# endif
# ifdef NAVIGATOR_TRACKPAD_MACOS_SCROLLING
// Reset macOS scroll accumulator when starting new gesture
macos_scroll_accumulated_h = 0;
macos_scroll_accumulated_v = 0;
# endif
}
// Handle finger up - evaluate tap at lift time (libinput style)
if (!is_touching && !was_idle) {
uint16_t duration = timer_elapsed(gesture.touch_start_time);
// Calculate distance from settled position (or treat as no movement if never settled)
int32_t dist_sq = 0;
if (gesture.settled) {
int16_t dx = gesture.prev_x - gesture.settled_x;
int16_t dy = gesture.prev_y - gesture.settled_y;
dist_sq = (int32_t)dx * dx + (int32_t)dy * dy;
}
// Check tap conditions: short duration AND small movement from settled position
bool is_tap = (duration <= NAVIGATOR_TRACKPAD_TAP_TIMEOUT) &&
(dist_sq <= NAVIGATOR_TRACKPAD_TAP_MOVE_THRESHOLD);
// Don't trigger single-finger taps that happen shortly after a scroll ends (within 100ms)
// But allow two-finger taps (right-click) even after scrolling
# ifdef NAVIGATOR_TRACKPAD_SCROLL_WITH_TWO_FINGERS
if (is_tap && gesture.max_finger_count == 1 &&
timer_elapsed(gesture.last_scroll_end) < 100) {
is_tap = false; // Suppress single-finger tap after scrolling
}
# endif
if (is_tap) {
# ifdef NAVIGATOR_TRACKPAD_ENABLE_DOUBLE_TAP
if (gesture.max_finger_count >= 2) {
mouse_report.x = 0;
mouse_report.y = 0;
mouse_report.buttons = pointing_device_handle_buttons(mouse_report.buttons, true, POINTING_DEVICE_BUTTON2);
gesture.pending_click = true;
} else
# endif
# ifdef NAVIGATOR_TRACKPAD_ENABLE_TAP
if (gesture.max_finger_count == 1) {
mouse_report.x = 0;
mouse_report.y = 0;
mouse_report.buttons = pointing_device_handle_buttons(mouse_report.buttons, true, POINTING_DEVICE_BUTTON1);
gesture.pending_click = true;
}
# endif
}
# if defined(NAVIGATOR_TRACKPAD_SCROLL_INERTIA_ENABLE) && defined(NAVIGATOR_TRACKPAD_SCROLL_WITH_TWO_FINGERS)
// Start scroll inertia if we were scrolling and have enough velocity
if (gesture.state == TP_SCROLLING) {
// Use smoothed velocity (already in Q8 format)
int16_t abs_vx = scroll_inertia.smooth_vx < 0 ? -scroll_inertia.smooth_vx : scroll_inertia.smooth_vx;
int16_t abs_vy = scroll_inertia.smooth_vy < 0 ? -scroll_inertia.smooth_vy : scroll_inertia.smooth_vy;
// Trigger threshold is now in Q8 (multiply by 256)
if (abs_vx >= (NAVIGATOR_TRACKPAD_SCROLL_INERTIA_TRIGGER * 256) ||
abs_vy >= (NAVIGATOR_TRACKPAD_SCROLL_INERTIA_TRIGGER * 256)) {
scroll_inertia.vx = scroll_inertia.smooth_vx;
scroll_inertia.vy = scroll_inertia.smooth_vy;
scroll_inertia.timer = timer_read();
scroll_inertia.active = true;
}
}
# endif
// Record if this was a scroll gesture ending
# ifdef NAVIGATOR_TRACKPAD_SCROLL_WITH_TWO_FINGERS
if (gesture.state == TP_SCROLLING || gesture.max_finger_count >= 2) {
gesture.last_scroll_end = timer_read();
}
# endif
gesture.state = TP_IDLE;
set_scrolling = false;
}
// Handle ongoing touch - movement and scrolling
if (is_touching && !was_idle) {
// Track max fingers during gesture
if (fingers > gesture.max_finger_count) {
gesture.max_finger_count = fingers;
}
# ifdef NAVIGATOR_TRACKPAD_SCROLL_WITH_TWO_FINGERS
// Determine mode based on finger count
// Once scrolling starts, keep scrolling until all fingers lift (no mid-gesture transitions)
if (fingers >= 2 && gesture.state != TP_SCROLLING) {
gesture.state = TP_SCROLLING;
// Reset position tracking - use finger[0] initially
gesture.prev_x = local_report.fingers[0].x;
gesture.prev_y = local_report.fingers[0].y;
}
// Note: We don't transition from SCROLLING back to MOVING mid-gesture anymore
// Once scroll starts, it continues until all fingers lift (gesture ends)
# endif
uint16_t duration = timer_elapsed(gesture.touch_start_time);
// Record settled position once settle time elapses
if (!gesture.settled && duration >= NAVIGATOR_TRACKPAD_TAP_SETTLE_TIME) {
gesture.settled = true;
gesture.settled_x = local_report.fingers[0].x;
gesture.settled_y = local_report.fingers[0].y;
}
// Check if we should suppress movement (might still be a tap)
int32_t dist_sq = 0;
if (gesture.settled) {
int16_t dx = local_report.fingers[0].x - gesture.settled_x;
int16_t dy = local_report.fingers[0].y - gesture.settled_y;
dist_sq = (int32_t)dx * dx + (int32_t)dy * dy;
}
// Only report movement if: timeout exceeded OR moved beyond tap threshold (after settling)
bool should_move = (duration > NAVIGATOR_TRACKPAD_TAP_TIMEOUT) ||
(gesture.settled && dist_sq > NAVIGATOR_TRACKPAD_TAP_MOVE_THRESHOLD);
if (should_move) {
int16_t delta_x = local_report.fingers[0].x - gesture.prev_x;
int16_t delta_y = local_report.fingers[0].y - gesture.prev_y;
// Clamp deltas to prevent jumps from bad data
if (delta_x > NAVIGATOR_TRACKPAD_MAX_DELTA) delta_x = NAVIGATOR_TRACKPAD_MAX_DELTA;
if (delta_x < -NAVIGATOR_TRACKPAD_MAX_DELTA) delta_x = -NAVIGATOR_TRACKPAD_MAX_DELTA;
if (delta_y > NAVIGATOR_TRACKPAD_MAX_DELTA) delta_y = NAVIGATOR_TRACKPAD_MAX_DELTA;
if (delta_y < -NAVIGATOR_TRACKPAD_MAX_DELTA) delta_y = -NAVIGATOR_TRACKPAD_MAX_DELTA;
if (delta_x != 0 || delta_y != 0) {
# ifdef NAVIGATOR_TRACKPAD_SCROLL_WITH_TWO_FINGERS
if (gesture.state == TP_SCROLLING) {
# ifdef NAVIGATOR_TRACKPAD_MACOS_SCROLLING
// macOS mode: send raw deltas, macOS handles scaling via HIDScrollResolution
// Apple trackpads report raw sensor deltas and let macOS apply acceleration
int16_t scroll_x = delta_x;
int16_t scroll_y = delta_y;
# else
// Hi-res mode: apply multiplier for Windows/Linux
// These OSes divide by the Resolution Multiplier (120)
int16_t scroll_x = delta_x * NAVIGATOR_TRACKPAD_SCROLL_MULTIPLIER;
int16_t scroll_y = delta_y * NAVIGATOR_TRACKPAD_SCROLL_MULTIPLIER;
# endif
# ifdef NAVIGATOR_TRACKPAD_SCROLL_INERTIA_ENABLE
// Track velocity for inertia using exponential smoothing (Q8 fixed point)
// Alpha = 0.3 (77/256) - balances responsiveness with smoothness
// When direction changes, reset smoothing to avoid fighting old momentum
int16_t new_vx = delta_x * 256;
int16_t new_vy = delta_y * 256;
// Detect direction change (signs differ and both non-zero)
bool dir_change_x = (scroll_inertia.smooth_vx > 0 && new_vx < 0) ||
(scroll_inertia.smooth_vx < 0 && new_vx > 0);
bool dir_change_y = (scroll_inertia.smooth_vy > 0 && new_vy < 0) ||
(scroll_inertia.smooth_vy < 0 && new_vy > 0);
if (dir_change_x) {
// Direction changed - blend toward zero first, then pick up new direction
scroll_inertia.smooth_vx = (scroll_inertia.smooth_vx * 128 + new_vx * 128) / 256;
} else {
// Same direction - normal EMA smoothing
scroll_inertia.smooth_vx = (scroll_inertia.smooth_vx * 179 + new_vx * 77) / 256;
}
if (dir_change_y) {
scroll_inertia.smooth_vy = (scroll_inertia.smooth_vy * 128 + new_vy * 128) / 256;
} else {
scroll_inertia.smooth_vy = (scroll_inertia.smooth_vy * 179 + new_vy * 77) / 256;
}
# endif
// Clamp to int8_t range for the report
scroll_x = (scroll_x > 127) ? 127 : ((scroll_x < -127) ? -127 : scroll_x);
scroll_y = (scroll_y > 127) ? 127 : ((scroll_y < -127) ? -127 : scroll_y);
// Apply scroll inversion if configured
# ifdef NAVIGATOR_SCROLL_INVERT_X
mouse_report.h = -scroll_x;
# else
mouse_report.h = scroll_x;
# endif
# ifdef NAVIGATOR_SCROLL_INVERT_Y
mouse_report.v = scroll_y;
# else
mouse_report.v = -scroll_y;
# endif
mouse_report.x = 0;
mouse_report.y = 0;
} else
# endif
{
// One-finger movement: mouse cursor
mouse_report.x = (delta_x < 0) ? -powf(-delta_x, 1.2) : powf(delta_x, 1.2);
mouse_report.y = (delta_y < 0) ? -powf(-delta_y, 1.2) : powf(delta_y, 1.2);
}
}
}
// Update prev position for next frame
gesture.prev_x = local_report.fingers[0].x;
gesture.prev_y = local_report.fingers[0].y;
}
#endif
has_motion = 0;
return mouse_report;
#endif // PRECISION_TRACKPAD_ENABLE
}
#endif // POINTING_DEVICE_ENABLE
void set_cirque_cpi(void) {
// traverse the sequence by compairing the cpi_x value with the current cpi_x value
// set the cpi to the next value in the sequence
switch (current_cpi) {
case CPI_1: {
current_cpi = CPI_2;
break;
}
case CPI_2: {
current_cpi = CPI_3;
break;
}
case CPI_3: {
current_cpi = CPI_4;
break;
}
case CPI_4: {
current_cpi = CPI_5;
break;
}
case CPI_5: {
current_cpi = CPI_6;
break;
}
case CPI_6: {
current_cpi = CPI_7;
break;
}
case CPI_7: {
current_cpi = CPI_1;
break;
}
default: {
current_cpi = CPI_4;
break;
}
}
}
uint16_t navigator_trackpad_get_cpi(void) {
return current_cpi;
}
void restore_cpi(uint8_t cpi) {
current_cpi = cpi;
set_cirque_cpi();
}
void navigator_trackpad_set_cpi(uint16_t cpi) {
if (cpi == 0) { // Decrease one tick
if (current_cpi > 1) {
current_cpi--;
}
} else {
if (current_cpi < CPI_TICKS) {
current_cpi++;
}
}
set_cirque_cpi();
};
#ifdef PRECISION_TRACKPAD_ENABLE
// External declaration for PTP report sending
extern void send_trackpad(report_trackpad_t *report);
// PTP task function - converts trackpad touches to PTP reports
void navigator_trackpad_task(void) {
#if defined(NAVIGATOR_TRACKPAD_PTP_MODE)
if (!trackpad_init) {
return;
}
// Always check for data, even if has_motion isn't set
// Windows needs continuous updates for gestures
if (!has_motion && !cirque_gen6_has_motion()) {
return;
}
// Read fresh data if available
if (cirque_gen6_has_motion()) {
cirque_gen_6_read_report();
}
// Create local snapshot to avoid race condition with callback
cgen6_report_t local_report = ptp_report;
uint8_t raw_fingers = finger_count(&local_report);
#ifdef CONSOLE_ENABLE
// Track min/max coordinates to find actual sensor range
static uint16_t max_x = 0, max_y = 0;
static uint16_t min_x = 0xFFFF, min_y = 0xFFFF;
static uint16_t debug_counter = 0;
if (local_report.fingers[0].tip) {
if (local_report.fingers[0].x > max_x) max_x = local_report.fingers[0].x;
if (local_report.fingers[0].y > max_y) max_y = local_report.fingers[0].y;
if (local_report.fingers[0].x < min_x && local_report.fingers[0].x > 0) min_x = local_report.fingers[0].x;
if (local_report.fingers[0].y < min_y && local_report.fingers[0].y > 0) min_y = local_report.fingers[0].y;
}
if (local_report.fingers[1].tip) {
if (local_report.fingers[1].x > max_x) max_x = local_report.fingers[1].x;
if (local_report.fingers[1].y > max_y) max_y = local_report.fingers[1].y;
if (local_report.fingers[1].x < min_x && local_report.fingers[1].x > 0) min_x = local_report.fingers[1].x;
if (local_report.fingers[1].y < min_y && local_report.fingers[1].y > 0) min_y = local_report.fingers[1].y;
}
if (++debug_counter % 100 == 0) { // Print every 100 reports
printf("PTP: fingers=%d, range: x=%d-%d, y=%d-%d, current: f0(%d,%d) f1(%d,%d)\n",
raw_fingers, min_x, max_x, min_y, max_y,
local_report.fingers[0].x, local_report.fingers[0].y,
local_report.fingers[1].x, local_report.fingers[1].y);
}
#endif
// Initialize PTP report
report_trackpad_t ptp = {0};
ptp.report_id = REPORT_ID_TRACKPAD;
// Map Cirque coordinates (0-4095 in PTP mode) to PTP logical range (0-4095)
// Physical dimensions: 4 inches (0x190) x 2.75 inches (0x113)
// The Cirque sensor already provides 12-bit resolution
if (raw_fingers >= 1 && local_report.fingers[0].tip) {
ptp.contacts[0].confidence = 1; // Assume intentional touch
ptp.contacts[0].tip = 1;
ptp.contacts[0].contact_id = 0;
ptp.contacts[0].x = local_report.fingers[0].x; // Already 12-bit
ptp.contacts[0].y = local_report.fingers[0].y;
}
if (raw_fingers >= 2 && local_report.fingers[1].tip) {
ptp.contacts[1].confidence = 1;
ptp.contacts[1].tip = 1;
ptp.contacts[1].contact_id = 1;
ptp.contacts[1].x = local_report.fingers[1].x;
ptp.contacts[1].y = local_report.fingers[1].y;
}
// Set contact count
ptp.contact_count = raw_fingers;
#ifdef CONSOLE_ENABLE
if (raw_fingers > 2) {
printf("ERROR: raw_fingers=%d (should be 0-2!)\n", raw_fingers);
}
#endif
// Set scan time (in 100μs units)
// Use timer_read() which returns milliseconds, convert to 100μs units
ptp.scan_time = (timer_read() * 10) & 0xFFFF;
// Set button states (physical buttons from Cirque)
ptp.button1 = (local_report.buttons & 0x01) ? 1 : 0;
ptp.button2 = (local_report.buttons & 0x02) ? 1 : 0;
ptp.button3 = (local_report.buttons & 0x04) ? 1 : 0;
// Send the PTP report
#ifdef CONSOLE_ENABLE
// Log raw bytes being sent
static uint16_t byte_log_counter = 0;
if (++byte_log_counter % 50 == 0 && raw_fingers > 0) {
uint8_t *bytes = (uint8_t *)&ptp;
printf("Sending bytes: %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x\n",
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14]);
}
#endif
send_trackpad(&ptp);
#ifdef CONSOLE_ENABLE
// Detailed PTP report logging
static uint16_t report_counter = 0;
if (++report_counter % 50 == 0 || raw_fingers > 0) { // Log every 50 reports or when touching
printf("PTP Report: count=%d, scan=%d, btn=%d%d%d\n",
ptp.contact_count, ptp.scan_time, ptp.button1, ptp.button2, ptp.button3);
if (ptp.contacts[0].tip) {
printf(" C0: id=%d, conf=%d, tip=%d, x=%d, y=%d\n",
ptp.contacts[0].contact_id, ptp.contacts[0].confidence,
ptp.contacts[0].tip, ptp.contacts[0].x, ptp.contacts[0].y);
}
if (ptp.contacts[1].tip) {
printf(" C1: id=%d, conf=%d, tip=%d, x=%d, y=%d\n",
ptp.contacts[1].contact_id, ptp.contacts[1].confidence,
ptp.contacts[1].tip, ptp.contacts[1].x, ptp.contacts[1].y);
}
}
#endif
// Also send a report with no contacts when fingers lift
// This is critical for Windows to detect gesture end
static uint8_t prev_contact_count = 0;
if (raw_fingers == 0 && prev_contact_count > 0) {
// Send empty report to signal fingers lifted
report_trackpad_t empty_ptp = {0};
empty_ptp.report_id = REPORT_ID_TRACKPAD;
empty_ptp.scan_time = (timer_read() * 10) & 0xFFFF;
send_trackpad(&empty_ptp);
}
prev_contact_count = raw_fingers;
has_motion = 0;
#endif
}
#include "navigator_trackpad_mouse.h"
#endif

View File

@@ -2,211 +2,14 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <stdint.h>
#include <stdbool.h>
#include "report.h"
#ifdef POINTING_DEVICE_ENABLE
# include "pointing_device.h"
#endif
// Dispatcher/compatibility layer for Navigator trackpad
// Includes the appropriate implementation based on build configuration
#define NAVIGATOR_TRACKPAD_READ 7
#define NAVIGATOR_TRACKPAD_PROBE 1000
#ifndef NAVIGATOR_TRACKPAD_TAP_MOVE_THRESHOLD
# define NAVIGATOR_TRACKPAD_TAP_MOVE_THRESHOLD 100 // Max movement (squared) before tap becomes a drag
#endif
#ifndef NAVIGATOR_TRACKPAD_TAP_TIMEOUT
# define NAVIGATOR_TRACKPAD_TAP_TIMEOUT 200 // Max duration (ms) for a tap
#endif
#ifndef NAVIGATOR_TRACKPAD_TAP_SETTLE_TIME
# define NAVIGATOR_TRACKPAD_TAP_SETTLE_TIME 30 // Ignore movement during initial contact (ms)
#endif
#ifndef NAVIGATOR_TRACKPAD_MAX_DELTA
# define NAVIGATOR_TRACKPAD_MAX_DELTA 250 // Max allowed delta per frame to prevent jumps
#endif
#ifndef NAVIGATOR_TRACKPAD_ADDRESS
# define NAVIGATOR_TRACKPAD_ADDRESS 0x58
#endif
#ifndef NAVIGATOR_TRACKPAD_TIMEOUT
# define NAVIGATOR_TRACKPAD_TIMEOUT 100
#endif
#define NAVIGATOR_TRACKPAD_PTP_MODE
#if !defined(NAVIGATOR_TRACKPAD_RELATIVE_MODE) && !defined(NAVIGATOR_TRACKPAD_PTP_MODE)
# define NAVIGATOR_TRACKPAD_PTP_MODE
#endif
#define CGEN6_MAX_PACKET_SIZE 17
#define CGEN6_PTP_REPORT_ID 0x01
#define CGEN6_MOUSE_REPORT_ID 0x06
#define CGEN6_ABSOLUTE_REPORT_ID 0x09
// C3 error codes when reading memory
#define CGEN6_SUCCESS 0x00
#define CGEN6_CKSUM_FAILED 0x01
#define CGEN6_LEN_MISMATCH 0x02
#define CGEN6_I2C_FAILED 0x03
// C3 register addresses
#define CGEN6_REG_BASE 0x20000800
#define CGEN6_HARDWARE_ID CGEN6_REG_BASE + 0x08
#define CGEN6_FIRMWARE_ID CGEN6_REG_BASE + 0x09
#define CGEN6_FIRMWARE_REV CGEN6_REG_BASE + 0x10
#define CGEN6_VENDOR_ID CGEN6_REG_BASE + 0x0A
#define CGEN6_PRODUCT_ID CGEN6_REG_BASE + 0x0C
#define CGEN6_VERSION_ID CGEN6_REG_BASE + 0x0E
#define CGEN6_FEED_CONFIG4 0x200E000B
#define CGEN6_FEED_CONFIG3 0x200E000A
#define CGEN6_SYS_CONFIG1 0x20000008
#define CGEN6_XY_CONFIG 0x20080018
#define CGEN6_SFR_BASE 0x40000008
#define CGEN6_GPIO_BASE 0x00052000
#define CGEN6_I2C_DR 0x61010000
#define CPI_TICKS 7
#define DEFAULT_CPI_TICK 4
#define CPI_1 200
#define CPI_2 400
#define CPI_3 800
#define CPI_4 1024
#define CPI_5 1400
#define CPI_6 1800
#define CPI_7 2048
#ifndef NAVIGATOR_TRACKPAD_SCROLL_DIVIDER
# define NAVIGATOR_TRACKPAD_SCROLL_DIVIDER 10
#endif
#ifndef NAVIGATOR_TRACKPAD_SCROLL_MULTIPLIER
# define NAVIGATOR_TRACKPAD_SCROLL_MULTIPLIER 3
#endif
// Two-finger scrolling (define to enable)
// #define NAVIGATOR_TRACKPAD_SCROLL_WITH_TWO_FINGERS
// macOS scrolling mode (define to enable)
// macOS doesn't respect the HID Resolution Multiplier descriptor.
// When enabled, sends raw scroll deltas without the multiplier (like Apple trackpads).
// macOS applies its own HIDScrollResolution (400 DPI) and acceleration curves.
// When disabled (default), applies multiplier for Windows/Linux hi-res scrolling.
// #define NAVIGATOR_TRACKPAD_MACOS_SCROLLING
// Scroll inversion configuration
// Define these to invert scroll direction on respective axes
// #define NAVIGATOR_SCROLL_INVERT_X
// #define NAVIGATOR_SCROLL_INVERT_Y
// Scroll inertia configuration
/*
To enable, add to your config.h:
#define NAVIGATOR_TRACKPAD_SCROLL_INERTIA_ENABLE
Configurable values (all optional):
- NAVIGATOR_TRACKPAD_SCROLL_INERTIA_FRICTION - Higher = stops faster (default: 50, range 1-255)
- NAVIGATOR_TRACKPAD_SCROLL_INERTIA_INTERVAL - Time between glide reports in ms (default: 15)
- NAVIGATOR_TRACKPAD_SCROLL_INERTIA_TRIGGER - Minimum velocity to trigger glide (default: 3)
*/
#ifdef NAVIGATOR_TRACKPAD_SCROLL_INERTIA_ENABLE
#ifndef NAVIGATOR_TRACKPAD_SCROLL_INERTIA_FRICTION
#define NAVIGATOR_TRACKPAD_SCROLL_INERTIA_FRICTION 5 // Higher = stops faster (1-255)
#endif
#ifndef NAVIGATOR_TRACKPAD_SCROLL_INERTIA_INTERVAL
#define NAVIGATOR_TRACKPAD_SCROLL_INERTIA_INTERVAL 7 // Glide report interval in ms
#endif
#ifndef NAVIGATOR_TRACKPAD_SCROLL_INERTIA_TRIGGER
#define NAVIGATOR_TRACKPAD_SCROLL_INERTIA_TRIGGER 1 // Min velocity to trigger glide
#endif
typedef struct {
int16_t vx; // Current X velocity (Q8 fixed point)
int16_t vy; // Current Y velocity (Q8 fixed point)
int16_t smooth_vx; // Smoothed X velocity (Q8 fixed point)
int16_t smooth_vy; // Smoothed Y velocity (Q8 fixed point)
uint16_t timer; // Timer for interval tracking
bool active; // Is glide currently active
#ifdef NAVIGATOR_TRACKPAD_MACOS_SCROLLING
uint8_t no_output_count; // Counter for consecutive frames with no output
#endif
} scroll_inertia_t;
#endif
#if defined(NAVIGATOR_TRACKPAD_PTP_MODE)
# ifndef MOUSE_EXTENDED_REPORT
# define MOUSE_EXTENDED_REPORT
# endif
typedef struct {
uint8_t tip;
uint16_t x;
uint16_t y;
} cgen6_finger_t;
typedef struct {
cgen6_finger_t fingers[2];
uint8_t buttons;
} cgen6_report_t;
// Trackpad gesture state machine
typedef enum {
TP_IDLE, // No fingers touching
TP_MOVING, // One finger movement = mouse cursor
TP_SCROLLING, // Two finger movement = scroll
} trackpad_state_t;
typedef struct {
trackpad_state_t state;
uint16_t touch_start_time; // When finger first touched
uint16_t settled_x; // Position after settle time (for tap threshold)
uint16_t settled_y;
uint16_t prev_x; // Previous position (for delta calculation)
uint16_t prev_y;
uint8_t max_finger_count; // Max fingers seen during this gesture
bool settled; // Has the settle time elapsed?
bool pending_click; // Need to send a click release next cycle
uint16_t last_scroll_end; // Time when last scroll gesture ended
} trackpad_gesture_t;
#endif
#if defined(NAVIGATOR_TRACKPAD_ABSOLUTE_MODE)
typedef struct {
uint16_t x;
uint16_t y;
uint8_t palm;
uint8_t z;
} finger_data_t;
typedef struct {
finger_data_t fingers[3]; // Cirque support 5 fingers, we only need 3 for our application
uint8_t contact_flags;
uint8_t buttons;
} cgen6_report_t;
#endif
#if defined(NAVIGATOR_TRACKPAD_RELATIVE_MODE)
typedef struct {
uint8_t buttons;
int8_t xDelta;
int8_t yDelta;
int8_t scrollDelta;
int8_t panDelta;
} cgen6_report_t;
#endif
#ifdef POINTING_DEVICE_ENABLE
const pointing_device_driver_t navigator_trackpad_pointing_device_driver;
report_mouse_t navigator_trackpad_get_report(report_mouse_t mouse_report);
#endif
void navigator_trackpad_device_init(void);
uint16_t navigator_trackpad_get_cpi(void);
void navigator_trackpad_set_cpi(uint16_t cpi);
void restore_cpi(uint8_t cpi);
#include "navigator_trackpad_common.h"
#ifdef PRECISION_TRACKPAD_ENABLE
void navigator_trackpad_task(void);
#include "navigator_trackpad_ptp.h"
#else
#include "navigator_trackpad_mouse.h"
#endif

View File

@@ -0,0 +1,373 @@
// Copyright 2025 ZSA Technology Labs, Inc <contact@zsa.io>
// SPDX-License-Identifier: GPL-2.0-or-later
// Common hardware layer for Navigator trackpad
// Shared by both mouse and PTP implementations
#include <stdint.h>
#include <sys/types.h>
#include "navigator_trackpad_common.h"
#include "i2c_master.h"
#include "quantum.h"
#include "timer.h"
// Shared globals
deferred_token callback_token = 0;
uint16_t current_cpi = DEFAULT_CPI_TICK;
uint8_t has_motion = 0;
bool trackpad_init = false;
cgen6_report_t ptp_report;
// I2C communication functions
i2c_status_t cirque_gen6_read_report(uint8_t *data, uint16_t cnt) {
i2c_status_t res = i2c_receive(NAVIGATOR_TRACKPAD_ADDRESS, data, cnt, NAVIGATOR_TRACKPAD_TIMEOUT);
if (res != I2C_STATUS_SUCCESS) {
return res;
}
wait_us(cnt * 15);
return res;
}
void cirque_gen6_clear(void) {
uint8_t buf[CGEN6_MAX_PACKET_SIZE];
for (uint8_t i = 0; i < 5; i++) {
wait_ms(1);
if (cirque_gen6_read_report(buf, CGEN6_MAX_PACKET_SIZE) != I2C_STATUS_SUCCESS) {
break;
}
}
}
uint8_t cirque_gen6_read_memory(uint32_t addr, uint8_t *data, uint16_t cnt, bool fast_read) {
uint8_t cksum = 0;
uint8_t res = CGEN6_SUCCESS;
uint8_t len[2];
uint16_t read = 0;
uint8_t preamble[8] = {0x01, 0x09, (uint8_t)(addr & (uint32_t)0x000000FF), (uint8_t)((addr & 0x0000FF00) >> 8), (uint8_t)((addr & 0x00FF0000) >> 16), (uint8_t)((addr & 0xFF000000) >> 24), (uint8_t)(cnt & 0x00FF), (uint8_t)((cnt & 0xFF00) >> 8)};
// Read the length of the data + 3 bytes (first 2 bytes for the length and the last byte for the checksum)
// Create a buffer to store the data
uint8_t buf[cnt + 3];
if (i2c_transmit_and_receive(NAVIGATOR_TRACKPAD_ADDRESS, preamble, 8, buf, cnt + 3, NAVIGATOR_TRACKPAD_TIMEOUT) != I2C_STATUS_SUCCESS) {
res |= CGEN6_I2C_FAILED;
trackpad_init = false;
}
// Read the data length
for (uint8_t i = 0; i < 2; i++) {
cksum += len[i] = buf[i];
read++;
}
// Populate the data buffer
for (uint16_t i = 2; i < cnt + 2; i++) {
cksum += data[i - 2] = buf[i];
read++;
}
if (!fast_read) {
// Check the checksum
if (cksum != buf[read]) {
res |= CGEN6_CKSUM_FAILED;
}
// Check the length (incremented first to account for the checksum)
if (++read != (len[0] | (len[1] << 8))) {
res |= CGEN6_LEN_MISMATCH;
}
wait_ms(1);
} else {
wait_us(250);
}
return res;
}
uint8_t cirque_gen6_write_memory(uint32_t addr, uint8_t *data, uint16_t cnt) {
uint8_t res = CGEN6_SUCCESS;
uint8_t cksum = 0, i = 0;
uint8_t preamble[8] = {0x00, 0x09, (uint8_t)(addr & 0x000000FF), (uint8_t)((addr & 0x0000FF00) >> 8), (uint8_t)((addr & 0x00FF0000) >> 16), (uint8_t)((addr & 0xFF000000) >> 24), (uint8_t)(cnt & 0x00FF), (uint8_t)((cnt & 0xFF00) >> 8)};
uint8_t buf[cnt + 9];
// Calculate the checksum
for (; i < 8; i++) {
cksum += buf[i] = preamble[i];
}
for (i = 0; i < cnt; i++) {
cksum += buf[i + 8] = data[i];
}
buf[cnt + 8] = cksum;
if (i2c_transmit(NAVIGATOR_TRACKPAD_ADDRESS, buf, cnt + 9, NAVIGATOR_TRACKPAD_TIMEOUT) != I2C_STATUS_SUCCESS) {
res |= CGEN6_I2C_FAILED;
trackpad_init = false;
}
wait_ms(1);
return res;
}
// Register access functions
uint8_t cirque_gen6_read_reg(uint32_t addr, bool fast_read) {
uint8_t data;
uint8_t res = cirque_gen6_read_memory(addr, &data, 1, fast_read);
if (res != CGEN6_SUCCESS) {
printf("Failed to read 8bits from register at address 0x%08X with error 0x%02X\n", (u_int)addr, res);
return 0;
}
return data;
}
uint16_t cirque_gen6_read_reg_16(uint32_t addr) {
uint8_t buf[2];
uint8_t res = cirque_gen6_read_memory(addr, buf, 2, false);
if (res != CGEN6_SUCCESS) {
printf("Failed to read 16bits from register at address 0x%08X with error 0x%02X\n", (u_int)addr, res);
return 0;
}
return (buf[1] << 8) | buf[0];
}
uint32_t cirque_gen6_read_reg_32(uint32_t addr) {
uint8_t buf[4];
uint8_t res = cirque_gen6_read_memory(addr, buf, 4, false);
if (res != CGEN6_SUCCESS) {
printf("Failed to read 32bits from register at address 0x%08X with error 0x%02X\n", (u_int)addr, res);
return 0;
}
return (buf[3] << 24) | (buf[2] << 16) | (buf[1] << 8) | buf[0];
}
uint8_t cirque_gen6_write_reg(uint32_t addr, uint8_t data) {
return cirque_gen6_write_memory(addr, &data, 1);
}
uint8_t cirque_gen6_write_reg_16(uint32_t addr, uint16_t data) {
uint8_t buf[2] = {data & 0xFF, (data >> 8) & 0xFF};
return cirque_gen6_write_memory(addr, buf, 2);
}
uint8_t cirque_gen6_write_reg_32(uint32_t addr, uint32_t data) {
uint8_t buf[4] = {data & 0xFF, (data >> 8) & 0xFF, (data >> 16) & 0xFF, (data >> 24) & 0xFF};
return cirque_gen6_write_memory(addr, buf, 4);
}
// Configuration functions
uint8_t cirque_gen6_set_relative_mode(void) {
uint8_t feed_config4 = cirque_gen6_read_reg(CGEN6_FEED_CONFIG4, false);
feed_config4 &= 0xF3;
return cirque_gen6_write_reg(CGEN6_FEED_CONFIG4, feed_config4);
}
uint8_t cirque_gen6_set_ptp_mode(void) {
uint8_t feed_config4 = cirque_gen6_read_reg(CGEN6_FEED_CONFIG4, false);
feed_config4 &= 0xF7;
feed_config4 |= 0x04;
return cirque_gen6_write_reg(CGEN6_FEED_CONFIG4, feed_config4);
}
uint8_t cirque_gen6_swap_xy(bool set) {
uint8_t xy_config = cirque_gen6_read_reg(CGEN6_XY_CONFIG, false);
if (set) {
xy_config |= 0x04;
} else {
xy_config &= ~0x04;
}
return cirque_gen6_write_reg(CGEN6_XY_CONFIG, xy_config);
}
uint8_t cirque_gen6_invert_y(bool set) {
uint8_t xy_config = cirque_gen6_read_reg(CGEN6_XY_CONFIG, false);
if (set) {
xy_config |= 0x02;
} else {
xy_config &= ~0x02;
}
return cirque_gen6_write_reg(CGEN6_XY_CONFIG, xy_config);
}
uint8_t cirque_gen6_invert_x(bool set) {
uint8_t xy_config = cirque_gen6_read_reg(CGEN6_XY_CONFIG, false);
if (set) {
xy_config |= 0x01;
} else {
xy_config &= ~0x01;
}
return cirque_gen6_write_reg(CGEN6_XY_CONFIG, xy_config);
}
uint8_t cirque_gen6_enable_logical_scaling(bool set) {
uint8_t xy_config = cirque_gen6_read_reg(CGEN6_XY_CONFIG, false);
if (set) {
xy_config &= ~0x08;
} else {
xy_config |= 0x08;
}
return cirque_gen6_write_reg(CGEN6_XY_CONFIG, xy_config);
}
// Motion detection
uint8_t cirque_gen6_has_motion(void) {
return cirque_gen6_read_reg(CGEN6_I2C_DR, true);
}
// Report reading - fills ptp_report global
void cirque_gen_6_read_report(void) {
uint8_t packet[CGEN6_MAX_PACKET_SIZE];
if (cirque_gen6_read_report(packet, CGEN6_MAX_PACKET_SIZE) != I2C_STATUS_SUCCESS) {
return;
}
uint8_t report_id = packet[2];
// PTP mode report
if (report_id == CGEN6_PTP_REPORT_ID) {
ptp_report.fingers[0].tip = (packet[3] & 0x02) >> 1;
ptp_report.fingers[0].x = packet[5] << 8 | packet[4];
ptp_report.fingers[0].y = packet[7] << 8 | packet[6];
ptp_report.fingers[1].tip = (packet[8] & 0x02) >> 1;
ptp_report.fingers[1].x = packet[10] << 8 | packet[9];
ptp_report.fingers[1].y = packet[12] << 8 | packet[11];
ptp_report.scan_time = packet[14] << 8 | packet[13];
ptp_report.buttons = packet[16];
}
// Mouse/relative mode report
else if (report_id == CGEN6_MOUSE_REPORT_ID) {
ptp_report.buttons = packet[3];
ptp_report.xDelta = packet[4];
ptp_report.yDelta = packet[5];
ptp_report.scrollDelta = packet[6];
ptp_report.panDelta = packet[7];
has_motion = 1;
}
}
// Deferred callback for polling
uint32_t cirque_gen6_read_callback(uint32_t trigger_time, void *cb_arg) {
if (!trackpad_init) {
navigator_trackpad_device_init();
return NAVIGATOR_TRACKPAD_PROBE;
}
if (cirque_gen6_has_motion()) {
has_motion = 1;
cirque_gen_6_read_report();
}
return NAVIGATOR_TRACKPAD_READ;
}
// Device initialization
void navigator_trackpad_device_init(void) {
i2c_init();
i2c_status_t status = i2c_ping_address(NAVIGATOR_TRACKPAD_ADDRESS, NAVIGATOR_TRACKPAD_TIMEOUT);
if (status != I2C_STATUS_SUCCESS) {
trackpad_init = false;
return;
}
cirque_gen6_clear();
wait_ms(50);
uint8_t res = CGEN6_SUCCESS;
#if defined(NAVIGATOR_TRACKPAD_PTP_MODE)
res = cirque_gen6_set_ptp_mode();
#elif defined(NAVIGATOR_TRACKPAD_RELATIVE_MODE)
res = cirque_gen6_set_relative_mode();
#endif
if (res != CGEN6_SUCCESS) {
return;
}
// Reset to the default alignment
cirque_gen6_swap_xy(false);
cirque_gen6_invert_x(false);
cirque_gen6_invert_y(false);
cirque_gen6_swap_xy(true);
cirque_gen6_invert_x(true);
cirque_gen6_invert_y(true);
cirque_gen6_enable_logical_scaling(true);
trackpad_init = true;
// Only register the callback for the first time
if (!callback_token) {
callback_token = defer_exec(NAVIGATOR_TRACKPAD_READ, cirque_gen6_read_callback, NULL);
}
}
// CPI management
void set_cirque_cpi(void) {
// traverse the sequence by comparing the cpi_x value with the current cpi_x value
// set the cpi to the next value in the sequence
switch (current_cpi) {
case CPI_1: {
current_cpi = CPI_2;
break;
}
case CPI_2: {
current_cpi = CPI_3;
break;
}
case CPI_3: {
current_cpi = CPI_4;
break;
}
case CPI_4: {
current_cpi = CPI_5;
break;
}
case CPI_5: {
current_cpi = CPI_6;
break;
}
case CPI_6: {
current_cpi = CPI_7;
break;
}
case CPI_7: {
current_cpi = CPI_1;
break;
}
default: {
current_cpi = CPI_4;
break;
}
}
}
uint16_t navigator_trackpad_get_cpi(void) {
return current_cpi;
}
void restore_cpi(uint8_t cpi) {
current_cpi = cpi;
set_cirque_cpi();
}
void navigator_trackpad_set_cpi(uint16_t cpi) {
if (cpi == 0) { // Decrease one tick
if (current_cpi > 1) {
current_cpi--;
}
} else {
if (current_cpi < CPI_TICKS) {
current_cpi++;
}
}
set_cirque_cpi();
}
// Helper function to count active fingers
uint8_t cirque_gen6_finger_count(cgen6_report_t *report) {
uint8_t fingers = 0;
if (report->fingers[0].tip) {
fingers++;
}
if (report->fingers[1].tip) {
fingers++;
}
return fingers;
}

View File

@@ -0,0 +1,139 @@
// Copyright 2025 ZSA Technology Labs, Inc <contact@zsa.io>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <stdint.h>
#include <stdbool.h>
#include "i2c_master.h"
#include "deferred_exec.h"
// I2C configuration
#define NAVIGATOR_TRACKPAD_READ 7
#define NAVIGATOR_TRACKPAD_PROBE 1000
#ifndef NAVIGATOR_TRACKPAD_ADDRESS
# define NAVIGATOR_TRACKPAD_ADDRESS 0x58
#endif
#ifndef NAVIGATOR_TRACKPAD_TIMEOUT
# define NAVIGATOR_TRACKPAD_TIMEOUT 100
#endif
// Packet and report IDs
#define CGEN6_MAX_PACKET_SIZE 17
#define CGEN6_PTP_REPORT_ID 0x01
#define CGEN6_MOUSE_REPORT_ID 0x06
#define CGEN6_ABSOLUTE_REPORT_ID 0x09
// C3 error codes when reading memory
#define CGEN6_SUCCESS 0x00
#define CGEN6_CKSUM_FAILED 0x01
#define CGEN6_LEN_MISMATCH 0x02
#define CGEN6_I2C_FAILED 0x03
// C3 register addresses
#define CGEN6_REG_BASE 0x20000800
#define CGEN6_HARDWARE_ID CGEN6_REG_BASE + 0x08
#define CGEN6_FIRMWARE_ID CGEN6_REG_BASE + 0x09
#define CGEN6_FIRMWARE_REV CGEN6_REG_BASE + 0x10
#define CGEN6_VENDOR_ID CGEN6_REG_BASE + 0x0A
#define CGEN6_PRODUCT_ID CGEN6_REG_BASE + 0x0C
#define CGEN6_VERSION_ID CGEN6_REG_BASE + 0x0E
#define CGEN6_FEED_CONFIG4 0x200E000B
#define CGEN6_FEED_CONFIG3 0x200E000A
#define CGEN6_SYS_CONFIG1 0x20000008
#define CGEN6_XY_CONFIG 0x20080018
#define CGEN6_SFR_BASE 0x40000008
#define CGEN6_GPIO_BASE 0x00052000
#define CGEN6_I2C_DR 0x61010000
// CPI configuration
#define CPI_TICKS 7
#define DEFAULT_CPI_TICK 4
#define CPI_1 200
#define CPI_2 400
#define CPI_3 800
#define CPI_4 1024
#define CPI_5 1400
#define CPI_6 1800
#define CPI_7 2048
// Physical trackpad dimensions (in 0.01 inch units for HID descriptor)
// Navigator trackpad is circular, 40mm diameter (1.575 inches actual)
// We report larger physical dimensions to maintain good cursor speed
// The OS uses this ratio to calculate movement sensitivity
#ifndef TRACKPAD_PHYSICAL_WIDTH
# define TRACKPAD_PHYSICAL_WIDTH 157 // 1.57 inches (40mm actual size)
#endif
#ifndef TRACKPAD_PHYSICAL_HEIGHT
# define TRACKPAD_PHYSICAL_HEIGHT 157 // 1.57 inches (40mm actual size)
#endif
// Logical coordinate range from Cirque Gen6 sensor in PTP mode
// Actual usable range is approximately 1-897, rounded to 900
#define TRACKPAD_LOGICAL_MAX 900
// Common finger structure (used by both mouse and PTP modes)
typedef struct {
uint8_t tip;
uint16_t x;
uint16_t y;
} cgen6_finger_t;
// Common report structure (used by both modes)
typedef struct {
cgen6_finger_t fingers[2];
uint8_t buttons;
uint16_t scan_time; // Sensor timestamp (100μs units) - used by PTP mode
int8_t xDelta; // Used by mouse mode
int8_t yDelta; // Used by mouse mode
int8_t scrollDelta; // Used by mouse mode
int8_t panDelta; // Used by mouse mode
} cgen6_report_t;
// Low-level I2C functions
i2c_status_t cirque_gen6_read_report(uint8_t *data, uint16_t cnt);
void cirque_gen6_clear(void);
uint8_t cirque_gen6_read_memory(uint32_t addr, uint8_t *data, uint16_t cnt, bool fast_read);
uint8_t cirque_gen6_write_memory(uint32_t addr, uint8_t *data, uint16_t cnt);
// Register access functions
uint8_t cirque_gen6_read_reg(uint32_t addr, bool fast_read);
uint16_t cirque_gen6_read_reg_16(uint32_t addr);
uint32_t cirque_gen6_read_reg_32(uint32_t addr);
uint8_t cirque_gen6_write_reg(uint32_t addr, uint8_t data);
uint8_t cirque_gen6_write_reg_16(uint32_t addr, uint16_t data);
uint8_t cirque_gen6_write_reg_32(uint32_t addr, uint32_t data);
// Configuration functions
uint8_t cirque_gen6_set_relative_mode(void);
uint8_t cirque_gen6_set_ptp_mode(void);
uint8_t cirque_gen6_swap_xy(bool set);
uint8_t cirque_gen6_invert_y(bool set);
uint8_t cirque_gen6_invert_x(bool set);
uint8_t cirque_gen6_enable_logical_scaling(bool set);
// Motion detection and report reading
uint8_t cirque_gen6_has_motion(void);
void cirque_gen_6_read_report(void);
// Deferred callback
uint32_t cirque_gen6_read_callback(uint32_t trigger_time, void *cb_arg);
// Device initialization
void navigator_trackpad_device_init(void);
// CPI management
uint16_t navigator_trackpad_get_cpi(void);
void navigator_trackpad_set_cpi(uint16_t cpi);
void restore_cpi(uint8_t cpi);
// Shared globals
extern deferred_token callback_token;
extern uint16_t current_cpi;
extern uint8_t has_motion;
extern bool trackpad_init;
extern cgen6_report_t ptp_report; // Shared between modes
// Helper functions
uint8_t cirque_gen6_finger_count(cgen6_report_t *report);

View File

@@ -0,0 +1,374 @@
// Copyright 2025 ZSA Technology Labs, Inc <contact@zsa.io>
// SPDX-License-Identifier: GPL-2.0-or-later
// Mouse mode implementation for Navigator trackpad
// Handles gesture detection, tap-to-click, two-finger scrolling, and scroll inertia
#include <math.h>
#include "navigator_trackpad_mouse.h"
#include "navigator_trackpad_common.h"
#include "navigator.h" // For scroll utilities
#include "quantum.h"
#include "timer.h"
// Pointing device driver definition
const pointing_device_driver_t navigator_trackpad_pointing_device_driver = {
.init = navigator_trackpad_device_init,
.get_report = navigator_trackpad_get_report,
.get_cpi = navigator_trackpad_get_cpi,
.set_cpi = navigator_trackpad_set_cpi
};
// Mode-specific globals
trackpad_gesture_t gesture = {0};
extern bool set_scrolling; // Declared in navigator.c
#ifdef NAVIGATOR_TRACKPAD_SCROLL_INERTIA_ENABLE
scroll_inertia_t scroll_inertia = {0};
#endif
#ifdef NAVIGATOR_TRACKPAD_MACOS_SCROLLING
float macos_scroll_accumulated_h = 0;
float macos_scroll_accumulated_v = 0;
#endif
report_mouse_t navigator_trackpad_get_report(report_mouse_t mouse_report) {
#ifdef PRECISION_TRACKPAD_ENABLE
// When PTP is enabled, don't send mouse reports - PTP task handles everything
return mouse_report;
#else
// Mouse mode - process gestures and send mouse reports
#if defined(NAVIGATOR_TRACKPAD_PTP_MODE)
// Handle pending click release from previous cycle
if (gesture.pending_click) {
gesture.pending_click = false;
mouse_report.buttons = 0;
return mouse_report;
}
# ifdef NAVIGATOR_TRACKPAD_SCROLL_INERTIA_ENABLE
// Process scroll inertia when active
if (scroll_inertia.active && timer_elapsed(scroll_inertia.timer) >= NAVIGATOR_TRACKPAD_SCROLL_INERTIA_INTERVAL) {
scroll_inertia.timer = timer_read();
// Apply friction to velocity (Q8 fixed point math)
// Friction reduces velocity towards zero
int16_t friction_x = (scroll_inertia.vx * NAVIGATOR_TRACKPAD_SCROLL_INERTIA_FRICTION) / 256;
int16_t friction_y = (scroll_inertia.vy * NAVIGATOR_TRACKPAD_SCROLL_INERTIA_FRICTION) / 256;
// Ensure we always reduce by at least 1 if not zero
if (scroll_inertia.vx > 0 && friction_x < 1) friction_x = 1;
if (scroll_inertia.vx < 0 && friction_x > -1) friction_x = -1;
if (scroll_inertia.vy > 0 && friction_y < 1) friction_y = 1;
if (scroll_inertia.vy < 0 && friction_y > -1) friction_y = -1;
scroll_inertia.vx -= friction_x;
scroll_inertia.vy -= friction_y;
// Convert Q8 velocity to scroll value
# ifdef NAVIGATOR_TRACKPAD_MACOS_SCROLLING
// macOS mode: send raw velocity deltas (descriptor tells macOS the resolution)
int16_t scroll_x = scroll_inertia.vx / 256;
int16_t scroll_y = scroll_inertia.vy / 256;
# else
// Hi-res mode: apply multiplier for Windows/Linux
int16_t scroll_x = (scroll_inertia.vx * NAVIGATOR_TRACKPAD_SCROLL_MULTIPLIER) / 256;
int16_t scroll_y = (scroll_inertia.vy * NAVIGATOR_TRACKPAD_SCROLL_MULTIPLIER) / 256;
# endif
// Clamp to int8_t range
scroll_x = (scroll_x > 127) ? 127 : ((scroll_x < -127) ? -127 : scroll_x);
scroll_y = (scroll_y > 127) ? 127 : ((scroll_y < -127) ? -127 : scroll_y);
// Check if velocity is too low to continue
int16_t abs_vx = scroll_inertia.vx < 0 ? -scroll_inertia.vx : scroll_inertia.vx;
int16_t abs_vy = scroll_inertia.vy < 0 ? -scroll_inertia.vy : scroll_inertia.vy;
# ifdef NAVIGATOR_TRACKPAD_MACOS_SCROLLING
// In macOS mode, track consecutive frames with no output to detect janky tail
if (scroll_x == 0 && scroll_y == 0) {
scroll_inertia.no_output_count++;
} else {
scroll_inertia.no_output_count = 0;
}
// Stop if: velocity very low OR too many consecutive frames without output
bool velocity_too_low = (abs_vx < 64 && abs_vy < 64); // Same as hi-res mode
bool stalled = (scroll_inertia.no_output_count > 5); // 5 frames (~35ms) with no output
if (velocity_too_low || stalled) {
scroll_inertia.active = false;
} else
# else
if (abs_vx < 64 && abs_vy < 64) { // Threshold in Q8 (0.25 in real units)
scroll_inertia.active = false;
} else
# endif
{
// Apply scroll inversion if configured
# ifdef NAVIGATOR_SCROLL_INVERT_X
mouse_report.h = -scroll_x;
# else
mouse_report.h = scroll_x;
# endif
# ifdef NAVIGATOR_SCROLL_INVERT_Y
mouse_report.v = scroll_y;
# else
mouse_report.v = -scroll_y;
# endif
return mouse_report;
}
}
# endif
#endif // End scroll inertia block
#if defined(NAVIGATOR_TRACKPAD_RELATIVE_MODE)
if (!has_motion || !trackpad_init) {
return mouse_report;
}
mouse_report.x = ptp_report.xDelta;
mouse_report.y = ptp_report.yDelta;
mouse_report.v = ptp_report.scrollDelta;
mouse_report.h = ptp_report.panDelta;
mouse_report.buttons = ptp_report.buttons;
#elif defined(NAVIGATOR_TRACKPAD_PTP_MODE)
if (!has_motion || !trackpad_init) {
return mouse_report;
}
// Create local snapshot to avoid race condition with callback updating ptp_report
cgen6_report_t local_report = ptp_report;
uint8_t raw_fingers = cirque_gen6_finger_count(&local_report);
bool is_touching = local_report.fingers[0].tip;
bool was_idle = (gesture.state == TP_IDLE);
uint8_t fingers = raw_fingers;
// Handle finger down - record start position (regardless of current state)
if (is_touching && was_idle) {
gesture.touch_start_time = timer_read();
gesture.prev_x = local_report.fingers[0].x;
gesture.prev_y = local_report.fingers[0].y;
gesture.settled_x = 0; // Will be set after settle time
gesture.settled_y = 0;
gesture.settled = false;
gesture.max_finger_count = fingers;
gesture.state = TP_MOVING;
# ifdef NAVIGATOR_TRACKPAD_SCROLL_INERTIA_ENABLE
// Stop any ongoing scroll inertia when finger touches
scroll_inertia.active = false;
scroll_inertia.smooth_vx = 0;
scroll_inertia.smooth_vy = 0;
# ifdef NAVIGATOR_TRACKPAD_MACOS_SCROLLING
scroll_inertia.no_output_count = 0;
# endif
# endif
# ifdef NAVIGATOR_TRACKPAD_MACOS_SCROLLING
// Reset macOS scroll accumulator when starting new gesture
macos_scroll_accumulated_h = 0;
macos_scroll_accumulated_v = 0;
# endif
}
// Handle finger up - evaluate tap at lift time (libinput style)
if (!is_touching && !was_idle) {
uint16_t duration = timer_elapsed(gesture.touch_start_time);
// Calculate distance from settled position (or treat as no movement if never settled)
int32_t dist_sq = 0;
if (gesture.settled) {
int16_t dx = gesture.prev_x - gesture.settled_x;
int16_t dy = gesture.prev_y - gesture.settled_y;
dist_sq = (int32_t)dx * dx + (int32_t)dy * dy;
}
// Check tap conditions: short duration AND small movement from settled position
bool is_tap = (duration <= NAVIGATOR_TRACKPAD_TAP_TIMEOUT) &&
(dist_sq <= NAVIGATOR_TRACKPAD_TAP_MOVE_THRESHOLD);
// Don't trigger single-finger taps that happen shortly after a scroll ends (within 100ms)
// But allow two-finger taps (right-click) even after scrolling
# ifdef NAVIGATOR_TRACKPAD_SCROLL_WITH_TWO_FINGERS
if (is_tap && gesture.max_finger_count == 1 &&
timer_elapsed(gesture.last_scroll_end) < 100) {
is_tap = false; // Suppress single-finger tap after scrolling
}
# endif
if (is_tap) {
# ifdef NAVIGATOR_TRACKPAD_ENABLE_DOUBLE_TAP
if (gesture.max_finger_count >= 2) {
mouse_report.x = 0;
mouse_report.y = 0;
mouse_report.buttons = pointing_device_handle_buttons(mouse_report.buttons, true, POINTING_DEVICE_BUTTON2);
gesture.pending_click = true;
} else
# endif
# ifdef NAVIGATOR_TRACKPAD_ENABLE_TAP
if (gesture.max_finger_count == 1) {
mouse_report.x = 0;
mouse_report.y = 0;
mouse_report.buttons = pointing_device_handle_buttons(mouse_report.buttons, true, POINTING_DEVICE_BUTTON1);
gesture.pending_click = true;
}
# endif
}
# if defined(NAVIGATOR_TRACKPAD_SCROLL_INERTIA_ENABLE) && defined(NAVIGATOR_TRACKPAD_SCROLL_WITH_TWO_FINGERS)
// Start scroll inertia if we were scrolling and have enough velocity
if (gesture.state == TP_SCROLLING) {
// Use smoothed velocity (already in Q8 format)
int16_t abs_vx = scroll_inertia.smooth_vx < 0 ? -scroll_inertia.smooth_vx : scroll_inertia.smooth_vx;
int16_t abs_vy = scroll_inertia.smooth_vy < 0 ? -scroll_inertia.smooth_vy : scroll_inertia.smooth_vy;
// Trigger threshold is now in Q8 (multiply by 256)
if (abs_vx >= (NAVIGATOR_TRACKPAD_SCROLL_INERTIA_TRIGGER * 256) ||
abs_vy >= (NAVIGATOR_TRACKPAD_SCROLL_INERTIA_TRIGGER * 256)) {
scroll_inertia.vx = scroll_inertia.smooth_vx;
scroll_inertia.vy = scroll_inertia.smooth_vy;
scroll_inertia.timer = timer_read();
scroll_inertia.active = true;
}
}
# endif
// Record if this was a scroll gesture ending
# ifdef NAVIGATOR_TRACKPAD_SCROLL_WITH_TWO_FINGERS
if (gesture.state == TP_SCROLLING || gesture.max_finger_count >= 2) {
gesture.last_scroll_end = timer_read();
}
# endif
gesture.state = TP_IDLE;
set_scrolling = false;
}
// Handle ongoing touch - movement and scrolling
if (is_touching && !was_idle) {
// Track max fingers during gesture
if (fingers > gesture.max_finger_count) {
gesture.max_finger_count = fingers;
}
# ifdef NAVIGATOR_TRACKPAD_SCROLL_WITH_TWO_FINGERS
// Determine mode based on finger count
// Once scrolling starts, keep scrolling until all fingers lift (no mid-gesture transitions)
if (fingers >= 2 && gesture.state != TP_SCROLLING) {
gesture.state = TP_SCROLLING;
// Reset position tracking - use finger[0] initially
gesture.prev_x = local_report.fingers[0].x;
gesture.prev_y = local_report.fingers[0].y;
}
// Note: We don't transition from SCROLLING back to MOVING mid-gesture anymore
// Once scroll starts, it continues until all fingers lift (gesture ends)
# endif
uint16_t duration = timer_elapsed(gesture.touch_start_time);
// Record settled position once settle time elapses
if (!gesture.settled && duration >= NAVIGATOR_TRACKPAD_TAP_SETTLE_TIME) {
gesture.settled = true;
gesture.settled_x = local_report.fingers[0].x;
gesture.settled_y = local_report.fingers[0].y;
}
// Check if we should suppress movement (might still be a tap)
int32_t dist_sq = 0;
if (gesture.settled) {
int16_t dx = local_report.fingers[0].x - gesture.settled_x;
int16_t dy = local_report.fingers[0].y - gesture.settled_y;
dist_sq = (int32_t)dx * dx + (int32_t)dy * dy;
}
// Only report movement if: timeout exceeded OR moved beyond tap threshold (after settling)
bool should_move = (duration > NAVIGATOR_TRACKPAD_TAP_TIMEOUT) ||
(gesture.settled && dist_sq > NAVIGATOR_TRACKPAD_TAP_MOVE_THRESHOLD);
if (should_move) {
int16_t delta_x = local_report.fingers[0].x - gesture.prev_x;
int16_t delta_y = local_report.fingers[0].y - gesture.prev_y;
// Clamp deltas to prevent jumps from bad data
if (delta_x > NAVIGATOR_TRACKPAD_MAX_DELTA) delta_x = NAVIGATOR_TRACKPAD_MAX_DELTA;
if (delta_x < -NAVIGATOR_TRACKPAD_MAX_DELTA) delta_x = -NAVIGATOR_TRACKPAD_MAX_DELTA;
if (delta_y > NAVIGATOR_TRACKPAD_MAX_DELTA) delta_y = NAVIGATOR_TRACKPAD_MAX_DELTA;
if (delta_y < -NAVIGATOR_TRACKPAD_MAX_DELTA) delta_y = -NAVIGATOR_TRACKPAD_MAX_DELTA;
if (delta_x != 0 || delta_y != 0) {
# ifdef NAVIGATOR_TRACKPAD_SCROLL_WITH_TWO_FINGERS
if (gesture.state == TP_SCROLLING) {
# ifdef NAVIGATOR_TRACKPAD_MACOS_SCROLLING
// macOS mode: send raw deltas, macOS handles scaling via HIDScrollResolution
// Apple trackpads report raw sensor deltas and let macOS apply acceleration
int16_t scroll_x = delta_x;
int16_t scroll_y = delta_y;
# else
// Hi-res mode: apply multiplier for Windows/Linux
// These OSes divide by the Resolution Multiplier (120)
int16_t scroll_x = delta_x * NAVIGATOR_TRACKPAD_SCROLL_MULTIPLIER;
int16_t scroll_y = delta_y * NAVIGATOR_TRACKPAD_SCROLL_MULTIPLIER;
# endif
# ifdef NAVIGATOR_TRACKPAD_SCROLL_INERTIA_ENABLE
// Track velocity for inertia using exponential smoothing (Q8 fixed point)
// Alpha = 0.3 (77/256) - balances responsiveness with smoothness
// When direction changes, reset smoothing to avoid fighting old momentum
int16_t new_vx = delta_x * 256;
int16_t new_vy = delta_y * 256;
// Detect direction change (signs differ and both non-zero)
bool dir_change_x = (scroll_inertia.smooth_vx > 0 && new_vx < 0) ||
(scroll_inertia.smooth_vx < 0 && new_vx > 0);
bool dir_change_y = (scroll_inertia.smooth_vy > 0 && new_vy < 0) ||
(scroll_inertia.smooth_vy < 0 && new_vy > 0);
if (dir_change_x) {
// Direction changed - blend toward zero first, then pick up new direction
scroll_inertia.smooth_vx = (scroll_inertia.smooth_vx * 128 + new_vx * 128) / 256;
} else {
// Same direction - normal EMA smoothing
scroll_inertia.smooth_vx = (scroll_inertia.smooth_vx * 179 + new_vx * 77) / 256;
}
if (dir_change_y) {
scroll_inertia.smooth_vy = (scroll_inertia.smooth_vy * 128 + new_vy * 128) / 256;
} else {
scroll_inertia.smooth_vy = (scroll_inertia.smooth_vy * 179 + new_vy * 77) / 256;
}
# endif
// Clamp to int8_t range for the report
scroll_x = (scroll_x > 127) ? 127 : ((scroll_x < -127) ? -127 : scroll_x);
scroll_y = (scroll_y > 127) ? 127 : ((scroll_y < -127) ? -127 : scroll_y);
// Apply scroll inversion if configured
# ifdef NAVIGATOR_SCROLL_INVERT_X
mouse_report.h = -scroll_x;
# else
mouse_report.h = scroll_x;
# endif
# ifdef NAVIGATOR_SCROLL_INVERT_Y
mouse_report.v = scroll_y;
# else
mouse_report.v = -scroll_y;
# endif
mouse_report.x = 0;
mouse_report.y = 0;
} else
# endif
{
// One-finger movement: mouse cursor
mouse_report.x = (delta_x < 0) ? -powf(-delta_x, 1.2) : powf(delta_x, 1.2);
mouse_report.y = (delta_y < 0) ? -powf(-delta_y, 1.2) : powf(delta_y, 1.2);
}
}
}
// Update prev position for next frame
gesture.prev_x = local_report.fingers[0].x;
gesture.prev_y = local_report.fingers[0].y;
}
#endif
has_motion = 0;
return mouse_report;
#endif // PRECISION_TRACKPAD_ENABLE
}

View File

@@ -0,0 +1,109 @@
// Copyright 2025 ZSA Technology Labs, Inc <contact@zsa.io>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "navigator_trackpad_common.h"
#include "pointing_device.h"
#include "report.h"
// Tap detection configuration
#ifndef NAVIGATOR_TRACKPAD_TAP_MOVE_THRESHOLD
# define NAVIGATOR_TRACKPAD_TAP_MOVE_THRESHOLD 100 // Max movement (squared) before tap becomes a drag
#endif
#ifndef NAVIGATOR_TRACKPAD_TAP_TIMEOUT
# define NAVIGATOR_TRACKPAD_TAP_TIMEOUT 200 // Max duration (ms) for a tap
#endif
#ifndef NAVIGATOR_TRACKPAD_TAP_SETTLE_TIME
# define NAVIGATOR_TRACKPAD_TAP_SETTLE_TIME 30 // Ignore movement during initial contact (ms)
#endif
#ifndef NAVIGATOR_TRACKPAD_MAX_DELTA
# define NAVIGATOR_TRACKPAD_MAX_DELTA 250 // Max allowed delta per frame to prevent jumps
#endif
// Scroll configuration
#ifndef NAVIGATOR_TRACKPAD_SCROLL_DIVIDER
# define NAVIGATOR_TRACKPAD_SCROLL_DIVIDER 10
#endif
#ifndef NAVIGATOR_TRACKPAD_SCROLL_MULTIPLIER
# define NAVIGATOR_TRACKPAD_SCROLL_MULTIPLIER 3
#endif
// Two-finger scrolling (define to enable)
// #define NAVIGATOR_TRACKPAD_SCROLL_WITH_TWO_FINGERS
// macOS scrolling mode (define to enable)
// macOS doesn't respect the HID Resolution Multiplier descriptor.
// When enabled, sends raw scroll deltas without the multiplier (like Apple trackpads).
// macOS applies its own HIDScrollResolution (400 DPI) and acceleration curves.
// When disabled (default), applies multiplier for Windows/Linux hi-res scrolling.
// #define NAVIGATOR_TRACKPAD_MACOS_SCROLLING
// Scroll inversion configuration
// Define these to invert scroll direction on respective axes
// #define NAVIGATOR_SCROLL_INVERT_X
// #define NAVIGATOR_SCROLL_INVERT_Y
// Scroll inertia configuration
/*
To enable, add to your config.h:
#define NAVIGATOR_TRACKPAD_SCROLL_INERTIA_ENABLE
Configurable values (all optional):
- NAVIGATOR_TRACKPAD_SCROLL_INERTIA_FRICTION - Higher = stops faster (default: 50, range 1-255)
- NAVIGATOR_TRACKPAD_SCROLL_INERTIA_INTERVAL - Time between glide reports in ms (default: 15)
- NAVIGATOR_TRACKPAD_SCROLL_INERTIA_TRIGGER - Minimum velocity to trigger glide (default: 3)
*/
#ifdef NAVIGATOR_TRACKPAD_SCROLL_INERTIA_ENABLE
#ifndef NAVIGATOR_TRACKPAD_SCROLL_INERTIA_FRICTION
#define NAVIGATOR_TRACKPAD_SCROLL_INERTIA_FRICTION 5 // Higher = stops faster (1-255)
#endif
#ifndef NAVIGATOR_TRACKPAD_SCROLL_INERTIA_INTERVAL
#define NAVIGATOR_TRACKPAD_SCROLL_INERTIA_INTERVAL 7 // Glide report interval in ms
#endif
#ifndef NAVIGATOR_TRACKPAD_SCROLL_INERTIA_TRIGGER
#define NAVIGATOR_TRACKPAD_SCROLL_INERTIA_TRIGGER 1 // Min velocity to trigger glide
#endif
typedef struct {
int16_t vx; // Current X velocity (Q8 fixed point)
int16_t vy; // Current Y velocity (Q8 fixed point)
int16_t smooth_vx; // Smoothed X velocity (Q8 fixed point)
int16_t smooth_vy; // Smoothed Y velocity (Q8 fixed point)
uint16_t timer; // Timer for interval tracking
bool active; // Is glide currently active
#ifdef NAVIGATOR_TRACKPAD_MACOS_SCROLLING
uint8_t no_output_count; // Counter for consecutive frames with no output
#endif
} scroll_inertia_t;
#endif
// Trackpad gesture state machine
typedef enum {
TP_IDLE, // No fingers touching
TP_MOVING, // One finger movement = mouse cursor
TP_SCROLLING, // Two finger movement = scroll
} trackpad_state_t;
typedef struct {
trackpad_state_t state;
uint16_t touch_start_time; // When finger first touched
uint16_t settled_x; // Position after settle time (for tap threshold)
uint16_t settled_y;
uint16_t prev_x; // Previous position (for delta calculation)
uint16_t prev_y;
uint8_t max_finger_count; // Max fingers seen during this gesture
bool settled; // Has the settle time elapsed?
bool pending_click; // Need to send a click release next cycle
uint16_t last_scroll_end; // Time when last scroll gesture ended
} trackpad_gesture_t;
// Mouse mode functions
report_mouse_t navigator_trackpad_get_report(report_mouse_t mouse_report);
// Pointing device driver struct
extern const pointing_device_driver_t navigator_trackpad_pointing_device_driver;

View File

@@ -0,0 +1,166 @@
// Copyright 2025 ZSA Technology Labs, Inc <contact@zsa.io>
// SPDX-License-Identifier: GPL-2.0-or-later
// PTP (Precision Touchpad) mode implementation for Navigator trackpad
// Converts Cirque Gen 6 sensor data to Windows Precision Touchpad HID reports
#include "navigator_trackpad_ptp.h"
#include "navigator_trackpad_common.h"
#include "precision_trackpad_drivers.h"
#include "quantum.h"
#include "report.h"
#ifdef PRECISION_TRACKPAD_ENABLE
// External declaration for PTP report sending
extern void send_trackpad(report_trackpad_t *report);
#if defined(NAVIGATOR_TRACKPAD_PTP_MODE)
// PTP task function - converts trackpad touches to PTP reports
static bool navigator_trackpad_ptp_task(void) {
if (!has_motion || !trackpad_init) {
return false;
}
// Create local snapshot to avoid race condition with callback
cgen6_report_t local_report = ptp_report;
uint8_t raw_fingers = cirque_gen6_finger_count(&local_report);
// Initialize PTP report
report_trackpad_t ptp = {0};
ptp.report_id = REPORT_ID_TRACKPAD;
// Sensitivity scaling: Track previous RAW sensor position and accumulated scaled position
// Apply sensitivity only to the delta from raw sensor, then accumulate
static uint16_t prev_raw_x[2] = {0, 0};
static uint16_t prev_raw_y[2] = {0, 0};
static uint16_t accum_x[2] = {0, 0};
static uint16_t accum_y[2] = {0, 0};
static bool was_touching[2] = {false, false};
if (local_report.fingers[0].tip) {
uint16_t raw_x = local_report.fingers[0].x;
uint16_t raw_y = local_report.fingers[0].y;
if (was_touching[0]) {
// Calculate delta from RAW sensor data
int16_t delta_x = (int16_t)(raw_x - prev_raw_x[0]);
int16_t delta_y = (int16_t)(raw_y - prev_raw_y[0]);
// Apply sensitivity to the delta
delta_x = (int16_t)((float)delta_x * NAVIGATOR_TRACKPAD_SENSITIVITY);
delta_y = (int16_t)((float)delta_y * NAVIGATOR_TRACKPAD_SENSITIVITY);
// Add scaled delta to accumulated position
int32_t new_x = (int32_t)accum_x[0] + delta_x;
int32_t new_y = (int32_t)accum_y[0] + delta_y;
// Clamp to valid range
if (new_x < 0) new_x = 0;
if (new_x > 4095) new_x = 4095;
if (new_y < 0) new_y = 0;
if (new_y > 4095) new_y = 4095;
accum_x[0] = (uint16_t)new_x;
accum_y[0] = (uint16_t)new_y;
} else {
// First touch - initialize accumulated position to raw position
accum_x[0] = raw_x;
accum_y[0] = raw_y;
}
ptp.contacts[0].confidence = 1;
ptp.contacts[0].tip = 1;
ptp.contacts[0].contact_id = 0;
ptp.contacts[0].x = accum_x[0];
ptp.contacts[0].y = accum_y[0];
prev_raw_x[0] = raw_x;
prev_raw_y[0] = raw_y;
was_touching[0] = true;
} else {
was_touching[0] = false;
}
if (local_report.fingers[1].tip) {
uint16_t raw_x = local_report.fingers[1].x;
uint16_t raw_y = local_report.fingers[1].y;
if (was_touching[1]) {
// Calculate delta from RAW sensor data
int16_t delta_x = (int16_t)(raw_x - prev_raw_x[1]);
int16_t delta_y = (int16_t)(raw_y - prev_raw_y[1]);
// Apply sensitivity to the delta
delta_x = (int16_t)((float)delta_x * NAVIGATOR_TRACKPAD_SENSITIVITY);
delta_y = (int16_t)((float)delta_y * NAVIGATOR_TRACKPAD_SENSITIVITY);
// Add scaled delta to accumulated position
int32_t new_x = (int32_t)accum_x[1] + delta_x;
int32_t new_y = (int32_t)accum_y[1] + delta_y;
// Clamp to valid range
if (new_x < 0) new_x = 0;
if (new_x > 4095) new_x = 4095;
if (new_y < 0) new_y = 0;
if (new_y > 4095) new_y = 4095;
accum_x[1] = (uint16_t)new_x;
accum_y[1] = (uint16_t)new_y;
} else {
// First touch - initialize accumulated position to raw position
accum_x[1] = raw_x;
accum_y[1] = raw_y;
}
ptp.contacts[1].confidence = 1;
ptp.contacts[1].tip = 1;
ptp.contacts[1].contact_id = 1;
ptp.contacts[1].x = accum_x[1];
ptp.contacts[1].y = accum_y[1];
prev_raw_x[1] = raw_x;
prev_raw_y[1] = raw_y;
was_touching[1] = true;
} else {
was_touching[1] = false;
}
// Set scan time, contact count, and buttons (after contacts per Microsoft spec)
// Use the sensor's scan_time for accurate gesture velocity calculation
ptp.scan_time = local_report.scan_time;
ptp.contact_count = raw_fingers;
ptp.button1 = (local_report.buttons & 0x01) ? 1 : 0;
ptp.button2 = (local_report.buttons & 0x02) ? 1 : 0;
ptp.button3 = (local_report.buttons & 0x04) ? 1 : 0;
if (raw_fingers > 0) {
send_trackpad(&ptp);
}
has_motion = 0;
return true;
}
#else
// Stub for when PTP mode is not enabled
static bool navigator_trackpad_ptp_task(void) {
return false;
}
#endif
// Internal init function
static void navigator_trackpad_ptp_init(void) {
navigator_trackpad_device_init(); // Common init
}
// Driver registration
const precision_trackpad_driver_t navigator_trackpad_precision_trackpad_driver = {
.init = navigator_trackpad_ptp_init,
.task = navigator_trackpad_ptp_task,
.set_cpi = navigator_trackpad_set_cpi,
.get_cpi = navigator_trackpad_get_cpi
};
#endif // PRECISION_TRACKPAD_ENABLE

View File

@@ -0,0 +1,28 @@
// Copyright 2025 ZSA Technology Labs, Inc <contact@zsa.io>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "navigator_trackpad_common.h"
#include "precision_trackpad_drivers.h"
#include "report.h"
// Trackpad sensitivity multiplier (1.0 = native, >1.0 = more sensitive)
// Higher values make cursor/gestures move faster
// Add to your config.h to customize:
// #define NAVIGATOR_TRACKPAD_SENSITIVITY 1.5f
#ifndef NAVIGATOR_TRACKPAD_SENSITIVITY
# define NAVIGATOR_TRACKPAD_SENSITIVITY 1.0f
#endif
#ifdef PRECISION_TRACKPAD_ENABLE
// Driver registration
extern const precision_trackpad_driver_t navigator_trackpad_precision_trackpad_driver;
#ifndef NAVIGATOR_TRACKPAD_PTP_MODE
#error "NAVIGATOR_TRACKPAD_PTP_MODE must be defined when using the precision trackpad driver"
#endif
#endif
#ifndef NAVIGATOR_TRACKPAD_SENSITIVITY
# define NAVIGATOR_TRACKPAD_SENSITIVITY 1.3f
#endif

View File

@@ -1,16 +0,0 @@
// Copyright 2024 ZSA Technology Labs
// SPDX-License-Identifier: GPL-2.0-or-later
#include "precision_trackpad.h"
#include "drivers/sensors/navigator_trackpad.h"
void precision_trackpad_init_kb(void) {
// Initialize the navigator trackpad sensor
navigator_trackpad_device_init();
}
bool precision_trackpad_task_kb(void) {
// Call the PTP task to process sensor data and send reports
navigator_trackpad_task();
return true; // Activity occurred
}

View File

@@ -1,16 +0,0 @@
// Copyright 2024 ZSA Technology Labs
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "quantum.h"
#ifdef PRECISION_TRACKPAD_ENABLE
// Initialize the trackpad
void trackpad_init(void);
// Update trackpad state and send reports
void trackpad_task(void);
#endif // PRECISION_TRACKPAD_ENABLE

View File

@@ -1,166 +0,0 @@
#!/usr/bin/env python3
"""
Linux HID Monitor for Windows Precision Trackpad Reports
Reads raw HID reports from /dev/hidrawX and parses them
Usage: sudo python3 ptp_monitor.py /dev/hidrawX
"""
import struct
import sys
import time
def parse_ptp_report(data):
"""Parse our PTP report format"""
if len(data) < 15:
return None
report_id = data[0]
# Debug: print first few bytes
if data[0] == 0x01:
print(f"Input Report (0x01): {' '.join(f'{b:02x}' for b in data[:15])}")
if report_id == 0x01: # REPORT_ID_TRACKPAD
# Contact 0 (bytes 1-5)
c0_flags = data[1]
c0_confidence = (c0_flags >> 0) & 1
c0_tip = (c0_flags >> 1) & 1
c0_contact_id = (c0_flags >> 2) & 3
c0_x = struct.unpack('<H', data[2:4])[0]
c0_y = struct.unpack('<H', data[4:6])[0]
# Contact 1 (bytes 6-10)
c1_flags = data[6]
c1_confidence = (c1_flags >> 0) & 1
c1_tip = (c1_flags >> 1) & 1
c1_contact_id = (c1_flags >> 2) & 3
c1_x = struct.unpack('<H', data[7:9])[0]
c1_y = struct.unpack('<H', data[9:11])[0]
# Scan time and contact count
scan_time = struct.unpack('<H', data[11:13])[0]
contact_count = data[13]
# Buttons
buttons = data[14]
btn1 = (buttons >> 0) & 1
btn2 = (buttons >> 1) & 1
btn3 = (buttons >> 2) & 1
return {
'type': 'input',
'contact_count': contact_count,
'scan_time': scan_time,
'c0': {
'tip': c0_tip,
'confidence': c0_confidence,
'id': c0_contact_id,
'x': c0_x,
'y': c0_y
},
'c1': {
'tip': c1_tip,
'confidence': c1_confidence,
'id': c1_contact_id,
'x': c1_x,
'y': c1_y
},
'buttons': {'b1': btn1, 'b2': btn2, 'b3': btn3}
}
elif report_id in [0x02, 0x03, 0x04, 0x05]: # Feature reports
return {
'type': 'feature',
'report_id': report_id,
'data': data[:15]
}
return None
def format_report(report, show_all=False):
"""Format report for display"""
if report['type'] == 'input':
lines = []
lines.append(f"Count: {report['contact_count']}, "
f"Scan: {report['scan_time']:5d}, "
f"Btns: {report['buttons']['b1']}{report['buttons']['b2']}{report['buttons']['b3']}")
# Only show contacts that are active (or show all if requested)
if report['c0']['tip'] or show_all:
lines.append(f" C0: id={report['c0']['id']}, "
f"conf={report['c0']['confidence']}, "
f"tip={report['c0']['tip']}, "
f"x={report['c0']['x']:4d}, "
f"y={report['c0']['y']:4d}")
if report['c1']['tip'] or show_all:
lines.append(f" C1: id={report['c1']['id']}, "
f"conf={report['c1']['confidence']}, "
f"tip={report['c1']['tip']}, "
f"x={report['c1']['x']:4d}, "
f"y={report['c1']['y']:4d}")
return '\n'.join(lines)
elif report['type'] == 'feature':
hex_str = ' '.join(f'{b:02x}' for b in report['data'])
return f"Feature Report ID: 0x{report['report_id']:02x}, Data: {hex_str}"
def main():
if len(sys.argv) < 2:
print("Usage: sudo python3 ptp_monitor.py /dev/hidrawX [--all]")
print("\nOptions:")
print(" --all Show all contacts, even when not touching")
print("\nExample:")
print(" sudo python3 ptp_monitor.py /dev/hidraw4")
sys.exit(1)
device_path = sys.argv[1]
show_all = '--all' in sys.argv
print(f"Opening {device_path}...")
print("Touch the trackpad to see reports (Ctrl+C to exit)")
print("=" * 70)
try:
with open(device_path, 'rb') as f:
report_count = 0
last_report_time = time.time()
while True:
data = f.read(64)
if not data:
time.sleep(0.001)
continue
report = parse_ptp_report(data)
if report:
report_count += 1
current_time = time.time()
delta_ms = (current_time - last_report_time) * 1000
last_report_time = current_time
# Only print if there's activity or we're in verbose mode
if report['type'] == 'input':
if show_all or report['contact_count'] > 0 or report_count % 100 == 0:
print(f"\n[{report_count:5d}] Δ{delta_ms:5.1f}ms")
print(format_report(report, show_all))
else:
# Always show feature reports
print(f"\n[{report_count:5d}] Δ{delta_ms:5.1f}ms")
print(format_report(report, show_all))
except KeyboardInterrupt:
print("\n" + "=" * 70)
print(f"Captured {report_count} reports")
except FileNotFoundError:
print(f"Error: Device {device_path} not found")
print("\nTry: ls -la /sys/class/hidraw/")
sys.exit(1)
except PermissionError:
print(f"Error: Permission denied. Run with sudo:")
print(f" sudo python3 {sys.argv[0]} {device_path}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -4,24 +4,39 @@
#include "precision_trackpad.h"
#include "timer.h"
// Weak functions that can be overridden by keyboard-specific implementations
__attribute__((weak)) void precision_trackpad_init_kb(void) {}
__attribute__((weak)) bool precision_trackpad_task_kb(void) {
return false;
}
// Include the driver implementation
#if defined(PRECISION_TRACKPAD_DRIVER_NAVIGATOR_TRACKPAD)
# include "drivers/sensors/navigator_trackpad_ptp.h"
#endif
// Driver selection (resolved at compile time by build system)
const precision_trackpad_driver_t *precision_trackpad_driver =
&PRECISION_TRACKPAD_DRIVER(PRECISION_TRACKPAD_DRIVER_NAME);
void precision_trackpad_init(void) {
precision_trackpad_init_kb();
if (precision_trackpad_driver && precision_trackpad_driver->init) {
precision_trackpad_driver->init();
}
}
bool precision_trackpad_task(void) {
static uint32_t last_update = 0;
// Throttle updates to 125Hz (8ms) to avoid overwhelming USB
if (timer_elapsed32(last_update) < 8) {
return false;
// No throttling - let the driver control timing
// PTP mode needs responsive updates for gesture recognition
if (precision_trackpad_driver && precision_trackpad_driver->task) {
return precision_trackpad_driver->task();
}
last_update = timer_read32();
return precision_trackpad_task_kb();
return false;
}
void precision_trackpad_set_cpi(uint16_t cpi) {
if (precision_trackpad_driver && precision_trackpad_driver->set_cpi) {
precision_trackpad_driver->set_cpi(cpi);
}
}
uint16_t precision_trackpad_get_cpi(void) {
if (precision_trackpad_driver && precision_trackpad_driver->get_cpi) {
return precision_trackpad_driver->get_cpi();
}
return 0;
}

View File

@@ -5,6 +5,7 @@
#include <stdint.h>
#include <stdbool.h>
#include "precision_trackpad_drivers.h"
/**
* @brief Initialize the precision trackpad
@@ -23,3 +24,20 @@ void precision_trackpad_init(void);
* @return true if trackpad activity occurred, false otherwise
*/
bool precision_trackpad_task(void);
/**
* @brief Set the trackpad CPI (counts per inch)
*
* @param cpi The desired CPI value
*/
void precision_trackpad_set_cpi(uint16_t cpi);
/**
* @brief Get the current trackpad CPI
*
* @return Current CPI value
*/
uint16_t precision_trackpad_get_cpi(void);
// Driver pointer (set at compile time)
extern const precision_trackpad_driver_t *precision_trackpad_driver;

View File

@@ -0,0 +1,20 @@
// Copyright 2025 ZSA Technology Labs, Inc <@zsa>
// Copyright 2025 Florian Didron <fdidron@zsa.io>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <stdint.h>
#include <stdbool.h>
// Precision trackpad driver interface
typedef struct {
void (*init)(void);
bool (*task)(void);
void (*set_cpi)(uint16_t);
uint16_t (*get_cpi)(void);
} precision_trackpad_driver_t;
// Driver registration macros (mirror pointing_device pattern)
#define PRECISION_TRACKPAD_DRIVER_CONCAT(name) name##_precision_trackpad_driver
#define PRECISION_TRACKPAD_DRIVER(name) PRECISION_TRACKPAD_DRIVER_CONCAT(name)

View File

@@ -40,14 +40,16 @@ enum hid_report_ids {
REPORT_ID_JOYSTICK,
REPORT_ID_DIGITIZER,
// PTP trackpad uses its own report ID sequence starting at 0x01
// to match Windows PTP HID specification
REPORT_ID_TRACKPAD = 0x01,
REPORT_ID_TRACKPAD_CONFIG = 0x0A,
REPORT_ID_TRACKPAD_FEATURE = 0x0B,
REPORT_ID_TRACKPAD_MAX_COUNT = 0x0C,
REPORT_ID_TRACKPAD_PTPHQA = 0x0D,
REPORT_ID_COUNT = REPORT_ID_TRACKPAD_PTPHQA
// PTP trackpad uses separate report IDs (per Microsoft PTP specification)
// These don't auto-increment from above, they're on a separate interface
REPORT_ID_TRACKPAD = 0x01, // Input report (multi-touch data)
REPORT_ID_TRACKPAD_MAX_COUNT = 0x02, // Feature: Contact Count Maximum
REPORT_ID_TRACKPAD_CONFIG = 0x03, // Feature: Input Mode configuration
REPORT_ID_TRACKPAD_FEATURE = 0x04, // Feature: Surface/Button switches
REPORT_ID_TRACKPAD_PTPHQA = 0x05, // Feature: Certification blob
// REPORT_ID_COUNT must be the highest ID from the auto-incrementing sequence
REPORT_ID_COUNT = REPORT_ID_DIGITIZER // Highest report ID value
};
#define IS_VALID_REPORT_ID(id) ((id) >= REPORT_ID_ALL && (id) <= REPORT_ID_COUNT)
@@ -260,6 +262,7 @@ typedef struct {
} PACKED report_trackpad_contact_t;
// Main trackpad input report
// Field order per Microsoft PTP spec: [ReportID][Contacts][ScanTime][Count][Buttons]
typedef struct {
uint8_t report_id; // Report ID for trackpad
report_trackpad_contact_t contacts[2]; // Up to 2 simultaneous contacts

View File

@@ -385,8 +385,11 @@ const USB_Descriptor_HIDReport_Datatype_t PROGMEM SharedReport[] = {
#endif
#ifdef PRECISION_TRACKPAD_ENABLE
// Include trackpad dimensions
#include "drivers/sensors/navigator_trackpad_common.h"
// Windows Precision Touchpad (PTP) HID Descriptor
// Based on Microsoft PTP specification
// Follows Microsoft PTP specification exactly
const USB_Descriptor_HIDReport_Datatype_t PROGMEM PrecisionTrackpadReport[] = {
// Touch Pad Input TLC
HID_RI_USAGE_PAGE(8, 0x0D), // Digitizers
@@ -394,7 +397,7 @@ const USB_Descriptor_HIDReport_Datatype_t PROGMEM PrecisionTrackpadReport[] = {
HID_RI_COLLECTION(8, 0x01), // Application
HID_RI_REPORT_ID(8, REPORT_ID_TRACKPAD),
// First finger
// First finger (contact 0)
HID_RI_USAGE(8, 0x22), // Finger
HID_RI_COLLECTION(8, 0x02), // Logical
HID_RI_LOGICAL_MINIMUM(8, 0x00),
@@ -416,21 +419,22 @@ const USB_Descriptor_HIDReport_Datatype_t PROGMEM PrecisionTrackpadReport[] = {
// X/Y position
HID_RI_USAGE_PAGE(8, 0x01), // Generic Desktop
HID_RI_LOGICAL_MINIMUM(8, 0x00),
HID_RI_LOGICAL_MAXIMUM(16, 0x0384), // 900 (actual Cirque range: 1-897)
HID_RI_LOGICAL_MAXIMUM(16, TRACKPAD_LOGICAL_MAX), // 4095 (12-bit resolution)
HID_RI_REPORT_SIZE(8, 0x10),
HID_RI_UNIT_EXPONENT(8, 0x0E), // -2
HID_RI_UNIT_EXPONENT(8, 0x0E), // -2 (hundredths)
HID_RI_UNIT(8, 0x13), // Inch, English Linear
HID_RI_PHYSICAL_MINIMUM(8, 0x00),
HID_RI_PHYSICAL_MAXIMUM(16, 0x009D), // 157 (1.57 inches / 40mm)
HID_RI_PHYSICAL_MAXIMUM(16, TRACKPAD_PHYSICAL_WIDTH), // Physical width in 0.01 inch
HID_RI_REPORT_COUNT(8, 0x01),
HID_RI_USAGE(8, 0x30), // X
HID_RI_INPUT(8, HID_IOF_DATA | HID_IOF_VARIABLE | HID_IOF_ABSOLUTE),
HID_RI_PHYSICAL_MAXIMUM(16, 0x009D), // 157 (1.57 inches / 40mm)
HID_RI_PHYSICAL_MAXIMUM(16, TRACKPAD_PHYSICAL_HEIGHT), // Physical height in 0.01 inch
HID_RI_USAGE(8, 0x31), // Y
HID_RI_INPUT(8, HID_IOF_DATA | HID_IOF_VARIABLE | HID_IOF_ABSOLUTE),
HID_RI_END_COLLECTION(0),
// Second finger (identical structure)
HID_RI_USAGE_PAGE(8, 0x0D), // Digitizers (switch back!)
HID_RI_USAGE(8, 0x22), // Finger
HID_RI_COLLECTION(8, 0x02), // Logical
HID_RI_LOGICAL_MINIMUM(8, 0x00),
@@ -451,22 +455,21 @@ const USB_Descriptor_HIDReport_Datatype_t PROGMEM PrecisionTrackpadReport[] = {
HID_RI_USAGE_PAGE(8, 0x01), // Generic Desktop
HID_RI_LOGICAL_MINIMUM(8, 0x00),
HID_RI_LOGICAL_MAXIMUM(16, 0x0384), // 900 (actual Cirque range: 1-897)
HID_RI_LOGICAL_MAXIMUM(16, TRACKPAD_LOGICAL_MAX), // 4095 (12-bit resolution)
HID_RI_REPORT_SIZE(8, 0x10),
HID_RI_UNIT_EXPONENT(8, 0x0E),
HID_RI_UNIT(8, 0x13),
HID_RI_PHYSICAL_MINIMUM(8, 0x00),
HID_RI_PHYSICAL_MAXIMUM(16, 0x009D), // 157 (1.57 inches / 40mm)
HID_RI_PHYSICAL_MAXIMUM(16, TRACKPAD_PHYSICAL_WIDTH), // Physical width in 0.01 inch
HID_RI_REPORT_COUNT(8, 0x01),
HID_RI_USAGE(8, 0x30), // X
HID_RI_INPUT(8, HID_IOF_DATA | HID_IOF_VARIABLE | HID_IOF_ABSOLUTE),
HID_RI_PHYSICAL_MAXIMUM(16, 0x009D), // 157 (1.57 inches / 40mm)
HID_RI_PHYSICAL_MAXIMUM(16, TRACKPAD_PHYSICAL_HEIGHT), // Physical height in 0.01 inch
HID_RI_USAGE(8, 0x31), // Y
HID_RI_INPUT(8, HID_IOF_DATA | HID_IOF_VARIABLE | HID_IOF_ABSOLUTE),
HID_RI_END_COLLECTION(0),
// Scan Time
HID_RI_USAGE_PAGE(8, 0x0D), // Digitizers
// Scan Time (comes AFTER contacts per Microsoft spec)
HID_RI_UNIT_EXPONENT(8, 0x0C), // -4
HID_RI_UNIT(16, 0x1001), // Seconds
HID_RI_LOGICAL_MINIMUM(8, 0x00),
@@ -475,10 +478,11 @@ const USB_Descriptor_HIDReport_Datatype_t PROGMEM PrecisionTrackpadReport[] = {
HID_RI_PHYSICAL_MAXIMUM(32, 0x0000FFFF),
HID_RI_REPORT_SIZE(8, 0x10),
HID_RI_REPORT_COUNT(8, 0x01),
HID_RI_USAGE_PAGE(8, 0x0D), // Digitizers
HID_RI_USAGE(8, 0x56), // Scan Time
HID_RI_INPUT(8, HID_IOF_DATA | HID_IOF_VARIABLE | HID_IOF_ABSOLUTE),
// Contact Count
// Contact Count (8 bits, not 4!)
HID_RI_USAGE(8, 0x54), // Contact Count
HID_RI_LOGICAL_MAXIMUM(8, 0x7F),
HID_RI_REPORT_SIZE(8, 0x08),