Skip to content

Instantly share code, notes, and snippets.

@BorisTestov
Last active October 14, 2025 22:46
Show Gist options
  • Select an option

  • Save BorisTestov/2d96e030f31fc903820b17d309beeaef to your computer and use it in GitHub Desktop.

Select an option

Save BorisTestov/2d96e030f31fc903820b17d309beeaef to your computer and use it in GitHub Desktop.
Hall Effect Utilities for QMK Keychron Keyboards
#pragma once
/*
* Hall Effect Utilities for QMK
*
* DISCLAIMER:
* This code was written by Boris Testov for personal use on K6 HE keyboard.
* You are free to use, modify, and distribute this code for any purpose, including
* commercial use. However, this code is provided "AS IS" without any warranty of
* any kind, express or implied. The author makes no guarantees about its
* functionality, reliability, or suitability for any particular purpose.
* Use at your own risk.
*
* Note: This code covers core analog matrix functionality but may not include every advanced feature available in the system.
*
* If you find this helpful, consider buying me a coffee:
* https://buymeacoffee.com/boristestov
*
* Usage in keymap.c:
* 1. Add include at top: #include "he_utils.h"
* 2. Use functions in keyboard_post_init_user()
* 3. See individual function comments below for detailed 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 <stdio.h>
#include <stdint.h>
// =============================================================================
// 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
}
/*
* ============================================================================
* DEBUGGING FUNCTIONS (CONSOLE)
* ============================================================================
*
* Console debugging functions for monitoring HE keyboard behavior in real-time.
*
* HOW TO ENABLE:
* 1. Set CONSOLE_ENABLE = yes in rules.mk
* 2. Use QMK Toolbox console tab or hid_listen tool to view output
* 3. Debug messages appear as readable text with timestamps
*
* EXAMPLE IMPLEMENTATIONS (see keymap.c for full examples):
*
* 1. Profile switching debugging in process_record_user():
* case HE_PROF_1:
* if (record->event.pressed) {
* switch_he_profile(1);
* send_debug_message("Profile -> 1"); // Shows profile changes
* }
* return false;
*
* 2. Key travel debugging for specific keys:
* if (keycode == KC_W) {
* if (record->event.pressed) {
* send_debug_message("W key pressed 2.0mm");
* debug_keyevent(&record->event); // Full key event details
* } else {
* send_debug_message("W key released");
* debug_keyevent(&record->event);
* }
* }
*
* 3. Initialization debugging in keyboard_post_init_user():
* send_debug_message("HE Init started");
* send_debug_message("WASD -> analog stick");
* send_debug_message("A vs D: neutral SOCD");
* send_debug_message("HE Init complete");
*
* 4. SOCD debugging (add to process_record_user for A/D keys):
* if (keycode == KC_A || keycode == KC_D) {
* if (record->event.pressed) {
* send_debug_message(keycode == KC_A ? "A pressed" : "D pressed");
* }
* }
*
* 5. Function call debugging:
* set_key_actuation_point(2, 0, 15); // Set A key to 1.5mm
* send_debug_message("A key actuation -> 1.5mm");
*
* OUTPUT EXAMPLES:
* DEBUG: Profile -> 1
* DEBUG: W key pressed 2.0mm
* KEY: r1 c2 PRESS
* TIME: 12345 TYPE: KEY
* DEBUG: A vs D: neutral SOCD
*
* NOTE: If you get "There are not enough available endpoints" error when enabling console,
* disable one or more of these features in rules.mk (set to 'no'):
* MOUSEKEY_ENABLE, EXTRAKEY_ENABLE, NKRO_ENABLE, MIDI_ENABLE,
* SERIAL_ENABLE, STENO_ENABLE, RAW_ENABLE, VIRTSER_ENABLE
* Keep CONSOLE_ENABLE = yes for debugging
*/
#ifdef CONSOLE_ENABLE
/**
* @brief Send debug message via console
*
* @details
* Sends a debug message to the console output with "DEBUG: " prefix.
* Requires CONSOLE_ENABLE=yes in rules.mk to function.
*
* @param message Debug message string to send to console
*
* @note Only works when CONSOLE_ENABLE is enabled in rules.mk
* @note Use QMK Toolbox console tab or hid_listen tool to view output
*/
static inline void send_debug_message(const char* message) {
uprintf("DEBUG: %s\n", message);
}
/**
* @brief Debug entire keyevent_t structure
*
* @details
* Outputs detailed information about a key event to the console, including
* key position, press/release state, timestamp, and event type.
* Requires CONSOLE_ENABLE=yes in rules.mk to function.
*
* Output format:
* - "KEY: r{row} c{col} {PRESS|RELEASE}"
* - "TIME: {timestamp} TYPE: {event_type}"
*
* @param event Pointer to keyevent_t structure to debug
*
* @note Only works when CONSOLE_ENABLE is enabled in rules.mk
* @note Use QMK Toolbox console tab or hid_listen tool to view output
*/
static inline void debug_keyevent(const keyevent_t* event) {
// First message: key position and pressed state
uprintf("KEY: r%d c%d %s\n",
event->key.row, event->key.col,
event->pressed ? "PRESS" : "RELEASE");
// Second message: time and event type
const char* type_str = "UNKNOWN";
switch (event->type) {
case TICK_EVENT: type_str = "TICK"; break;
case KEY_EVENT: type_str = "KEY"; break;
case ENCODER_CW_EVENT: type_str = "ENC_CW"; break;
case ENCODER_CCW_EVENT: type_str = "ENC_CCW"; break;
case COMBO_EVENT: type_str = "COMBO"; break;
}
uprintf("TIME: %u TYPE: %s\n", event->time, type_str);
}
#else
/**
* @brief Send debug message via console (stub function)
* @details Stub function when CONSOLE_ENABLE is disabled. Does nothing.
* @param message Unused debug message parameter
*/
static inline void send_debug_message(const char* message) { (void)message; }
/**
* @brief Debug entire keyevent_t structure (stub function)
* @details Stub function when CONSOLE_ENABLE is disabled. Does nothing.
* @param event Unused keyevent_t parameter
*/
static inline void debug_keyevent(const keyevent_t* event) { (void)event; }
#endif
/*
* ============================================================================
* 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);
}
@BorisTestov
Copy link
Author

BorisTestov commented Sep 15, 2025

Release Notes - Update 15.09.2025

  • Fixed bug in set_analog_sensitivity_curve which ignored third control point and always used (0,0) as origin point
  • Corrected OKMC bit manipulation documentation to reflect actual additive accumulation behavior
  • Added Hall Effect calibration functions
  • All function comments now use proper Doxygen format and function signatures now recognizable in IDEs
  • OKMC/DKS documentation: Emphasized additive accumulation behavior with clear examples showing how keys accumulate

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment