Merge pull request #6 from timfee/copilot/fix-horizontal-flip-issue

Add OLED display support based on Keebart vial_oled reference
This commit is contained in:
Tim Feeley 2026-04-12 16:21:16 -07:00 committed by GitHub
commit b6aff44420
Failed to generate hash of commit
4 changed files with 311 additions and 54 deletions

21
users/timfee/bitmaps.h Normal file
View file

@ -0,0 +1,21 @@
#pragma once
static const char EMPTY_BITMAP [] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
static const char NUM_LOCK_BITMAP [] PROGMEM = {
0x00, 0xc0, 0x20, 0x38, 0x24, 0xb2, 0xaa, 0x2a, 0x2a, 0x2a, 0xb2, 0x24, 0x38, 0x20, 0xc0, 0x00,
0x00, 0x3f, 0x40, 0x40, 0x40, 0x5f, 0x40, 0x43, 0x4c, 0x50, 0x5f, 0x40, 0x40, 0x40, 0x3f, 0x00
};
static const char CAPS_LOCK_BITMAP [] PROGMEM = {
0x00, 0xc0, 0x20, 0x38, 0x24, 0x32, 0xaa, 0xaa, 0xaa, 0xaa, 0x32, 0x24, 0x38, 0x20, 0xc0, 0x00,
0x00, 0x3f, 0x40, 0x40, 0x40, 0x4f, 0x50, 0x50, 0x50, 0x50, 0x49, 0x40, 0x40, 0x40, 0x3f, 0x00
};
static const char SCROLL_LOCK_BITMAP [] PROGMEM = {
0x00, 0xc0, 0x20, 0x38, 0x24, 0x32, 0xaa, 0xaa, 0xaa, 0xaa, 0xb2, 0x24, 0x38, 0x20, 0xc0, 0x00,
0x00, 0x3f, 0x40, 0x40, 0x40, 0x53, 0x52, 0x52, 0x54, 0x54, 0x4c, 0x40, 0x40, 0x40, 0x3f, 0x00
};

View file

@ -15,7 +15,7 @@
#define COMBO_TERM 40 #define COMBO_TERM 40
#define COMBO_ONLY_FROM_LAYER 0 #define COMBO_ONLY_FROM_LAYER 0
// ── OLED (matches Keebart vial_oled reference) ── // ── OLED (SSD1312 on Keebart Corne Choc Pro) ──
#define OLED_IC OLED_IC_SSD1312 #define OLED_IC OLED_IC_SSD1312
#define OLED_DISPLAY_128X64 #define OLED_DISPLAY_128X64
#define OLED_FLIP_SEGMENT #define OLED_FLIP_SEGMENT
@ -23,9 +23,11 @@
#define OLED_CHARGE_PUMP_VALUE 0x72 #define OLED_CHARGE_PUMP_VALUE 0x72
#define OLED_BRIGHTNESS 64 #define OLED_BRIGHTNESS 64
#define OLED_TIMEOUT 0 #define OLED_TIMEOUT 0
#define OLED_TIMEOUT_USER 60000
#define OLED_FADE_OUT #define OLED_FADE_OUT
// ── Split sync ── // ── Split transport ──
#define SPLIT_OLED_ENABLE #define SPLIT_TRANSPORT_MIRROR
#define SPLIT_WPM_ENABLE #define SPLIT_WPM_ENABLE
#define SPLIT_LAYER_STATE_ENABLE #define SPLIT_WATCHDOG_TIMEOUT 4000
#define SPLIT_TRANSACTION_IDS_USER USER_SYNC_OLED_STATE, USER_SYNC_LASTKEY, USER_SYNC_PRESSES

View file

@ -1,5 +1,6 @@
COMBO_ENABLE = yes COMBO_ENABLE = yes
WPM_ENABLE = yes
OLED_ENABLE = yes OLED_ENABLE = yes
OLED_DRIVER = ssd1306 OLED_DRIVER = ssd1306
OLED_TRANSPORT = i2c OLED_TRANSPORT = i2c
WPM_ENABLE = yes
KEYCODE_STRING_ENABLE = yes

View file

@ -1,8 +1,12 @@
#include "timfee.h" #include "timfee.h"
#ifdef OLED_ENABLE
#include "bitmaps.h"
#include "transactions.h"
#endif
// ── State for require-prior-idle ── // ── State for require-prior-idle ──
static uint16_t last_key_time = 0; static uint16_t last_key_time = 0;
static uint16_t last_keycode = KC_NO;
// ── Combos (matching Vial config) ── // ── Combos (matching Vial config) ──
const uint16_t PROGMEM lparen_combo[] = {KC_R, KC_T, COMBO_END}; const uint16_t PROGMEM lparen_combo[] = {KC_R, KC_T, COMBO_END};
@ -25,10 +29,180 @@ combo_t key_combos[COMBO_COUNT] = {
COMBO(bslh_combo, KC_BSLS), COMBO(bslh_combo, KC_BSLS),
}; };
// ── Require-prior-idle: bypass hold-tap during typing ── // ═══════════════════════════════════════════════════════════════════
// OLED state (compiled only when OLED_ENABLE is set)
// ═══════════════════════════════════════════════════════════════════
#ifdef OLED_ENABLE
static const uint8_t OLED_WIDTH = OLED_DISPLAY_HEIGHT;
static const char PROGMEM QMK_LOGO_1[] = { 0x81, 0x82, 0x83, 0x84, 0x00 };
static const char PROGMEM QMK_LOGO_2[] = { 0xA1, 0xA2, 0xA3, 0xA4, 0x00 };
static const char PROGMEM QMK_LOGO_3[] = { 0xC1, 0xC2, 0xC3, 0xC4, 0x00 };
typedef struct { bool oled_on; } oled_state_m2s_t;
typedef struct { uint16_t keycode; } lastkey_m2s_t;
typedef struct { uint32_t left; uint32_t right; } presses_m2s_t;
static bool g_oled_init_done = false;
static uint8_t g_oled_max_char;
static uint32_t g_user_ontime = 0;
static uint16_t g_last_keycode = KC_NO;
static uint32_t g_press_left = 0;
static uint32_t g_press_right = 0;
static oled_state_m2s_t g_remote_oled_state = { false };
static presses_m2s_t g_remote_presses = { 0, 0 };
// ── Charge-pump enable pin (hardware-specific) ──
static inline pin_t get_charge_pump_enable_pin(void) {
return is_keyboard_left() ? GP5 : GP25;
}
// ── Raw-byte blit for 16×16 icons ──
void oled_blit_16x16_P(const char *icon, uint8_t x, uint8_t page) {
for (uint8_t i = 0; i < 16; i++) {
oled_write_raw_byte(pgm_read_byte(&icon[i]), page * OLED_WIDTH + x + i);
oled_write_raw_byte(pgm_read_byte(&icon[16 + i]), (page + 1) * OLED_WIDTH + x + i);
}
}
// ── Helpers ──
static uint16_t unwrap_keycode(uint16_t kc) {
if (kc >= QK_MOD_TAP && kc <= QK_MOD_TAP_MAX)
return QK_MOD_TAP_GET_TAP_KEYCODE(kc);
if (kc >= QK_LAYER_TAP && kc <= QK_LAYER_TAP_MAX)
return QK_LAYER_TAP_GET_TAP_KEYCODE(kc);
return kc;
}
static uint8_t round_percentage(float x) {
float f = x + 0.5f;
uint8_t r = (uint8_t)f;
if ((f - (float)r) == 0.0f && (r & 1)) r--;
return r;
}
static void oled_print_right_aligned(const char *text, uint8_t width) {
uint8_t len = strlen(text);
uint8_t pad = (len < width) ? (width - len) : 0;
for (uint8_t i = 0; i < pad; i++) oled_write_P(PSTR(" "), false);
oled_write(text, false);
}
// ── Info renderers ──
static void print_current_layer(uint8_t row) {
char buf[8];
switch (get_highest_layer(layer_state)) {
case 0: strcpy(buf, "Base"); break;
case 1: strcpy(buf, "Symbols"); break;
case 2: strcpy(buf, "Nav"); break;
default: snprintf(buf, sizeof(buf), "%d", (int)get_highest_layer(layer_state)); break;
}
oled_set_cursor(0, row);
oled_print_right_aligned(buf, g_oled_max_char);
}
static void print_uptime(uint8_t row) {
uint32_t total_min = timer_read32() / 60000u;
uint32_t hours = total_min / 60u;
uint32_t minutes = total_min % 60u;
if (hours > 999u) { hours = 999u; minutes = 59u; }
char buf[8];
snprintf(buf, sizeof(buf), "%3luh%02lum", hours, minutes);
oled_set_cursor(0, row);
oled_print_right_aligned(buf, g_oled_max_char);
}
static void print_wpm(uint8_t row) {
uint16_t dwpm = (uint16_t)get_current_wpm() * 10u;
char buf[11];
snprintf(buf, sizeof(buf), "%3u.%1u WPM", dwpm / 10u, dwpm % 10u);
oled_set_cursor(0, row);
oled_print_right_aligned(buf, g_oled_max_char);
}
static void print_balance(uint8_t row, uint8_t pct) {
char buf[6];
snprintf(buf, sizeof(buf), "%3u %%", pct);
oled_set_cursor(0, row);
oled_print_right_aligned(buf, g_oled_max_char);
}
// ── Split RPC callbacks (slave side) ──
static void user_sync_oled_state_slave(uint8_t in_len, const void *in_data,
uint8_t out_len, void *out_data) {
if (in_len >= sizeof(oled_state_m2s_t))
memcpy(&g_remote_oled_state, in_data, sizeof(oled_state_m2s_t));
}
static void user_sync_lastkey_slave(uint8_t in_len, const void *in_data,
uint8_t out_len, void *out_data) {
if (in_len >= sizeof(lastkey_m2s_t))
g_last_keycode = ((const lastkey_m2s_t *)in_data)->keycode;
}
static void user_sync_presses_slave(uint8_t in_len, const void *in_data,
uint8_t out_len, void *out_data) {
if (in_len >= sizeof(presses_m2s_t))
memcpy(&g_remote_presses, in_data, sizeof(presses_m2s_t));
}
#endif // OLED_ENABLE
// ═══════════════════════════════════════════════════════════════════
// QMK hooks
// ═══════════════════════════════════════════════════════════════════
#ifdef OLED_ENABLE
void keyboard_post_init_user(void) {
pin_t pen = get_charge_pump_enable_pin();
gpio_set_pin_output(pen);
gpio_write_pin_low(pen);
wait_ms(5);
transaction_register_rpc(USER_SYNC_OLED_STATE, user_sync_oled_state_slave);
transaction_register_rpc(USER_SYNC_LASTKEY, user_sync_lastkey_slave);
transaction_register_rpc(USER_SYNC_PRESSES, user_sync_presses_slave);
if (!is_keyboard_master()) wait_ms(90);
}
void housekeeping_task_user(void) {
if (is_keyboard_master()) {
static uint32_t last_sync = 0;
if (timer_elapsed32(last_sync) > 500) {
oled_state_m2s_t pkt = { is_oled_on() };
(void)transaction_rpc_send(USER_SYNC_OLED_STATE, sizeof(pkt), &pkt);
last_sync = timer_read32();
}
}
}
#endif // OLED_ENABLE
// ── Require-prior-idle + OLED keypress tracking ──
bool process_record_user(uint16_t keycode, keyrecord_t *record) { bool process_record_user(uint16_t keycode, keyrecord_t *record) {
#ifdef OLED_ENABLE
g_user_ontime = timer_read32();
#endif
if (record->event.pressed) { if (record->event.pressed) {
last_keycode = keycode; #ifdef OLED_ENABLE
// Track every keypress for OLED (before RPI may intercept)
g_last_keycode = keycode;
if (record->event.key.row < MATRIX_ROWS / 2) {
g_press_left++;
} else {
g_press_right++;
}
if (is_keyboard_master()) {
lastkey_m2s_t kpkt = { g_last_keycode };
(void)transaction_rpc_send(USER_SYNC_LASTKEY, sizeof(kpkt), &kpkt);
presses_m2s_t ppkt = { g_press_left, g_press_right };
(void)transaction_rpc_send(USER_SYNC_PRESSES, sizeof(ppkt), &ppkt);
}
#endif
// Require-prior-idle handling
uint16_t elapsed = timer_elapsed(last_key_time); uint16_t elapsed = timer_elapsed(last_key_time);
switch (keycode) { switch (keycode) {
@ -127,65 +301,124 @@ uint16_t get_quick_tap_term(uint16_t keycode, keyrecord_t *record) {
} }
} }
// ── OLED display (rotation matches Keebart vial_oled reference) ── // ═══════════════════════════════════════════════════════════════════
// OLED rendering
// ═══════════════════════════════════════════════════════════════════
#ifdef OLED_ENABLE #ifdef OLED_ENABLE
oled_rotation_t oled_init_user(oled_rotation_t rotation) { oled_rotation_t oled_init_user(oled_rotation_t rotation) {
return OLED_ROTATION_90; return OLED_ROTATION_90;
} }
static void render_layer(void) { static bool oled_post_init(void) {
oled_write_P(PSTR("Layer: "), false); if (!g_oled_init_done) {
switch (get_highest_layer(layer_state)) { g_oled_max_char = oled_max_chars();
case 0:
oled_write_ln_P(PSTR("Base"), false);
break;
case 1:
oled_write_ln_P(PSTR("Symbols"), false);
break;
case 2:
oled_write_ln_P(PSTR("Nav/Fn"), false);
break;
default:
oled_write_ln_P(PSTR("???"), false);
break;
}
}
static void render_keycode(void) { pin_t pen = get_charge_pump_enable_pin();
oled_write_P(PSTR("Key: 0x"), false); gpio_write_pin_high(pen);
// Print last keycode as 4-digit hex wait_ms(20);
static const char hex[] = "0123456789ABCDEF"; oled_clear();
char buf[5];
buf[0] = hex[(last_keycode >> 12) & 0xF];
buf[1] = hex[(last_keycode >> 8) & 0xF];
buf[2] = hex[(last_keycode >> 4) & 0xF];
buf[3] = hex[ last_keycode & 0xF];
buf[4] = '\0';
oled_write_ln(buf, false);
}
static void render_wpm(void) { g_user_ontime = timer_read32();
oled_write_P(PSTR("WPM: "), false); g_oled_init_done = true;
uint8_t wpm = get_current_wpm();
char buf[4];
buf[0] = (wpm >= 100) ? ('0' + wpm / 100) : ' ';
buf[1] = (wpm >= 10) ? ('0' + (wpm / 10) % 10) : ' ';
buf[2] = '0' + wpm % 10;
buf[3] = '\0';
oled_write_ln(buf, false);
}
bool oled_task_user(void) {
if (is_keyboard_left()) {
render_layer();
render_keycode();
render_wpm();
} else {
render_wpm();
render_layer();
} }
return false; return false;
} }
#endif bool oled_task_user(void) {
oled_post_init();
// ── Resolve press counts (master has live data, slave gets synced) ──
uint32_t lpresses, rpresses;
if (is_keyboard_master()) {
lpresses = g_press_left;
rpresses = g_press_right;
} else {
lpresses = g_remote_presses.left;
rpresses = g_remote_presses.right;
}
// ── OLED on/off management ──
if (is_keyboard_master()) {
uint32_t idle = timer_elapsed32(g_user_ontime);
if (is_oled_on() && idle > OLED_TIMEOUT_USER) {
oled_off();
oled_state_m2s_t pkt = { false };
(void)transaction_rpc_send(USER_SYNC_OLED_STATE, sizeof(pkt), &pkt);
return false;
}
if (!is_oled_on() && idle <= OLED_TIMEOUT_USER) {
oled_on();
oled_state_m2s_t pkt = { true };
(void)transaction_rpc_send(USER_SYNC_OLED_STATE, sizeof(pkt), &pkt);
}
} else {
if (g_remote_oled_state.oled_on) {
if (!is_oled_on()) oled_on();
} else {
if (is_oled_on()) oled_off();
return false;
}
}
// ── Compute balance ──
uint32_t total = lpresses + rpresses;
if (total == 0) total = 1;
uint8_t pct_left = round_percentage((100.0f * lpresses) / total);
uint8_t pct_right = round_percentage((100.0f * rpresses) / total);
// ── Left half ──
if (is_keyboard_left()) {
oled_set_cursor(0, 0);
oled_write_P(PSTR("Layer:"), false);
print_current_layer(1);
// Lock indicators (16×16 bitmaps)
led_t led_state = host_keyboard_led_state();
oled_blit_16x16_P(led_state.num_lock ? NUM_LOCK_BITMAP : EMPTY_BITMAP, 0, 3);
oled_blit_16x16_P(led_state.caps_lock ? CAPS_LOCK_BITMAP : EMPTY_BITMAP, 24, 3);
oled_blit_16x16_P(led_state.scroll_lock ? SCROLL_LOCK_BITMAP : EMPTY_BITMAP, 48, 3);
oled_set_cursor(0, 7);
oled_write_P(PSTR("Left:"), false);
print_balance(8, pct_left);
oled_set_cursor(0, 10);
oled_write_P(PSTR("Last Key:"), false);
oled_set_cursor(0, 11);
oled_print_right_aligned(get_keycode_string(unwrap_keycode(g_last_keycode)),
g_oled_max_char);
// QMK logo
oled_set_cursor(0, 13); oled_write_P(QMK_LOGO_1, false);
oled_set_cursor(0, 14); oled_write_P(QMK_LOGO_2, false);
oled_set_cursor(0, 15); oled_write_P(QMK_LOGO_3, false);
oled_set_cursor(7, 15); oled_write_P(PSTR("QMK"), false);
}
// ── Right half ──
else {
oled_set_cursor(0, 0);
oled_write_P(PSTR("Uptime:"), false);
print_uptime(1);
oled_set_cursor(0, 3);
oled_write_P(PSTR("Avg Speed"), false);
oled_set_cursor(0, 4);
oled_write_P(PSTR("(25 s):"), false);
print_wpm(5);
oled_set_cursor(0, 7);
oled_write_P(PSTR("Right:"), false);
print_balance(8, pct_right);
// QMK logo
oled_set_cursor(0, 13); oled_write_P(QMK_LOGO_1, false);
oled_set_cursor(0, 14); oled_write_P(QMK_LOGO_2, false);
oled_set_cursor(0, 15); oled_write_P(QMK_LOGO_3, false);
oled_set_cursor(7, 15); oled_write_P(PSTR("QMK"), false);
}
return false;
}
#endif // OLED_ENABLE