From ed09b1151d0d06e40bc5579493910918464101f7 Mon Sep 17 00:00:00 2001 From: BorisTestov Date: Sun, 21 Sep 2025 17:03:59 +0300 Subject: [PATCH] Add hall effect utils file --- .../keychron/common/hall_effect_utilities.h | 1041 +++++++++++++++++ 1 file changed, 1041 insertions(+) create mode 100644 keyboards/keychron/common/hall_effect_utilities.h diff --git a/keyboards/keychron/common/hall_effect_utilities.h b/keyboards/keychron/common/hall_effect_utilities.h new file mode 100644 index 000000000000..3796e792b882 --- /dev/null +++ b/keyboards/keychron/common/hall_effect_utilities.h @@ -0,0 +1,1041 @@ +#pragma once + +/* Copyright 2025 @ Boris Testov + * + * 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 . + */ + +/* + * Usage in keymap.c: + * 1. Add include at top: #include "hall_effect_utilities.h" + * 2. Use functions in keyboard_post_init_user() + * 3. See individual function comments below for detailed usage examples + */ + +#include "quantum.h" +#include "analog_matrix.h" +#include "profile.h" +#include "action_socd.h" +#include "game_controller_common.h" +#include "xinput_keycodes.h" +#include + +// ============================================================================= +// CONSTANTS AND DEFINES +// ============================================================================= + +// Advanced Mode Constants +// ======================== +// These control the advanced behavior modes for individual keys (from profile.c enum) +#define ADV_MODE_CLEAR 0 // Clear/disable advanced mode (normal key behavior) +#define ADV_MODE_OKMC 1 // Dynamic Key Strokes mode (OKMC - One Key Multi Code) +#define ADV_MODE_GAME_CONTROLLER 2 // Game controller/XInput mode for analog axes and buttons +#define ADV_MODE_TOGGLE 3 // Toggle mode - key acts as a toggle switch + +// Hall Effect Calibration Constants +// ================================== +// These commands are sent to the analog matrix controller via analog_matrix_rx() +#define AMC_CALIBRATE 0x40 // Command to control calibration mode +#define AMC_GET_CALIBRATE_STATE 0x41 // Query current calibration state +#define AMC_GET_CALIBRATED_VALUE 0x42 // Retrieve calibrated values for a key + +// Calibration States +// ================== +// The analog matrix controller maintains these internal states during calibration +#define CALIB_OFF 0 // Calibration inactive - normal operation +#define CALIB_ZERO_TRAVEL_POWER_ON 1 // Auto zero calibration on power-up (not used in manual flow) +#define CALIB_ZERO_TRAVEL_MANUAL 2 // Manual zero travel calibration active + // LED: Solid RED - Keys should be completely released (not pressed at all) +#define CALIB_FULL_TRAVEL_MANUAL 3 // Manual full travel calibration active + // LED: Solid PURPLE - Press keys to maximum travel (turns GREEN when complete) +#define CALIB_SAVE_AND_EXIT 4 // Save calibration data and exit to normal mode (auto-saved) +#define CALIB_CLEAR 5 // Clear all stored calibration data + +// ============================================================================= +// FUNCTION DEFINITIONS +// ============================================================================= + +/* + * ============================================================================ + * BASIC KEY CONFIGURATION FUNCTIONS + * ============================================================================ + */ + +/** + * @brief Set custom actuation point for a specific key + * + * @details + * Configures the actuation distance for an individual key, overriding the global profile setting. + * The system automatically calculates deactuation point as (actuation_point - 3) with a minimum of 0, + * providing 0.3mm hysteresis to prevent bouncing. + * + * @param row Matrix row index starting from 0 (0 = top row, increases downward) + * @param col Matrix column index starting from 0 (0 = leftmost column, increases rightward) + * @param actuation_point Actuation distance in 0.1mm units (10 = 1mm, 5 = 0.5mm, 0 = use global setting) + * + * @note To find valid row/col for a specific key, look at the LED matrix in ansi.c / iso_encoder.c / jis_encoder.c etc. + * where __ means no key exists at that position. If no LED matrix exists, check + * analog_matrix_mask[] and read each binary line right-to-left (1 = valid key, 0 = no key). + * Example: 0b110111111111111 = cols 14,13,11,10,9,8,7,6,5,4,3,2,1,0 exist, col 12 missing. + * @note FALLBACK BEHAVIOR: If actuation_point = 0: Uses global profile setting (cur_prof->global.act_pt) + * @note If actuation_point > maximum allowed value: The key config stores the value as-is, but + * the system will clamp it during processing. Values that exceed physical limits will + * effectively behave as maximum travel. + * + * Example usage: + * ```c + * set_key_actuation_point(0, 1, 3); // "1" key = 0.3mm actuation point + * set_key_actuation_point(0, 2, 10); // "2" key = 1.0mm actuation point + * set_key_actuation_point(1, 0, 0); // Use global setting for this key + * ``` + */ +static inline void set_key_actuation_point(uint8_t row, uint8_t col, uint8_t actuation_point) { + analog_matrix_profile_t *cur_prof = profile_get_current(); + analog_key_config_t *p_key_cfg = &cur_prof->key_config[row][col]; + + // Set custom actuation point (0 = use global setting) + p_key_cfg->act_pt = actuation_point; + + // Apply new key config + update_key_config(row, col); +} + +/** + * @brief Configure rapid trigger sensitivity for a specific key + * + * @details + * Sets up rapid trigger mode for an individual key with custom press and release sensitivities. + * Rapid trigger activates/deactivates keys based on movement sensitivity rather than fixed positions. + * This function automatically sets the key mode to AKM_RAPID, overriding any previous mode. + * + * @param row Matrix row index starting from 0 (0 = top row, increases downward) + * @param col Matrix column index starting from 0 (0 = leftmost column, increases rightward) + * @param press_sensitivity Press sensitivity distance in 0.1mm units (10 = 1mm, 5 = 0.5mm, 0 = use global setting) + * @param release_sensitivity Release sensitivity distance in 0.1mm units (10 = 1mm, 5 = 0.5mm, 0 = use global setting) + * + * @note To find valid row/col for a specific key, look at the LED matrix in ansi.c / iso_encoder.c / jis_encoder.c etc. + * where __ means no key exists at that position. If no LED matrix exists, check + * analog_matrix_mask[] and read each binary line right-to-left (1 = valid key, 0 = no key). + * Example: 0b110111111111111 = cols 14,13,11,10,9,8,7,6,5,4,3,2,1,0 exist, col 12 missing. + * @note FALLBACK BEHAVIOR: If press_sensitivity = 0: Uses global profile setting (cur_prof->global.rpd_trig_sen) + * @note If release_sensitivity = 0: Uses global profile setting. If global release sensitivity + * is also 0, falls back to global press sensitivity (cur_prof->global.rpd_trig_sen) + * @note Values exceeding physical limits will be clamped during processing + * @note All sensitivity values are internally scaled by TRAVEL_SCALE (6) for precision + * @note MODE OVERRIDE: This function automatically sets the key mode to AKM_RAPID, overriding any previous mode + * + * Example usage: + * ```c + * set_key_rapid_trigger(0, 1, 3, 3); // "1" key = 0.3mm press/release sensitivity + * set_key_rapid_trigger(0, 3, 5, 8); // "3" key = 0.5mm press, 0.8mm release + * set_key_rapid_trigger(2, 4, 0, 0); // Use global settings for this key + * ``` + */ +static inline void set_key_rapid_trigger(uint8_t row, uint8_t col, uint8_t press_sensitivity, uint8_t release_sensitivity) { + analog_matrix_profile_t *cur_prof = profile_get_current(); + analog_key_config_t *p_key_cfg = &cur_prof->key_config[row][col]; + + // Set rapid trigger mode + p_key_cfg->mode = AKM_RAPID; + + // Set sensitivities + p_key_cfg->rpd_trig_sen = press_sensitivity; + p_key_cfg->rpd_trig_sen_deact = release_sensitivity; + + // Apply new key config + update_key_config(row, col); +} + +/** + * @brief Configure Last Key Priority SOCD (Simultaneous Opposing Cardinal Directions) + * + * @details + * Sets up conflict resolution where the last pressed key takes priority when both keys are pressed. + * SOCD is an array-based system with limited slots available (SOCD_COUNT = 20 by default). + * This is commonly used for WASD gaming where A+D or W+S conflicts are resolved + * by prioritizing whichever key was pressed most recently. + * + * @param key1_row First key matrix row index starting from 0 (0 = top row, increases downward) + * @param key1_col First key matrix column index starting from 0 (0 = leftmost column, increases rightward) + * @param key2_row Second key matrix row index starting from 0 (0 = top row, increases downward) + * @param key2_col Second key matrix column index starting from 0 (0 = leftmost column, increases rightward) + * @param socd_index SOCD configuration slot index (0 to SOCD_COUNT-1, so 0-19 by default) + * + * @note To find valid row/col for a specific key, look at the LED matrix in ansi.c / iso_encoder.c / jis_encoder.c etc. + * where __ means no key exists at that position. If no LED matrix exists, check + * analog_matrix_mask[] and read each binary line right-to-left (1 = valid key, 0 = no key). + * Example: 0b110111111111111 = cols 14,13,11,10,9,8,7,6,5,4,3,2,1,0 exist, col 12 missing. + * @note SOCD_COUNT is defined in analog_matrix_eeconfig.h (default: 20) + * @note Each socd_index represents one slot in the socd[SOCD_COUNT] array + * @note Think of socd_index as "which conflict resolution rule number you're setting up" + * @note When both keys are pressed simultaneously, the last pressed key takes priority + * + * Example usage: + * ```c + * set_last_key_priority(1, 0, 1, 3, 0); // W vs S keys, using slot 0 + * set_last_key_priority(1, 1, 1, 2, 1); // A vs D keys, using slot 1 + * // You could set up to 18 more pairs using slots 2-19 + * ``` + */ +static inline void set_last_key_priority(uint8_t key1_row, uint8_t key1_col, uint8_t key2_row, uint8_t key2_col, uint8_t socd_index) { + uint8_t cur_prof_idx = profile_get_current_index(); + + if (socd_index >= SOCD_COUNT) return; // Prevent array overflow + + // Configure SOCD entry - this modifies profile_get_current()->socd[socd_index] + uint8_t data[7] = { + cur_prof_idx, // Profile index (which profile to modify) + key1_row, // Key 1 row position + key1_col, // Key 1 col position + key2_row, // Key 2 row position + key2_col, // Key 2 col position + socd_index, // Which array slot to use (0 to SOCD_COUNT-1) + SOCD_PRI_LAST_KEYSTROKE // Last key priority type + }; + + profile_set_socd(data); +} + +/** + * @brief Configure Snap Click (Deeper Travel Priority) SOCD + * + * @details + * Sets up conflict resolution where the key pressed deeper takes priority when both keys are active. + * Snap Click prevents accidental key presses by prioritizing the key pressed deeper. + * During normal use, the deeper pressed key becomes active and blocks the shallower one. + * + * @param key1_row First key matrix row index starting from 0 (0 = top row, increases downward) + * @param key1_col First key matrix column index starting from 0 (0 = leftmost column, increases rightward) + * @param key2_row Second key matrix row index starting from 0 (0 = top row, increases downward) + * @param key2_col Second key matrix column index starting from 0 (0 = leftmost column, increases rightward) + * @param socd_index SOCD configuration slot index (0 to SOCD_COUNT-1, so 0-19 by default) + * @param single_activation true for single activation mode, false for continuous + * + * @note To find valid row/col for a specific key, look at the LED matrix in ansi.c / iso_encoder.c / jis_encoder.c etc. + * where __ means no key exists at that position. If no LED matrix exists, check + * analog_matrix_mask[] and read each binary line right-to-left (1 = valid key, 0 = no key). + * Example: 0b110111111111111 = cols 14,13,11,10,9,8,7,6,5,4,3,2,1,0 exist, col 12 missing. + * @note SOCD_COUNT is defined in analog_matrix_eeconfig.h (default: 20) + * @note Each socd_index represents one slot in the socd[SOCD_COUNT] array + * @note Think of socd_index as "which conflict resolution rule number you're setting up" + * @note single_activation = true: SOCD_PRI_DEEPER_TRAVEL_SINGLE (only deeper key activates) + * @note single_activation = false: SOCD_PRI_DEEPER_TRAVEL (both can activate, deeper has priority) + * @note single_activation = true: When both keys are pressed nearly to the bottom, keeps last active key + * @note single_activation = false: When both keys are pressed nearly to the bottom, both keys remain active + * + * Example usage: + * ```c + * // Set up snap click for adjacent keys that might be accidentally pressed together + * set_snap_click(2, 4, 2, 5, 0, true); // J vs K keys, single activation, slot 0 + * set_snap_click(1, 4, 1, 5, 1, false); // R vs T keys, both active, slot 1 + * // You could set up to 18 more pairs using slots 2-19 + * ``` + */ +static inline void set_snap_click(uint8_t key1_row, uint8_t key1_col, uint8_t key2_row, uint8_t key2_col, uint8_t socd_index, bool single_activation) { + uint8_t cur_prof_idx = profile_get_current_index(); + + if (socd_index >= SOCD_COUNT) return; // Prevent array overflow + + // Configure SOCD entry - this modifies profile_get_current()->socd[socd_index] + uint8_t data[7] = { + cur_prof_idx, // Profile index + key1_row, // Key 1 row position + key1_col, // Key 1 col position + key2_row, // Key 2 row position + key2_col, // Key 2 col position + socd_index, // Which array slot to use (0 to SOCD_COUNT-1) + single_activation ? SOCD_PRI_DEEPER_TRAVEL_SINGLE : SOCD_PRI_DEEPER_TRAVEL // Snap click type + }; + + profile_set_socd(data); +} + +/* + * ============================================================================ + * ADVANCED SOCD MODES + * ============================================================================ + */ + +/** + * @brief Configure Neutral SOCD (Simultaneous Opposing Cardinal Directions) + * + * @details + * Sets up neutral SOCD where both keys are blocked when pressed simultaneously. + * SOCD is an array-based system with limited slots available (SOCD_COUNT = 20 by default). + * This is commonly used in fighting games where pressing both left and right results in no movement + * to prevent directional conflicts. + * + * @param key1_row First key matrix row index starting from 0 (0 = top row, increases downward) + * @param key1_col First key matrix column index starting from 0 (0 = leftmost column, increases rightward) + * @param key2_row Second key matrix row index starting from 0 (0 = top row, increases downward) + * @param key2_col Second key matrix column index starting from 0 (0 = leftmost column, increases rightward) + * @param socd_index SOCD configuration slot index (0 to SOCD_COUNT-1, so 0-19 by default) + * + * @note To find valid row/col for a specific key, look at the LED matrix in ansi.c / iso_encoder.c / jis_encoder.c etc. + * where __ means no key exists at that position. If no LED matrix exists, check + * analog_matrix_mask[] and read each binary line right-to-left (1 = valid key, 0 = no key). + * Example: 0b110111111111111 = cols 14,13,11,10,9,8,7,6,5,4,3,2,1,0 exist, col 12 missing. + * @note SOCD_COUNT is defined in analog_matrix_eeconfig.h (default: 20) + * @note Each socd_index represents one slot in the socd[SOCD_COUNT] array + * @note Think of socd_index as "which conflict resolution rule number you're setting up" + * @note When both keys are pressed simultaneously, neither key activates (neutral state) + * + * Example usage: + * ```c + * set_neutral_socd(2, 1, 2, 3, 0); // A vs D keys neutralize each other, slot 0 + * set_neutral_socd(1, 4, 1, 18, 1); // W vs S keys neutralize each other, slot 1 + * ``` + */ +static inline void set_neutral_socd(uint8_t key1_row, uint8_t key1_col, uint8_t key2_row, uint8_t key2_col, uint8_t socd_index) { + uint8_t cur_prof_idx = profile_get_current_index(); + + if (socd_index >= SOCD_COUNT) return; + + uint8_t data[7] = { + cur_prof_idx, // Profile index + key1_row, // Key 1 row position + key1_col, // Key 1 col position + key2_row, // Key 2 row position + key2_col, // Key 2 col position + socd_index, // Which array slot to use + SOCD_PRI_NEUTRAL // Neutral SOCD type + }; + + profile_set_socd(data); +} + +/** + * @brief Configure Key 1 Priority SOCD (Simultaneous Opposing Cardinal Directions) + * + * @details + * Sets up SOCD where key1 always takes priority when both keys are pressed. + * SOCD is an array-based system with limited slots available (SOCD_COUNT = 20 by default). + * This creates a hierarchy where one key dominates another in conflict situations. + * + * @param key1_row First key matrix row index starting from 0 (0 = top row, increases downward) + * @param key1_col First key matrix column index starting from 0 (0 = leftmost column, increases rightward) + * @param key2_row Second key matrix row index starting from 0 (0 = top row, increases downward) + * @param key2_col Second key matrix column index starting from 0 (0 = leftmost column, increases rightward) + * @param socd_index SOCD configuration slot index (0 to SOCD_COUNT-1, so 0-19 by default) + * + * @note To find valid row/col for a specific key, look at the LED matrix in ansi.c / iso_encoder.c / jis_encoder.c etc. + * where __ means no key exists at that position. If no LED matrix exists, check + * analog_matrix_mask[] and read each binary line right-to-left (1 = valid key, 0 = no key). + * Example: 0b110111111111111 = cols 14,13,11,10,9,8,7,6,5,4,3,2,1,0 exist, col 12 missing. + * @note SOCD_COUNT is defined in analog_matrix_eeconfig.h (default: 20) + * @note Each socd_index represents one slot in the socd[SOCD_COUNT] array + * @note Think of socd_index as "which conflict resolution rule number you're setting up" + * @note When both keys are pressed together: only key1 activates, key2 is blocked + * + * Example usage: + * ```c + * set_key1_priority_socd(1, 2, 2, 2, 0); // W always beats S, slot 0 + * set_key1_priority_socd(2, 1, 2, 3, 1); // A always beats D, slot 1 + * ``` + */ +static inline void set_key1_priority_socd(uint8_t key1_row, uint8_t key1_col, uint8_t key2_row, uint8_t key2_col, uint8_t socd_index) { + uint8_t cur_prof_idx = profile_get_current_index(); + + if (socd_index >= SOCD_COUNT) return; + + uint8_t data[7] = { + cur_prof_idx, // Profile index + key1_row, // Key 1 row position + key1_col, // Key 1 col position + key2_row, // Key 2 row position + key2_col, // Key 2 col position + socd_index, // Which array slot to use + SOCD_PRI_KEY_1 // Key 1 priority type + }; + + profile_set_socd(data); +} + +/** + * @brief Configure Key 2 Priority SOCD (Simultaneous Opposing Cardinal Directions) + * + * @details + * Sets up SOCD where key2 always takes priority when both keys are pressed. + * SOCD is an array-based system with limited slots available (SOCD_COUNT = 20 by default). + * This creates a hierarchy where the second key dominates the first in conflict situations. + * + * @param key1_row First key matrix row index starting from 0 (0 = top row, increases downward) + * @param key1_col First key matrix column index starting from 0 (0 = leftmost column, increases rightward) + * @param key2_row Second key matrix row index starting from 0 (0 = top row, increases downward) + * @param key2_col Second key matrix column index starting from 0 (0 = leftmost column, increases rightward) + * @param socd_index SOCD configuration slot index (0 to SOCD_COUNT-1, so 0-19 by default) + * + * @note To find valid row/col for a specific key, look at the LED matrix in ansi.c / iso_encoder.c / jis_encoder.c etc. + * where __ means no key exists at that position. If no LED matrix exists, check + * analog_matrix_mask[] and read each binary line right-to-left (1 = valid key, 0 = no key). + * Example: 0b110111111111111 = cols 14,13,11,10,9,8,7,6,5,4,3,2,1,0 exist, col 12 missing. + * @note SOCD_COUNT is defined in analog_matrix_eeconfig.h (default: 20) + * @note Each socd_index represents one slot in the socd[SOCD_COUNT] array + * @note Think of socd_index as "which conflict resolution rule number you're setting up" + * @note When both keys are pressed together: only key2 activates, key1 is blocked + * + * Example usage: + * ```c + * set_key2_priority_socd(1, 2, 2, 2, 0); // S always beats W, slot 0 + * set_key2_priority_socd(2, 1, 2, 3, 1); // D always beats A, slot 1 + * ``` + */ +static inline void set_key2_priority_socd(uint8_t key1_row, uint8_t key1_col, uint8_t key2_row, uint8_t key2_col, uint8_t socd_index) { + uint8_t cur_prof_idx = profile_get_current_index(); + + if (socd_index >= SOCD_COUNT) return; + + uint8_t data[7] = { + cur_prof_idx, // Profile index + key1_row, // Key 1 row position + key1_col, // Key 1 col position + key2_row, // Key 2 row position + key2_col, // Key 2 col position + socd_index, // Which array slot to use + SOCD_PRI_KEY_2 // Key 2 priority type + }; + + profile_set_socd(data); +} + +/** + * @brief Configure Dynamic Key Strokes (DKS) / One Key Multi Code (OKMC) + * + * @details + * Dynamic Key Strokes (DKS) allows one key to perform up to 4 different actions based on travel depth. + * This is also known as One Key Multi Code (OKMC) in the codebase. + * OKMC is an array-based system with limited slots available (OKMC_COUNT = 20 by default). + * + * Each action parameter is a 4-bit value where each bit controls one travel zone: + * - Bit 0 (0x01): Add key on shallow_act (shallow press) + * - Bit 1 (0x02): Add key on shallow_deact (shallow release) + * - Bit 2 (0x04): Add key on deep_act (deep press) + * - Bit 3 (0x08): Add key on deep_deact (deep release) + * + * IMPORTANT: OKMC uses ADDITIVE ACCUMULATION behavior: + * - Keys get ADDED at each travel zone (never individually removed) + * - Adding the same key multiple times has no effect (it's already active) + * - ALL keys are released together when the physical key fully releases + * + * @param row Key matrix row index starting from 0 (0 = top row, increases downward) + * @param col Key matrix column index starting from 0 (0 = leftmost column, increases rightward) + * @param okmc_index OKMC configuration slot index (0 to OKMC_COUNT-1, so 0-19 by default) + * @param shallow_act Shallow actuation point in 0.1mm units (when to trigger shallow actions) + * @param shallow_deact Shallow deactuation point in 0.1mm units (when to release shallow actions) + * @param deep_act Deep actuation point in 0.1mm units (when to trigger deep actions) + * @param deep_deact Deep deactuation point in 0.1mm units (when to release deep actions) + * @param keycode1 First QMK keycode to send (KC_A, KC_LSFT, etc., 0 = no action) + * @param keycode2 Second QMK keycode to send (KC_A, KC_LSFT, etc., 0 = no action) + * @param keycode3 Third QMK keycode to send (KC_A, KC_LSFT, etc., 0 = no action) + * @param keycode4 Fourth QMK keycode to send (KC_A, KC_LSFT, etc., 0 = no action) + * @param action1 Action configuration for keycode1 (4-bit value controlling travel zones, 0 = skip) + * @param action2 Action configuration for keycode2 (4-bit value controlling travel zones, 0 = skip) + * @param action3 Action configuration for keycode3 (4-bit value controlling travel zones, 0 = skip) + * @param action4 Action configuration for keycode4 (4-bit value controlling travel zones, 0 = skip) + * + * @note To find valid row/col for a specific key, look at the LED matrix in ansi.c / iso_encoder.c / jis_encoder.c etc. + * where __ means no key exists at that position. If no LED matrix exists, check + * analog_matrix_mask[] and read each binary line right-to-left (1 = valid key, 0 = no key). + * Example: 0b110111111111111 = cols 14,13,11,10,9,8,7,6,5,4,3,2,1,0 exist, col 12 missing. + * @note OKMC_COUNT is defined in analog_matrix_eeconfig.h (default: 20) + * @note Each okmc_index represents one slot in the okmc[OKMC_COUNT] array + * @note Think of okmc_index as "which DKS profile number you're setting up" + * + * Common action patterns: + * - 0x01 = Add key on shallow press + * - 0x02 = Add key on shallow release + * - 0x04 = Add key on deep press + * - 0x08 = Add key on deep release + * - 0 = Skip this keycode (no action) + * + * Example usage: + * ```c + * // Gaming example: Walk on shallow, run on deep press + * set_dynamic_key_strokes(2, 3, 0, // W key: Row 2, Col 3, OKMC slot 0 + * 15, 12, 30, 25, // Travel: 1.5mm shallow act, 1.2mm shallow deact, 3.0mm deep act, 2.5mm deep deact + * KC_W, KC_LSFT, 0, 0, // W key for walking, Shift for running + * 0x01, 0x04, 0, 0); // W on shallow press, Shift on deep press, skip unused slots + * + * // Sequential accumulation: Each keycode gets added at different travel zones + * // SHALLOW PRESS → SHALLOW RELEASE → DEEP PRESS → DEEP RELEASE + * // A → A+B → A+B+C → A+B+C+D → all released + * set_dynamic_key_strokes(1, 5, 1, // Different key, OKMC slot 1 + * 10, 7, 25, 20, // Different travel points + * KC_A, KC_B, KC_C, KC_D, // 4 different keycodes + * 0x01, 0x02, 0x04, 0x08); // Add at: shallow press, shallow release, deep press, deep release + * + * // Keychron marketing example - shows actual behavior vs accumulation + * // SHALLOW PRESS → DEEP PRESS → DEEP RELEASE → SHALLOW RELEASE + * // C → C+LCTL → C+LCTL+H → C+LCTL+H+F → all released + * // Note: C+LCTL gets replaced by H, then H gets replaced by F (not true accumulation) + * set_dynamic_key_strokes(1, 5, 1, // Different key, OKMC slot 1 + * 10, 7, 25, 20, // Different travel points + * KC_C, KC_LCTL, KC_H, KC_F, // 4 different keycodes for different depths + * 0x01, 0x04, 0x08, 0x02); // Add at: shallow press, deep press, deep release, shallow release + * ``` + */ +static inline void set_dynamic_key_strokes(uint8_t row, uint8_t col, uint8_t okmc_index, + uint8_t shallow_act, uint8_t shallow_deact, uint8_t deep_act, uint8_t deep_deact, + uint16_t keycode1, uint16_t keycode2, uint16_t keycode3, uint16_t keycode4, + uint8_t action1, uint8_t action2, uint8_t action3, uint8_t action4) { + uint8_t cur_prof_idx = profile_get_current_index(); + + if (okmc_index >= OKMC_COUNT) return; // Prevent array overflow + + // Configure OKMC entry - this modifies profile_get_current()->okmc[okmc_index] + uint8_t data[25] = { + cur_prof_idx, // [0] Profile index + ADV_MODE_OKMC, // [1] Mode = OKMC/DKS + row, // [2] Key row position + col, // [3] Key col position + okmc_index, // [4] Which OKMC slot to use (0 to OKMC_COUNT-1) + shallow_act, // [5] Shallow actuation point + shallow_deact, // [6] Shallow deactuation point + deep_act, // [7] Deep actuation point + deep_deact, // [8] Deep deactuation point + // Keycodes (4 x 2 bytes = 8 bytes) [9-16] + keycode1 & 0xFF, (keycode1 >> 8) & 0xFF, // [9-10] Keycode 1 + keycode2 & 0xFF, (keycode2 >> 8) & 0xFF, // [11-12] Keycode 2 + keycode3 & 0xFF, (keycode3 >> 8) & 0xFF, // [13-14] Keycode 3 + keycode4 & 0xFF, (keycode4 >> 8) & 0xFF, // [15-16] Keycode 4 + // Actions (4 x 2 bytes = 8 bytes) [17-24] + // Each okmc_action_t: shallow_act:4, shallow_deact:4, deep_act:4, deep_deact:4 + // Convert bit flags to 4-bit fields: bit 0->shallow_act, bit 1->shallow_deact, bit 2->deep_act, bit 3->deep_deact + ((action1 & 0x01) ? 0x02 : 0) | (((action1 & 0x02) ? 0x02 : 0) << 4), // [17] shallow fields for action1 + ((action1 & 0x04) ? 0x02 : 0) | (((action1 & 0x08) ? 0x02 : 0) << 4), // [18] deep fields for action1 + ((action2 & 0x01) ? 0x02 : 0) | (((action2 & 0x02) ? 0x02 : 0) << 4), // [19] shallow fields for action2 + ((action2 & 0x04) ? 0x02 : 0) | (((action2 & 0x08) ? 0x02 : 0) << 4), // [20] deep fields for action2 + ((action3 & 0x01) ? 0x02 : 0) | (((action3 & 0x02) ? 0x02 : 0) << 4), // [21] shallow fields for action3 + ((action3 & 0x04) ? 0x02 : 0) | (((action3 & 0x08) ? 0x02 : 0) << 4), // [22] deep fields for action3 + ((action4 & 0x01) ? 0x02 : 0) | (((action4 & 0x02) ? 0x02 : 0) << 4), // [23] shallow fields for action4 + ((action4 & 0x04) ? 0x02 : 0) | (((action4 & 0x08) ? 0x02 : 0) << 4) // [24] deep fields for action4 + }; + + profile_set_adv_mode(data); +} + +/* + * ============================================================================ + * TOGGLE MODE FUNCTIONS + * ============================================================================ + */ + +/** + * @brief Set a key to toggle mode (AKM_TOGGLE) + * + * @details + * Sets a key to toggle mode where the key acts like a toggle switch. + * This is different from normal keys that are only active while being pressed down. + * + * Toggle behavior: + * - First press: Key becomes active and stays active + * - Second press: Key becomes inactive and stays inactive + * - Key remains in its current state until pressed again + * + * Toggle mode is useful for: + * - Custom Caps Lock behavior + * - Gaming modes that need to stay active + * - Modifier keys you want to "stick" (like software-based Sticky Keys) + * - Mode switches in applications + * + * @param row Key matrix row index starting from 0 (0 = top row, increases downward) + * @param col Key matrix column index starting from 0 (0 = leftmost column, increases rightward) + * + * @note To find valid row/col for a specific key, look at the LED matrix in ansi.c / iso_encoder.c / jis_encoder.c etc. + * where __ means no key exists at that position. If no LED matrix exists, check + * analog_matrix_mask[] and read each binary line right-to-left (1 = valid key, 0 = no key). + * Example: 0b110111111111111 = cols 14,13,11,10,9,8,7,6,5,4,3,2,1,0 exist, col 12 missing. + * @note Toggle mode configurations are per-profile and persist in EEPROM + * @note You can have different toggle settings for different HE profiles + * + * Example usage: + * ```c + * set_key_toggle_mode(2, 0); // Make Caps Lock a proper toggle + * set_key_toggle_mode(1, 0); // Make Tab a toggle for gaming mode + * set_key_toggle_mode(3, 4); // Make a modifier key sticky + * ``` + */ +static inline void set_key_toggle_mode(uint8_t row, uint8_t col) { + uint8_t cur_prof_idx = profile_get_current_index(); + + // Configure key for toggle mode + uint8_t data[5] = { + cur_prof_idx, // [0] Profile index + ADV_MODE_TOGGLE, // [1] Mode = Toggle mode + row, // [2] Key row position + col, // [3] Key col position + 0 // [4] Index (unused for toggle mode) + }; + + profile_set_adv_mode(data); +} + +/* + * ============================================================================ + * ANALOG GAME CONTROLLER FUNCTIONS + * ============================================================================ + */ + +/** + * @brief Set a key to game controller mode (XInput button or axis) + * + * @details + * Sets a key to game controller mode, allowing it to send XInput signals instead of normal keyboard keycodes. + * This is useful for gaming applications that expect gamepad input. + * + * The analog sticks provide proportional output based on key travel distance. + * Deeper presses result in larger analog stick deflection values. + * + * Available XInput Button Keycodes: + * - XB_A, XB_B, XB_X, XB_Y - Face buttons + * - XB_LB, XB_RB - Shoulder buttons + * - XB_VIEW, XB_MEMU - View/Menu buttons + * - XB_L3, XB_R3 - Stick press buttons + * - XB_UP, XB_DOWN, XB_LEFT, XB_RGHT - D-pad buttons + * - XB_XBOX - Xbox button + * - XB_LT, XB_RT - Left/Right triggers (analog) + * + * Available XInput Analog Stick Keycodes: + * - LS_LEFT, LS_RGHT, LS_UP, LS_DOWN - Left stick directions + * - RS_LEFT, RS_RGHT, RS_UP, RS_DOWN - Right stick directions + * + * @param row Key matrix row index starting from 0 (0 = top row, increases downward) + * @param col Key matrix column index starting from 0 (0 = leftmost column, increases rightward) + * @param xinput_keycode XInput keycode from xinput_keycodes.h + * + * @note To find valid row/col for a specific key, look at the LED matrix in ansi.c / iso_encoder.c / jis_encoder.c etc. + * where __ means no key exists at that position. If no LED matrix exists, check + * analog_matrix_mask[] and read each binary line right-to-left (1 = valid key, 0 = no key). + * Example: 0b110111111111111 = cols 14,13,11,10,9,8,7,6,5,4,3,2,1,0 exist, col 12 missing. + * @note Game controller configurations are per-profile and persist in EEPROM + * @note You can have different controller mappings for different HE profiles + * + * Example usage: + * ```c + * // Gaming setup: WASD as left stick, arrow keys as right stick + * set_key_game_controller(1, 2, LS_UP); // W key -> Left stick up + * set_key_game_controller(2, 0, LS_LEFT); // A key -> Left stick left + * set_key_game_controller(2, 2, LS_DOWN); // S key -> Left stick down + * set_key_game_controller(2, 3, LS_RGHT); // D key -> Left stick right + * + * // Face buttons + * set_key_game_controller(1, 5, XB_Y); // T key -> Y button + * set_key_game_controller(1, 6, XB_X); // Y key -> X button + * set_key_game_controller(1, 7, XB_B); // U key -> B button + * set_key_game_controller(1, 8, XB_A); // I key -> A button + * + * // Triggers (analog based on key travel) + * set_key_game_controller(0, 1, XB_LT); // "1" key -> Left trigger + * set_key_game_controller(0, 2, XB_RT); // "2" key -> Right trigger + * ``` + */ +static inline void set_key_game_controller(uint8_t row, uint8_t col, uint16_t xinput_keycode) { + uint8_t cur_prof_idx = profile_get_current_index(); + + // Extract axis index from xinput_keycode + // XInput keycodes are encoded as: ((axis_index << 3 | AKM_GAMEPAD) << 2) | (mode & 3) + uint8_t axis_index = (xinput_keycode >> 5) & 0x1F; // Extract bits 5-9 (axis index) + + // Configure key for game controller mode + uint8_t data[5] = { + cur_prof_idx, // [0] Profile index + ADV_MODE_GAME_CONTROLLER, // [1] Mode = Game controller mode + row, // [2] Key row position + col, // [3] Key col position + axis_index // [4] Axis/button index + }; + + profile_set_adv_mode(data); +} + +/** + * @brief Set analog sensitivity curve for game controller axes + * + * @details + * Configures the analog sensitivity curve that determines how key travel distance + * maps to analog stick/trigger values for game controller mode. The curve is defined + * by 4 control points that create 3 linear segments, with (0,0) as the fixed origin. + * + * @param x1 First control point X coordinate (key travel in 0.1mm units, 0-40) + * @param y1 First control point Y coordinate (output intensity 0-127) + * @param x2 Second control point X coordinate (key travel in 0.1mm units, 0-40) + * @param y2 Second control point Y coordinate (output intensity 0-127) + * @param x3 Third control point X coordinate (key travel in 0.1mm units, 0-40) + * @param y3 Third control point Y coordinate (output intensity 0-127) + * @param x4 Fourth control point X coordinate (key travel in 0.1mm units, 0-40) + * @param y4 Fourth control point Y coordinate (output intensity 0-127) + * + * @note - Points must be in ascending X order: x1 < x2 < x3 < x4 + * @note - X values: 0-40 representing 0.0mm to 4.0mm travel distance + * @note - Y values: 0-127 where 127 = maximum analog output + * @note - Curve applies globally to all analog game controller axes and persists in EEPROM + * @note - By default curve settings are (0, 0), (10,31), (30,95), (40,127) + * + * Example usage: + * ```c + * //Linear curve (1:1 mapping) - smooth and predictable response + * set_analog_sensitivity_curve(10, 32, 20, 64, 30, 96, 40, 127); + * + * // Sensitive curve (more output for less travel) - great for racing games + * set_analog_sensitivity_curve(5, 40, 15, 80, 25, 110, 40, 127); + * + * // Gaming curve with dead zone - prevents accidental inputs + * set_analog_sensitivity_curve(8, 0, 12, 30, 25, 100, 40, 127); + * + * // Typing-friendly curve - minimal sensitivity until deeper travel + * set_analog_sensitivity_curve(12, 10, 20, 30, 30, 70, 40, 127); + * ``` + */ +static inline void set_analog_sensitivity_curve(uint8_t x1, uint8_t y1, uint8_t x2, uint8_t y2, uint8_t x3, uint8_t y3, uint8_t x4, uint8_t y4) { + // Validate curve points are in ascending order + if (x1 >= x2 || x2 >= x3 || x3 >= x4) { + return; // Invalid curve - points must be in ascending X order + } + + // Create curve points array (point 0 is always {0, 0}) + point_t curve_points[4] = { + {x1, y1}, // Point 1: First control point + {x2, y2}, // Point 2: Second control point + {x3, y3}, // Point 3: Third control point + {x4, y4} + }; + + // Set the new curve + game_controller_set_curve(curve_points); +} + +/* + * ============================================================================ + * HE PROFILE MANAGEMENT FUNCTIONS + * ============================================================================ + * + * These functions allow switching between 3 different HE profiles (0, 1, 2). + * Check your profiles in profiles.c. + * + * Note: some profiles can use XInput instead of normal keys. + * If you want to override this, copy profiles.c file to your keymap, change XInput keys (e.g. XB_XBOX) to 0 + * and add SRC += profiles.c to rules.mk + * + * Global Profile Modes (in profiles.c profile_gobal_mode array): + * The profile_gobal_mode array defines the default key behavior for each profile: + * AKM_REGULAR: Static actuation points - keys activate at fixed travel distance (default: 20 = 2.0mm) + * AKM_RAPID: Rapid trigger mode - keys activate/deactivate based on movement sensitivity (default: 4 = 0.4mm) + * AKM_DKS: Dynamic Key Strokes mode - keys perform different actions at different travel depths + * AKM_GAMEPAD: Game controller mode - keys send XInput signals (XB_A/B/X/Y buttons, LS_x/RS_x analog sticks, XB_LT/RT triggers) + * AKM_TOGGLE: Toggle mode - keys act as toggle switches (press once = on, press again = off) + * Individual keys can override the global mode using functions in this file. + * Defaults defined in analog_matrix.h: DEFAULT_ACTUATION_POINT=20, DEFAULT_RAPID_TRIGGER_SENSITIVITY=4 + * + * DRY Configuration Pattern: + * Instead of separate functions for each profile, use the switching approach: + * uint8_t original_profile = get_current_he_profile(); + * + * switch_he_profile(0); // Configure gaming profile + * set_key_actuation_point(2, 0, 5); // W key: 0.5mm + * set_key_rapid_trigger(2, 0, 1, 1); // Very sensitive + * + * switch_he_profile(1); // Configure typing profile + * set_key_actuation_point(2, 0, 15); // W key: 1.5mm + * set_key_rapid_trigger(2, 0, 3, 5); // Less sensitive + * + * switch_he_profile(original_profile); // Restore original + */ + +/** + * @brief Switch to a different Hall Effect profile + * + * @details + * Switches to a different Hall Effect profile (0-2). + * All subsequent configuration functions will operate on this profile. + * This is similar to QMK layer switching but for HE analog settings. + * Like TO(1) which switches to a layer persistently, this switches the active HE profile. + * + * @param profile_index HE profile to switch to (0, 1, or 2) + * + * @note Profile index must be between 0 and PROFILE_COUNT-1 + * @note LED indication is enabled when switching profiles + * + * Example runtime switching: + * ```c + * enum custom_keycodes { + * HE_PROF_0 = SAFE_RANGE, // Switch to HE profile 0 + * HE_PROF_1, // Switch to HE profile 1 + * HE_PROF_2, // Switch to HE profile 2 + * HE_CYCLE // Cycle through HE profiles + * }; + * + * bool process_record_user(uint16_t keycode, keyrecord_t *record) { + * switch (keycode) { + * case HE_PROF_0: + * if (record->event.pressed) { + * switch_he_profile(0); // Switch to HE profile 0 + * } + * return false; + * case HE_PROF_1: + * if (record->event.pressed) { + * switch_he_profile(1); // Switch to HE profile 1 + * } + * return false; + * case HE_PROF_2: + * if (record->event.pressed) { + * switch_he_profile(2); // Switch to HE profile 2 + * } + * return false; + * case HE_CYCLE: + * if (record->event.pressed) { + * cycle_he_profile(); // Cycle through profiles + * } + * return false; + * } + * return true; + * } + * + * // Configure different actuation points for "1" key on all profiles + * void keyboard_post_init_user(void) { + * uint8_t original_profile = get_current_he_profile(); + * + * // Profile 0: Gaming (very sensitive) + * switch_he_profile(0); + * set_key_actuation_point(0, 1, 3); // "1" key = 0.3mm + * + * // Profile 1: Typing (normal) + * switch_he_profile(1); + * set_key_actuation_point(0, 1, 15); // "1" key = 1.5mm + * + * // Profile 2: Heavy typing (less sensitive) + * switch_he_profile(2); + * set_key_actuation_point(0, 1, 35); // "1" key = 3.5mm + * + * switch_he_profile(original_profile); // Restore original profile + * } + * ``` + */ +static inline void switch_he_profile(uint8_t profile_index) { + if (profile_index >= PROFILE_COUNT) { + return; // Invalid profile index + } + + profile_select(profile_index, true); // true = LED indication when switching; false = no LED indication +} + +/** + * @brief Get current Hall Effect profile index + * + * @details + * Returns the currently active HE profile index (0-2). + * + * Useful for: + * - Saving current profile before configuration + * - Displaying current profile on OLED/LCD + * - Conditional logic based on active profile + * + * @return Current HE profile index (0 to PROFILE_COUNT-1) + */ +static inline uint8_t get_current_he_profile(void) { + return profile_get_current_index(); +} + +/** + * @brief Cycle to next Hall Effect profile + * + * @details + * Cycles to the next HE profile in sequence: 0 → 1 → 2 → 0 → ... + * Convenient for a single key that cycles through all available profiles. + * + * @note LED indication is enabled when switching profiles + */ +static inline void cycle_he_profile(void) { + uint8_t current = profile_get_current_index(); + uint8_t next = (current + 1) % PROFILE_COUNT; + profile_select(next, true); // true = LED indication when switching; false = no LED indication +} + +/* + * ============================================================================ + * HALL EFFECT CALIBRATION FUNCTIONS + * ============================================================================ + * + * CALIBRATION PROCESS OVERVIEW: + * 1. Start with CALIB_ZERO_TRAVEL_MANUAL - ensure all keys are in rest position (not pressed) + * 2. Controller automatically transitions to CALIB_FULL_TRAVEL_MANUAL after zero phase + * 3. Manually press all keys to maximum travel depth - LED changes from PURPLE to GREEN when done + * 4. Calibration data is automatically saved when complete - no manual save needed + * + * IMPORTANT NOTES: + * - During calibration, keys are unresponsive and will not print anything + * - User must manually press each key during the calibration phases + * - If LED stays RED during calibration, one or more keys failed to calibrate properly + * - If calibration fails (LED remains red), restart calibration by relaunching the keyboard + * - No manual stop calibration function is implemented because keys are unresponsive during calibration + */ + +/** + * @brief Start Hall Effect calibration process + * + * @details + * Initiates the manual two-phase calibration sequence: + * 1. Zero travel calibration (LED: RED) - ensure all keys are released + * 2. Full travel calibration (LED: PURPLE->GREEN) - manually press all keys to maximum depth + * + * WARNING: During calibration, keys become unresponsive and will not type anything! + * User must manually press keys during each calibration phase. + * The keyboard LED indicates the current phase and completion status. + * + * @note Keys become unresponsive during calibration + * @note LED color indicates calibration phase: RED = zero phase, PURPLE = full phase, GREEN = complete + * @note Includes 1 second delay to ensure all keys are released before starting + * + * Example usage: + * ```c + * case KC_CALIB_START: + * if (record->event.pressed) { + * start_calibration(); + * } + * return false; + * ``` + */ +static inline void start_calibration(void) { + wait_ms(1000); // 1 second delay to ensure all keys are released before calibration + uint8_t data[4] = {0xA9, AMC_CALIBRATE, CALIB_ZERO_TRAVEL_MANUAL, 0}; + analog_matrix_rx(data, 4); +} + +/** + * @brief Get current calibration state + * + * @details + * Returns the current calibration state from the analog matrix controller. + * Useful for monitoring calibration progress or implementing custom calibration logic. + * + * @return Current calibration state: + * - CALIB_OFF: Normal operation + * - CALIB_ZERO_TRAVEL_MANUAL: Zero calibration active (LED: RED) + * - CALIB_FULL_TRAVEL_MANUAL: Full calibration active (LED: PURPLE->GREEN) + * + * Example usage: + * ```c + * uint8_t state = get_calibration_state(); + * if (state == CALIB_OFF) { + * // Calibration complete or not active + * } + * ``` + */ +static inline uint8_t get_calibration_state(void) { + uint8_t data[20] = {0xA9, AMC_GET_CALIBRATE_STATE, 0}; + analog_matrix_rx(data, 20); + return data[3]; +} + +/** + * @brief Check if calibration is currently active + * + * @details + * Returns true if calibration is currently in progress, false otherwise. + * Convenient wrapper around get_calibration_state() for simple checks. + * + * @return true if calibration is active, false if normal operation + * + * Example usage: + * ```c + * if (is_calibrating()) { + * // Add your logic during calibration process here + * return; + * } + * ``` + */ +static inline bool is_calibrating(void) { + return (get_calibration_state() != CALIB_OFF); +} + +/** + * @brief Get calibrated values for a specific key + * + * @details + * Retrieves the stored calibration data for a single key position. + * + * ADC Value Notes: + * - Higher ADC values = magnet closer to sensor + * - Zero travel (key at rest) should have HIGHER ADC values than full travel + * - Full travel (key pressed) should have LOWER ADC values + * - If zero_travel <= full_travel, the key may not be calibrated correctly + * + * @param row Key matrix row index starting from 0 (0 = top row, increases downward) + * @param col Key matrix column index starting from 0 (0 = leftmost column, increases rightward) + * @param zero_travel Pointer to store the zero travel ADC value (key at rest position) + * @param full_travel Pointer to store the full travel ADC value (key fully pressed) + * + * @return true if calibration data exists and was retrieved successfully, false otherwise + * + * @note To find valid row/col for a specific key, look at the LED matrix in ansi.c / iso_encoder.c / jis_encoder.c etc. + * where __ means no key exists at that position. If no LED matrix exists, check + * analog_matrix_mask[] and read each binary line right-to-left (1 = valid key, 0 = no key). + * Example: 0b110111111111111 = cols 14,13,11,10,9,8,7,6,5,4,3,2,1,0 exist, col 12 missing. + * + * Example usage: + * ```c + * uint16_t zero, full; + * if (get_key_calibration(1, 2, &zero, &full)) { // W key + * // Successfully got calibration data + * uint16_t range = zero - full; // Calculate travel range + * if (range > 100) { + * // Do something here + * } + * } else { + * // Key not calibrated or invalid position or default values used + * } + * ``` + */ +static inline bool get_key_calibration(uint8_t row, uint8_t col, uint16_t *zero_travel, uint16_t *full_travel) { + if (row >= MATRIX_ROWS || col >= MATRIX_COLS) { + return false; + } + + uint8_t data[20] = {0xA9, AMC_GET_CALIBRATED_VALUE, row, col, 0}; + analog_matrix_rx(data, 20); + + if (data[4] == 0) { + *zero_travel = (data[6] << 8) | data[5]; // Little endian conversion + *full_travel = (data[8] << 8) | data[7]; // Little endian conversion + return true; + } + + return false; +} + +/** + * @brief Clear all calibration data + * + * @details + * Erases all stored calibration values and resets keys to factory defaults. + * Keys will continue to work normally with default calibration values. + * New calibration will overwrite old data, so clearing is optional. + * + * Usage: Use only for troubleshooting calibration issues. + * + * @note Keys continue to work with factory default calibration values after clearing + * @note New calibration will overwrite old data automatically + * + * Example usage: + * ```c + * case KC_CALIB_CLEAR: + * if (record->event.pressed) { + * clear_calibration_data(); + * } + * return false; + * ``` + */ +static inline void clear_calibration_data(void) { + uint8_t data[4] = {0xA9, AMC_CALIBRATE, CALIB_CLEAR, 0}; + analog_matrix_rx(data, 4); +}