This commit is contained in:
TS Design Works 2025-07-23 13:39:25 -07:00 committed by GitHub
commit 0b74613f79
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 1543 additions and 0 deletions

View File

@ -0,0 +1,31 @@
# TS Southpaw
A southpaw numpad keyboard with RGB matrix and work timer functionality.
* Keyboard Maintainer: [TS Design Works LLC](https://github.com/tsdesignworks)
* Hardware Supported: TS Southpaw Rev 1.6, RP2040 MCU
* Hardware Availability: [GitHub](https://github.com/tsdesignworks/tssouthpaw)
Make example for this keyboard (after setting up your build environment):
make tssouthpaw:default
Flashing example for this keyboard:
make tssouthpaw:default:flash
See the [build environment setup](https://docs.qmk.fm/#/getting_started_build_tools) and the [make instructions](https://docs.qmk.fm/#/getting_started_make_guide) for more information. Brand new to QMK? Start with our [Complete Newbs Guide](https://docs.qmk.fm/#/newbs).
## Bootloader
Enter the bootloader in 2 ways:
* **Physical reset button**: Briefly press the button on the back of the PCB
* **Bootmagic reset**: Hold down the ESC key and plug in the keyboard
## Features
* RGB Matrix with 104 LEDs
* Rotary encoder for volume control
* Work timer with visual notifications
* Southpaw numpad layout (numpad on the left side)

View File

@ -0,0 +1,50 @@
/* Copyright 2025 TS Design Works LLC
*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
// LED indices for indicator LEDs
#define CAPS_LOCK_LED 62 // LED index for Caps Lock indicator
#define NUM_LOCK_LED 2 // LED index for Num Lock indicator
#define MIC_MUTE_LED 3 // LED index for KC_MICMUTE
// Work Timer LED indices
#define WORK_TIMER_LED_START 4 // F1 key LED
#define WORK_TIMER_LED_END 15 // F12 key LED
// Work Timer EEPROM addresses
// Using built-in wear leveling for RP2040
#define EEPROM_WORK_TIMER_ACTIVE 32 // 1 byte - active state
#define EEPROM_WORK_TIMER_START 33 // 4 bytes - start time
#define EEPROM_WORK_TIMER_ELAPSED 37 // 4 bytes - elapsed work time
// 4 bytes - total break time (37+4)
// 1 byte - timer type (37+8)
// 1 byte - break state (37+9)
// 4 bytes - break start time (37+10)
// Total: 15 bytes
// Work Timer Notification Configuration
#define WORK_TIMER_NOTIFICATION_BRIGHTNESS 250 // Brightness for work timer notifications
// Minimum brightness for timer notifications to ensure visibility
#define RGB_MATRIX_MINIMUM_BRIGHTNESS 64 // Ensures timer notifications remain visible
// Dynamic macro configuration
#define DYNAMIC_MACRO_SIZE 128 // More efficient macro size
// RP2040 Settings
#define RP2040_BOOTLOADER_DOUBLE_TAP_RESET // Enable double-tap reset
#define RP2040_BOOTLOADER_DOUBLE_TAP_RESET_TIMEOUT 500U // Timeout window in ms

View File

@ -0,0 +1,310 @@
{
"manufacturer": "TS Design Works LLC",
"keyboard_name": "TS-Southpaw-Rev-1.6",
"maintainer": "ts_design_works_llc",
"bootloader": "rp2040",
"processor": "RP2040",
"diode_direction": "COL2ROW",
"url": "https://github.com/tsdesignworks/tssouthpaw",
"build": {
"lto": true
},
"matrix_pins": {
"cols": ["GP26", "GP25", "GP24", "GP23", "GP22", "GP16", "GP17", "GP15", "GP14", "GP13", "GP12", "GP11", "GP10", "GP9", "GP8", "GP7", "GP6", "GP5", "GP4", "GP3", "GP2"],
"rows": ["GP29", "GP28", "GP21", "GP20", "GP19", "GP18"]
},
"usb": {
"device_version": "0.0.2",
"pid": "0x6163",
"vid": "0x7856",
"suspend_wakeup_delay": 200
},
"ws2812": {
"driver": "vendor",
"pin": "GP27"
},
"encoder": {
"rotary": [
{"pin_a": "GP1", "pin_b": "GP0", "resolution": 4}
]
},
"features": {
"bootmagic": true,
"caps_word": true,
"extrakey": true,
"nkro": true,
"rgb_matrix": true,
"dynamic_macro": true,
},
"debounce": 5,
"rgb_matrix": {
"driver": "ws2812",
"sleep": true,
"led_process_limit": 8,
"default_mode": "SOLID_COLOR",
"animations": {
"solid_color": true,
"breathing": true,
"gradient_up_down": true,
"gradient_left_right": true,
"cycle_out_in": true,
"cycle_out_in_dual": true,
"cycle_pinwheel": true,
"cycle_spiral": true,
"rainbow_moving_chevron": true,
"rainbow_beacon": true,
"rainbow": true,
"rainbow_swirl": true,
"rainbow_pinwheels": true,
"solid_reactive_simple": true,
"solid_reactive": true,
"splash": true,
"multisplash": true,
"solid_splash": true,
"raindrops": true,
"jellybean_raindrops": true,
"typing_heatmap": true,
"digital_rain": true,
"pixel_rain": true
},
"layout": [
{"matrix": [0, 0], "x": 0, "y": 0, "flags": 4},
{"matrix": [0, 1], "x": 11, "y": 0, "flags": 4},
{"matrix": [0, 2], "x": 22, "y": 0, "flags": 4},
{"matrix": [0, 3], "x": 34, "y": 0, "flags": 4},
{"matrix": [0, 8], "x": 45, "y": 0, "flags": 4},
{"matrix": [0, 9], "x": 56, "y": 0, "flags": 4},
{"matrix": [0, 10], "x": 68, "y": 0, "flags": 4},
{"matrix": [0, 11], "x": 78, "y": 0, "flags": 4},
{"matrix": [0, 12], "x": 89, "y": 0, "flags": 4},
{"matrix": [0, 13], "x": 100, "y": 0, "flags": 4},
{"matrix": [0, 14], "x": 112, "y": 0, "flags": 4},
{"matrix": [0, 15], "x": 123, "y": 0, "flags": 4},
{"matrix": [0, 16], "x": 134, "y": 0, "flags": 4},
{"matrix": [0, 17], "x": 146, "y": 0, "flags": 4},
{"matrix": [0, 18], "x": 157, "y": 0, "flags": 4},
{"matrix": [0, 19], "x": 168, "y": 0, "flags": 4},
{"matrix": [0, 20], "x": 179, "y": 0, "flags": 4},
{"matrix": [1, 0], "x": 191, "y": 0, "flags": 4},
{"matrix": [1, 1], "x": 202, "y": 0, "flags": 4},
{"matrix": [1, 2], "x": 213, "y": 0, "flags": 4},
{"matrix": [1, 3], "x": 224, "y": 0, "flags": 4},
{"matrix": [1, 4], "x": 0, "y": 13, "flags": 4},
{"matrix": [1, 5], "x": 11, "y": 13, "flags": 4},
{"matrix": [1, 6], "x": 22, "y": 13, "flags": 4},
{"matrix": [1, 7], "x": 34, "y": 13, "flags": 4},
{"matrix": [1, 8], "x": 45, "y": 13, "flags": 4},
{"matrix": [1, 9], "x": 56, "y": 13, "flags": 4},
{"matrix": [1, 10], "x": 68, "y": 13, "flags": 4},
{"matrix": [1, 11], "x": 78, "y": 13, "flags": 4},
{"matrix": [1, 12], "x": 89, "y": 13, "flags": 4},
{"matrix": [1, 13], "x": 100, "y": 13, "flags": 4},
{"matrix": [1, 14], "x": 112, "y": 13, "flags": 4},
{"matrix": [1, 15], "x": 123, "y": 13, "flags": 4},
{"matrix": [1, 16], "x": 134, "y": 13, "flags": 4},
{"matrix": [1, 17], "x": 146, "y": 13, "flags": 4},
{"matrix": [1, 18], "x": 157, "y": 13, "flags": 4},
{"matrix": [1, 20], "x": 168, "y": 13, "flags": 4},
{"matrix": [2, 0], "x": 179, "y": 13, "flags": 4},
{"matrix": [2, 1], "x": 191, "y": 13, "flags": 4},
{"matrix": [2, 2], "x": 202, "y": 13, "flags": 4},
{"matrix": [2, 4], "x": 213, "y": 13, "flags": 4},
{"matrix": [2, 5], "x": 224, "y": 13, "flags": 4},
{"matrix": [2, 7], "x": 0, "y": 26, "flags": 4},
{"matrix": [2, 8], "x": 11, "y": 26, "flags": 4},
{"matrix": [2, 9], "x": 22, "y": 26, "flags": 4},
{"matrix": [2, 10], "x": 34, "y": 26, "flags": 4},
{"matrix": [2, 11], "x": 45, "y": 26, "flags": 4},
{"matrix": [2, 12], "x": 56, "y": 26, "flags": 4},
{"matrix": [2, 13], "x": 68, "y": 26, "flags": 4},
{"matrix": [2, 14], "x": 78, "y": 26, "flags": 4},
{"matrix": [2, 15], "x": 89, "y": 26, "flags": 4},
{"matrix": [2, 16], "x": 100, "y": 26, "flags": 4},
{"matrix": [2, 17], "x": 112, "y": 26, "flags": 4},
{"matrix": [2, 18], "x": 123, "y": 26, "flags": 4},
{"matrix": [2, 19], "x": 134, "y": 26, "flags": 4},
{"matrix": [2, 20], "x": 146, "y": 26, "flags": 4},
{"matrix": [3, 0], "x": 157, "y": 26, "flags": 4},
{"matrix": [3, 1], "x": 168, "y": 26, "flags": 4},
{"matrix": [3, 2], "x": 179, "y": 26, "flags": 4},
{"matrix": [3, 3], "x": 191, "y": 26, "flags": 4},
{"matrix": [3, 4], "x": 202, "y": 26, "flags": 4},
{"matrix": [3, 5], "x": 213, "y": 26, "flags": 4},
{"matrix": [3, 7], "x": 224, "y": 26, "flags": 4},
{"matrix": [3, 8], "x": 0, "y": 38, "flags": 4},
{"matrix": [3, 9], "x": 11, "y": 38, "flags": 4},
{"matrix": [3, 10], "x": 22, "y": 38, "flags": 4},
{"matrix": [3, 11], "x": 34, "y": 38, "flags": 4},
{"matrix": [3, 12], "x": 45, "y": 38, "flags": 4},
{"matrix": [3, 13], "x": 56, "y": 38, "flags": 4},
{"matrix": [3, 14], "x": 68, "y": 38, "flags": 4},
{"matrix": [3, 15], "x": 78, "y": 38, "flags": 4},
{"matrix": [3, 16], "x": 89, "y": 38, "flags": 4},
{"matrix": [3, 17], "x": 100, "y": 38, "flags": 4},
{"matrix": [3, 18], "x": 112, "y": 38, "flags": 4},
{"matrix": [3, 20], "x": 123, "y": 38, "flags": 4},
{"matrix": [4, 0], "x": 134, "y": 38, "flags": 4},
{"matrix": [4, 1], "x": 146, "y": 38, "flags": 4},
{"matrix": [4, 2], "x": 157, "y": 38, "flags": 4},
{"matrix": [4, 5], "x": 168, "y": 38, "flags": 4},
{"matrix": [4, 7], "x": 179, "y": 38, "flags": 4},
{"matrix": [4, 9], "x": 191, "y": 38, "flags": 4},
{"matrix": [4, 10], "x": 202, "y": 38, "flags": 4},
{"matrix": [4, 11], "x": 213, "y": 38, "flags": 4},
{"matrix": [4, 12], "x": 224, "y": 38, "flags": 4},
{"matrix": [4, 13], "x": 0, "y": 51, "flags": 4},
{"matrix": [4, 14], "x": 11, "y": 51, "flags": 4},
{"matrix": [4, 15], "x": 22, "y": 51, "flags": 4},
{"matrix": [4, 16], "x": 34, "y": 51, "flags": 4},
{"matrix": [4, 17], "x": 45, "y": 51, "flags": 4},
{"matrix": [4, 18], "x": 56, "y": 51, "flags": 4},
{"matrix": [4, 19], "x": 68, "y": 51, "flags": 4},
{"matrix": [5, 1], "x": 78, "y": 51, "flags": 4},
{"matrix": [5, 2], "x": 89, "y": 51, "flags": 4},
{"matrix": [5, 3], "x": 100, "y": 51, "flags": 4},
{"matrix": [5, 4], "x": 112, "y": 51, "flags": 4},
{"matrix": [5, 5], "x": 123, "y": 51, "flags": 4},
{"matrix": [5, 6], "x": 134, "y": 51, "flags": 4},
{"matrix": [5, 8], "x": 146, "y": 51, "flags": 4},
{"matrix": [5, 9], "x": 157, "y": 51, "flags": 4},
{"matrix": [5, 13], "x": 168, "y": 51, "flags": 4},
{"matrix": [5, 16], "x": 179, "y": 51, "flags": 4},
{"matrix": [5, 18], "x": 191, "y": 51, "flags": 4},
{"matrix": [5, 19], "x": 202, "y": 51, "flags": 4},
{"matrix": [5, 20], "x": 213, "y": 51, "flags": 4}
]
},
"layouts": {
"LAYOUT": {
"layout": [
{"label": "Esc", "matrix": [0, 0], "x": 0, "y": 0},
{"label": "Delete", "matrix": [0, 1], "x": 1, "y": 0},
{"label": "Num Lock", "matrix": [0, 2], "x": 2, "y": 0},
{"label": "MicMute", "matrix": [0, 3], "x": 3, "y": 0},
{"label": "No", "matrix": [0, 4], "x": 4, "y": 0},
{"label": "No", "matrix": [0, 5], "x": 5, "y": 0},
{"label": "No", "matrix": [0, 6], "x": 6, "y": 0},
{"label": "Mute", "matrix": [0, 7], "x": 7, "y": 0},
{"label": "F1", "matrix": [0, 8], "x": 8, "y": 0},
{"label": "F2", "matrix": [0, 9], "x": 9, "y": 0},
{"label": "F3", "matrix": [0, 10], "x": 10, "y": 0},
{"label": "F4", "matrix": [0, 11], "x": 11, "y": 0},
{"label": "F5", "matrix": [0, 12], "x": 12, "y": 0},
{"label": "F6", "matrix": [0, 13], "x": 13, "y": 0},
{"label": "F7", "matrix": [0, 14], "x": 14, "y": 0},
{"label": "F8", "matrix": [0, 15], "x": 15, "y": 0},
{"label": "F9", "matrix": [0, 16], "x": 16, "y": 0},
{"label": "F10", "matrix": [0, 17], "x": 17, "y": 0},
{"label": "F11", "matrix": [0, 18], "x": 18, "y": 0},
{"label": "F12", "matrix": [0, 19], "x": 19, "y": 0},
{"label": "LGUI(KC_L)", "matrix": [0, 20], "x": 20, "y": 0},
{"label": "Backspace", "matrix": [1, 0], "x": 0, "y": 1},
{"label": "/", "matrix": [1, 1], "x": 1, "y": 1},
{"label": "*", "matrix": [1, 2], "x": 2, "y": 1},
{"label": "-", "matrix": [1, 3], "x": 3, "y": 1},
{"label": "DM_REC1", "matrix": [1, 4], "x": 4, "y": 1},
{"label": "DM_REC2", "matrix": [1, 5], "x": 5, "y": 1},
{"label": "`", "matrix": [1, 6], "x": 6, "y": 1},
{"label": "1", "matrix": [1, 7], "x": 7, "y": 1},
{"label": "2", "matrix": [1, 8], "x": 8, "y": 1},
{"label": "3", "matrix": [1, 9], "x": 9, "y": 1},
{"label": "4", "matrix": [1, 10], "x": 10, "y": 1},
{"label": "5", "matrix": [1, 11], "x": 11, "y": 1},
{"label": "6", "matrix": [1, 12], "x": 12, "y": 1},
{"label": "7", "matrix": [1, 13], "x": 13, "y": 1},
{"label": "8", "matrix": [1, 14], "x": 14, "y": 1},
{"label": "9", "matrix": [1, 15], "x": 15, "y": 1},
{"label": "0", "matrix": [1, 16], "x": 16, "y": 1},
{"label": "-", "matrix": [1, 17], "x": 17, "y": 1},
{"label": "=", "matrix": [1, 18], "x": 18, "y": 1},
{"label": "No", "matrix": [1, 19], "x": 19, "y": 1},
{"label": "Backspace", "matrix": [1, 20], "x": 20, "y": 1},
{"label": "7", "matrix": [2, 0], "x": 0, "y": 2},
{"label": "8", "matrix": [2, 1], "x": 1, "y": 2},
{"label": "9", "matrix": [2, 2], "x": 2, "y": 2},
{"label": "NO", "matrix": [2, 3], "x": 3, "y": 2},
{"label": "Calculator", "matrix": [2, 4], "x": 4, "y": 2},
{"label": "Print Screen", "matrix": [2, 5], "x": 5, "y": 2},
{"label": "No", "matrix": [2, 6], "x": 6, "y": 2},
{"label": "Tab", "matrix": [2, 7], "x": 7, "y": 2},
{"label": "Q", "matrix": [2, 8], "x": 8, "y": 2},
{"label": "W", "matrix": [2, 9], "x": 9, "y": 2},
{"label": "E", "matrix": [2, 10], "x": 10, "y": 2},
{"label": "R", "matrix": [2, 11], "x": 11, "y": 2},
{"label": "T", "matrix": [2, 12], "x": 12, "y": 2},
{"label": "Y", "matrix": [2, 13], "x": 13, "y": 2},
{"label": "U", "matrix": [2, 14], "x": 14, "y": 2},
{"label": "I", "matrix": [2, 15], "x": 15, "y": 2},
{"label": "O", "matrix": [2, 16], "x": 16, "y": 2},
{"label": "P", "matrix": [2, 17], "x": 17, "y": 2},
{"label": "[", "matrix": [2, 18], "x": 18, "y": 2},
{"label": "]", "matrix": [2, 19], "x": 19, "y": 2},
{"label": "\\", "matrix": [2, 20], "x": 20, "y": 2},
{"label": "4", "matrix": [3, 0], "x": 0, "y": 3},
{"label": "5", "matrix": [3, 1], "x": 1, "y": 3},
{"label": "6", "matrix": [3, 2], "x": 2, "y": 3},
{"label": "PSLS", "matrix": [3, 3], "x": 3, "y": 3},
{"label": "DM_PLY1", "matrix": [3, 4], "x": 4, "y": 3},
{"label": "DM_PLY2", "matrix": [3, 5], "x": 5, "y": 3},
{"label": "No", "matrix": [3, 6], "x": 6, "y": 3},
{"label": "Caps Lock", "matrix": [3, 7], "x": 7, "y": 3},
{"label": "A", "matrix": [3, 8], "x": 8, "y": 3},
{"label": "S", "matrix": [3, 9], "x": 9, "y": 3},
{"label": "D", "matrix": [3, 10], "x": 10, "y": 3},
{"label": "F", "matrix": [3, 11], "x": 11, "y": 3},
{"label": "G", "matrix": [3, 12], "x": 12, "y": 3},
{"label": "H", "matrix": [3, 13], "x": 13, "y": 3},
{"label": "J", "matrix": [3, 14], "x": 14, "y": 3},
{"label": "K", "matrix": [3, 15], "x": 15, "y": 3},
{"label": "L", "matrix": [3, 16], "x": 16, "y": 3},
{"label": ";", "matrix": [3, 17], "x": 17, "y": 3},
{"label": "'", "matrix": [3, 18], "x": 18, "y": 3},
{"label": "No", "matrix": [3, 19], "x": 19, "y": 3},
{"label": "Enter", "matrix": [3, 20], "x": 20, "y": 3},
{"label": "1", "matrix": [4, 0], "x": 0, "y": 4},
{"label": "2", "matrix": [4, 1], "x": 1, "y": 4},
{"label": "3", "matrix": [4, 2], "x": 2, "y": 4},
{"label": "NO", "matrix": [4, 3], "x": 3, "y": 4},
{"label": "No", "matrix": [4, 4], "x": 4, "y": 4},
{"label": "Up", "matrix": [4, 5], "x": 5, "y": 4},
{"label": "No", "matrix": [4, 6], "x": 6, "y": 4},
{"label": "Shift", "matrix": [4, 7], "x": 7, "y": 4},
{"label": "No", "matrix": [4, 8], "x": 8, "y": 4},
{"label": "Z", "matrix": [4, 9], "x": 9, "y": 4},
{"label": "X", "matrix": [4, 10], "x": 10, "y": 4},
{"label": "C", "matrix": [4, 11], "x": 11, "y": 4},
{"label": "V", "matrix": [4, 12], "x": 12, "y": 4},
{"label": "B", "matrix": [4, 13], "x": 13, "y": 4},
{"label": "N", "matrix": [4, 14], "x": 14, "y": 4},
{"label": "M", "matrix": [4, 15], "x": 15, "y": 4},
{"label": ",", "matrix": [4, 16], "x": 16, "y": 4},
{"label": ".", "matrix": [4, 17], "x": 17, "y": 4},
{"label": "/", "matrix": [4, 18], "x": 18, "y": 4},
{"label": "Shift", "matrix": [4, 19], "x": 19, "y": 4},
{"label": "No", "matrix": [4, 20], "x": 20, "y": 4},
{"label": "No", "matrix": [5, 0], "x": 0, "y": 5},
{"label": "0", "matrix": [5, 1], "x": 1, "y": 5},
{"label": ".", "matrix": [5, 2], "x": 2, "y": 5},
{"label": "Enter", "matrix": [5, 3], "x": 3, "y": 5},
{"label": "Left", "matrix": [5, 4], "x": 4, "y": 5},
{"label": "Down", "matrix": [5, 5], "x": 5, "y": 5},
{"label": "Right", "matrix": [5, 6], "x": 6, "y": 5},
{"label": "No", "matrix": [5, 7], "x": 7, "y": 5},
{"label": "Ctrl", "matrix": [5, 8], "x": 8, "y": 5},
{"label": "Alt", "matrix": [5, 9], "x": 9, "y": 5},
{"label": "No", "matrix": [5, 10], "x": 10, "y": 5},
{"label": "No", "matrix": [5, 11], "x": 11, "y": 5},
{"label": "No", "matrix": [5, 12], "x": 12, "y": 5},
{"label": "Space", "matrix": [5, 13], "x": 13, "y": 5},
{"label": "No", "matrix": [5, 14], "x": 14, "y": 5},
{"label": "No", "matrix": [5, 15], "x": 15, "y": 5},
{"label": "Alt", "matrix": [5, 16], "x": 16, "y": 5},
{"label": "No", "matrix": [5, 17], "x": 17, "y": 5},
{"label": "GUI", "matrix": [5, 18], "x": 18, "y": 5},
{"label": "MO(FN)", "matrix": [5, 19], "x": 19, "y": 5},
{"label": "Ctrl", "matrix": [5, 20], "x": 20, "y": 5}
]
}
}
}

View File

@ -0,0 +1,169 @@
/* Copyright 2025 TS Design Works LLC
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include QMK_KEYBOARD_H
#include "rgb_effects/rgb_effects.h"
#include "rgb_effects/work_timer.h"
// Define layers
enum layers {
BASE, // Base layer
FN, // Function layer
};
// Define custom keycodes
enum custom_keycodes {
KC_MICMUTE = SAFE_RANGE, // Microphone mute key
KC_MEDIA, // Media layer toggle
KC_WRKTMR, // Work timer toggle
KC_WRKPAU, // Work timer pause/resume
KC_CPSWRD, // Caps word toggle
KC_30MIN, // 30-minute timer
KC_1HR, // 1-hour timer
KC_4HR, // 4-hour timer
KC_8HR, // 8-hour timer
KC_10HR, // 10-hour timer
};
#ifdef ENCODER_MAP_ENABLE
const uint16_t PROGMEM encoder_map[][NUM_ENCODERS][NUM_DIRECTIONS] = {
[BASE] = { ENCODER_CCW_CW(KC_VOLD, KC_VOLU) },
[FN] = { ENCODER_CCW_CW(_______, _______) },
};
#endif
/**
* Key processing function
* Handles custom keycodes and effects
*/
bool process_record_user(uint16_t keycode, keyrecord_t *record) {
// Handle ESC key with ripple effect
if (keycode == KC_ESC) {
if (record->event.pressed) {
start_esc_ripple_effect();
return true; // Let QMK handle the actual keypress
} else {
stop_esc_ripple_effect();
return true;
}
}
// Handle other custom keycodes
switch (keycode) {
case KC_MUTE:
if (record->event.pressed) {
tap_code(KC_MUTE); // Trigger mute on key press
tap_code(KC_MPLY); // Play/pause on key press
}
return false;
case KC_MICMUTE:
if (record->event.pressed) {
tap_code16(LCTL(LSFT(KC_M))); // MS Teams mic mute shortcut
toggle_mic_mute_effect(); // Toggle the red-pulse effect
}
return false;
// Work timer keycodes
case KC_WRKTMR:
if (record->event.pressed) {
toggle_work_timer(); // Toggle timer on/off
}
return false;
case KC_30MIN:
if (record->event.pressed) {
start_timer(TIMER_TYPE_30MIN);
}
return false;
case KC_1HR:
if (record->event.pressed) {
start_timer(TIMER_TYPE_1HR);
}
return false;
case KC_4HR:
if (record->event.pressed) {
start_timer(TIMER_TYPE_4HR);
}
return false;
case KC_8HR:
if (record->event.pressed) {
start_timer(TIMER_TYPE_8HR);
}
return false;
case KC_10HR:
if (record->event.pressed) {
start_timer(TIMER_TYPE_10HR);
}
return false;
case KC_WRKPAU:
if (record->event.pressed) {
toggle_pause_work_timer(); // Pause/resume the work timer
}
return false;
case KC_CPSWRD:
if (record->event.pressed) {
caps_word_toggle(); // Toggle caps word mode
}
return false;
default:
return true; // Process all other keycodes normally
}
}
/**
* Caps Word Callback
* Determine which keys should maintain caps word mode
*/
bool caps_word_press_user(uint16_t keycode) {
switch (keycode) {
// These keys don't disable Caps Word
case KC_A ... KC_Z:
case KC_MINS:
return true;
}
return false;
}
// Keymap definitions
const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
/* Base Layer */
[BASE] = LAYOUT(
KC_ESC, KC_DEL, KC_NUM_LOCK, KC_MICMUTE, KC_NO, KC_NO, KC_NO, KC_MUTE, KC_F1, KC_F2, KC_F3, KC_F4, KC_F5, KC_F6, KC_F7, KC_F8, KC_F9, KC_F10, KC_F11, KC_F12, LGUI(KC_L),
KC_BSPC, KC_PSLS, KC_PAST, KC_PMNS, DM_REC1, DM_REC2, KC_GRV, KC_1, KC_2, KC_3, KC_4, KC_5, KC_6, KC_7, KC_8, KC_9, KC_0, KC_MINS, KC_EQL, KC_NO, KC_BSPC,
KC_P7, KC_P8, KC_P9, KC_NO, KC_CALC, KC_PSCR, KC_NO, KC_TAB, KC_Q, KC_W, KC_E, KC_R, KC_T, KC_Y, KC_U, KC_I, KC_O, KC_P, KC_LBRC, KC_RBRC, KC_BSLS,
KC_P4, KC_P5, KC_P6, KC_PPLS, DM_PLY1, DM_PLY2, KC_NO, KC_CAPS, KC_A, KC_S, KC_D, KC_F, KC_G, KC_H, KC_J, KC_K, KC_L, KC_SCLN, KC_QUOT, KC_NO, KC_ENT,
KC_P1, KC_P2, KC_P3, KC_NO, KC_NO, KC_UP, KC_NO, KC_LSFT, KC_NO, KC_Z, KC_X, KC_C, KC_V, KC_B, KC_N, KC_M, KC_COMM, KC_DOT, KC_SLSH, KC_RSFT, KC_NO,
KC_NO, KC_P0, KC_PDOT, KC_PENT, KC_LEFT, KC_DOWN, KC_RIGHT, KC_NO, KC_LCTL, KC_LALT, KC_NO, KC_NO, KC_NO, KC_SPC, KC_NO, KC_NO, KC_RALT, KC_NO, KC_RGUI, MO(FN), KC_RCTL
),
/* Function Layer */
[FN] = LAYOUT(
_______, _______, _______, _______, KC_NO , KC_NO , KC_NO , _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______,
_______, _______, _______, RGB_VAD, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, KC_NO , _______,
_______, KC_8HR , KC_10HR, KC_NO , _______, _______, KC_NO , _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______,
KC_4HR , _______, _______, RGB_VAI, _______, _______, KC_NO , KC_CPSWRD, _______, KC_WRKTMR, _______, _______, _______, _______, _______, _______, _______, _______, _______, KC_NO , _______,
KC_1HR , _______, KC_30MIN, KC_NO , KC_NO , RGB_HUI, KC_NO , _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, KC_NO ,
KC_NO , _______, _______, _______, RGB_RMOD, RGB_HUD, RGB_MOD, KC_NO , _______, _______, KC_NO , KC_NO , KC_NO , KC_WRKPAU, KC_NO , KC_NO , _______, KC_NO , _______, _______, _______
),
};

View File

@ -0,0 +1,35 @@
/**
* This is a readme.md file that should be placed in the keymaps/default/ directory
* to provide information about the keymap.
*/
## keymaps/default/readme.md
```markdown
# Default Keymap for TS Southpaw
This is the default keymap for the TS Southpaw keyboard. It includes a base layer with a standard QWERTY layout plus southpaw numpad, and a function layer with work timer controls and RGB lighting adjustments.
## Features
- Southpaw numpad layout (numpad on the left side)
- Work timer system with multiple duration options
- Volume control via rotary encoder
- RGB lighting effects and controls
- Arrow keys highlighted in blue
- ESC key ripple effect
- Caps Lock and Num Lock indicators
- Microphone mute function with indicator
## Layers
### Base Layer
- Standard QWERTY layout
- Left-side numpad
- Media controls
- Function key row (F1-F12)
### Function Layer (Accessed via key to left of right Control)
- RGB lighting controls
- Work timer controls (30MIN, 1HR, 4HR, 8HR, 10HR)
- Caps Word toggle

View File

@ -0,0 +1,267 @@
/* Copyright 2025 TS Design Works LLC
*
* 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 <http://www.gnu.org/licenses/>.
*/
/**
* TS Southpaw RGB Effects System
*/
#include "rgb_effects.h"
#include "quantum.h"
#include "rgb_matrix.h"
#include "work_timer.h"
/**
* Color definitions for RGB effects
*/
static const rgb_color_t CAPS_LOCK_COLOR = {255, 60, 0}; // Orange for Caps Lock
static const rgb_color_t NUM_LOCK_COLOR = {255, 60, 0}; // Orange for Num Lock
static const rgb_color_t ESC_RIPPLE_COLOR = {255, 0, 0}; // Red for ESC ripple effect
static const rgb_color_t MIC_MUTE_COLOR = {255, 0, 0}; // Red for Mic Mute
static const rgb_color_t ARROW_KEYS_COLOR = {0, 0, 255}; // Blue for arrow keys
/**
* State tracking structure for LED effects
*/
typedef struct {
bool active;
uint8_t prev_rgb_mode;
} led_state_t;
/**
* State variables for LED indicators
*/
static led_state_t caps_lock_state = {false, 0};
static led_state_t num_lock_state = {false, 0};
static led_state_t mic_mute_state = {false, 0};
/**
* ESC ripple effect state variables
*/
static struct {
bool active;
uint8_t step;
uint16_t timer;
bool expanding;
} ripple_state = {false, 0, 0, true};
/**
* ESC ripple effect LED configuration
*/
#define MAX_RIPPLE_LAYERS 6
#define MAX_LEDS_PER_LAYER 10
// LED indices for each ripple layer
// 255 is used as a placeholder for unused indices
static const uint8_t esc_splash_ripple[MAX_RIPPLE_LAYERS][MAX_LEDS_PER_LAYER] = {
{0, 255, 255, 255, 255, 255, 255, 255, 255, 255}, // Center LED only
{1, 19, 255, 255, 255, 255, 255, 255, 255, 255}, // First ring
{38, 39, 6, 7, 15, 255, 255, 255, 255, 255}, // Second ring
{42, 43, 25, 48, 49, 255, 255, 255, 255, 255}, // Third ring
{80, 81, 67, 68, 75, 76, 255, 255, 255, 255}, // Fourth ring
{102, 103, 71, 36, 18, 93, 100, 255, 255, 255} // Fifth ring
};
// Number of actual LEDs in each ripple layer for more efficient processing
static const uint8_t leds_per_layer[MAX_RIPPLE_LAYERS] = {1, 2, 5, 5, 6, 7};
/**
* Arrow key LED configuration
*/
static const uint8_t arrow_key_leds[] = {78, 94, 95, 96};
#define ARROW_KEY_COUNT (sizeof(arrow_key_leds) / sizeof(arrow_key_leds[0]))
/**
* Apply a pulsing effect to a specific LED
*
* @param led_index The index of the LED to apply the effect to
* @param color The base RGB color to use
*/
static void apply_pulse_effect(uint8_t led_index, rgb_color_t color) {
// Create a pulsing effect by varying brightness based on timer
uint8_t brightness = abs((timer_read() / 2) % 510 - 255);
rgb_matrix_set_color(led_index,
(color.r * brightness) / 255,
(color.g * brightness) / 255,
(color.b * brightness) / 255);
}
/**
* Handle the state change for an indicator LED
*
* @param active Whether the indicator should be active
* @param state Pointer to the LED state structure to update
*/
static void update_indicator_state(bool active, led_state_t *state) {
if (active) {
if (!state->active) {
state->prev_rgb_mode = rgb_matrix_get_mode();
state->active = true;
}
} else if (state->active) {
state->active = false;
rgb_matrix_mode_noeeprom(state->prev_rgb_mode);
}
}
/**
* Handle Caps Lock indicator lighting
*/
void handle_caps_lock_rgb(void) {
bool caps_active = host_keyboard_led_state().caps_lock;
update_indicator_state(caps_active, &caps_lock_state);
if (caps_lock_state.active) {
apply_pulse_effect(CAPS_LOCK_LED, CAPS_LOCK_COLOR);
}
}
/**
* Handle Num Lock indicator lighting
* Note: Active when Num Lock is OFF (inverse logic)
*/
void handle_num_lock_rgb(void) {
bool num_lock_inactive = !host_keyboard_led_state().num_lock;
update_indicator_state(num_lock_inactive, &num_lock_state);
if (num_lock_state.active) {
apply_pulse_effect(NUM_LOCK_LED, NUM_LOCK_COLOR);
}
}
/**
* Toggle the Mic Mute effect state
*/
void toggle_mic_mute_effect(void) {
if (!mic_mute_state.active) {
mic_mute_state.prev_rgb_mode = rgb_matrix_get_mode();
mic_mute_state.active = true;
} else {
mic_mute_state.active = false;
rgb_matrix_mode_noeeprom(mic_mute_state.prev_rgb_mode);
}
}
/**
* Handle Mic Mute indicator lighting
*/
void handle_mic_mute_rgb(void) {
if (mic_mute_state.active) {
apply_pulse_effect(MIC_MUTE_LED, MIC_MUTE_COLOR);
}
}
/**
* Start the ESC ripple effect
*/
void start_esc_ripple_effect(void) {
ripple_state.active = true;
ripple_state.step = 0;
ripple_state.timer = timer_read();
ripple_state.expanding = true;
}
/**
* Stop the ESC ripple effect
*/
void stop_esc_ripple_effect(void) {
ripple_state.active = false;
}
/**
* Handle the ESC ripple effect animation
*/
void handle_esc_ripple_effect(void) {
if (!ripple_state.active) return;
// Update ripple step based on timer (every 15ms)
if (timer_elapsed(ripple_state.timer) > 15) {
ripple_state.timer = timer_read();
// Update step based on direction
if (ripple_state.expanding) {
ripple_state.step++;
if (ripple_state.step >= MAX_RIPPLE_LAYERS - 1) {
ripple_state.step = MAX_RIPPLE_LAYERS - 1;
ripple_state.expanding = false;
}
} else {
if (ripple_state.step > 0) {
ripple_state.step--;
} else {
ripple_state.expanding = true;
}
}
}
// Turn off all ripple LEDs first
for (uint8_t layer = 0; layer < MAX_RIPPLE_LAYERS; layer++) {
for (uint8_t i = 0; i < leds_per_layer[layer]; i++) {
uint8_t led_index = esc_splash_ripple[layer][i];
if (led_index != 255) { // Skip placeholder values
rgb_matrix_set_color(led_index, 0, 0, 0);
}
}
}
// Set active layer LEDs
uint8_t current_layer = ripple_state.step;
for (uint8_t i = 0; i < leds_per_layer[current_layer]; i++) {
uint8_t led_index = esc_splash_ripple[current_layer][i];
if (led_index != 255) { // Skip placeholder values
rgb_matrix_set_color(led_index,
ESC_RIPPLE_COLOR.r,
ESC_RIPPLE_COLOR.g,
ESC_RIPPLE_COLOR.b);
}
}
}
/**
* Handle arrow key highlighting
*/
static void handle_arrow_keys(void) {
// Only override arrows in SOLID_COLOR mode
if (rgb_matrix_get_mode() == RGB_MATRIX_SOLID_COLOR) {
uint8_t val = rgb_matrix_get_val(); // current brightness (0255)
for (uint8_t i = 0; i < ARROW_KEY_COUNT; i++) {
rgb_matrix_set_color(arrow_key_leds[i],
ARROW_KEYS_COLOR.r * val / 255,
ARROW_KEYS_COLOR.g * val / 255,
ARROW_KEYS_COLOR.b * val / 255);
}
}
}
/**
* Combined overlay for arrow-keys + reactive handlers
*/
bool rgb_matrix_indicators_user(void) {
// Apply arrow key highlighting
handle_arrow_keys();
// Run all reactive handlers
handle_caps_lock_rgb();
handle_num_lock_rgb();
handle_mic_mute_rgb();
handle_esc_ripple_effect();
// Handle work timer through extern call (implemented in work_timer.c)
update_work_timer();
handle_work_timer();
return true; // continue with the normal effect pipeline
}

View File

@ -0,0 +1,85 @@
/* Copyright 2025 TS Design Works LLC
*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "quantum.h"
// LED indices for indicator LEDs
#define CAPS_LOCK_LED 62 // LED index for Caps Lock indicator
#define NUM_LOCK_LED 2 // LED index for Num Lock indicator
#define MIC_MUTE_LED 3 // LED index for KC_MICMUTE
/**
* RGB Effect types enumeration
* For potential future extension of effect types
*/
typedef enum {
RGB_EFFECT_NONE,
RGB_EFFECT_BREATHING,
RGB_EFFECT_RAINBOW,
RGB_EFFECT_CUSTOM
} rgb_effect_type_t;
/**
* RGB Color structure for cleaner color definitions
*/
typedef struct {
uint8_t r;
uint8_t g;
uint8_t b;
} rgb_color_t;
/**
* Function declarations for handling RGB effects
*/
// Handle LED indicators for keyboard state
void handle_caps_lock_rgb(void);
void handle_num_lock_rgb(void);
void handle_mic_mute_rgb(void);
// Ripple effect functions
void handle_esc_ripple_effect(void);
void start_esc_ripple_effect(void);
void stop_esc_ripple_effect(void);
// Microphone mute effect toggle
void toggle_mic_mute_effect(void);
/**
* Utility functions for RGB effects
*/
// Gets the keyboard's current RGB mode
static inline uint8_t get_current_rgb_mode(void) {
return rgb_matrix_get_mode();
}
// Sets an LED to a specific RGB color
static inline void set_led_color(uint8_t led_index, uint8_t r, uint8_t g, uint8_t b) {
rgb_matrix_set_color(led_index, r, g, b);
}
// Calculate color gradient between two colors based on progress (0.0 - 1.0)
static inline rgb_color_t calculate_gradient_color(rgb_color_t start, rgb_color_t end, float progress) {
rgb_color_t result;
result.r = start.r + (int)((float)(end.r - start.r) * progress);
result.g = start.g + (int)((float)(end.g - start.g) * progress);
result.b = start.b + (int)((float)(end.b - start.b) * progress);
return result;
}

View File

@ -0,0 +1,393 @@
/* Copyright 2025 TS Design Works LLC
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "work_timer.h"
#include "quantum.h"
#include "rgb_effects/rgb_effects.h"
#include "timer.h"
// Simple work timer structure
typedef struct {
// Basic state
bool active; // Is the timer running?
bool paused; // Is the timer paused?
work_timer_type_t type; // Timer type (30min, 1hr, etc.)
// Time tracking
uint32_t start_time; // When this timer started
uint32_t end_time; // When this timer should end
uint32_t pause_time; // When timer was paused (if paused)
uint32_t break_time; // Break duration for this timer
// Midpoint break tracking
uint32_t mid_point; // Time value at midpoint (when break starts)
uint32_t mid_point_end; // Time value when break ends
} work_timer_t;
// Global timer state
static work_timer_t timer = {0};
// Timer colors
static const rgb_color_t TIMER_START_COLOR = {WORK_TIMER_START_R, WORK_TIMER_START_G, WORK_TIMER_START_B};
static const rgb_color_t TIMER_MID_COLOR = {WORK_TIMER_MID_R, WORK_TIMER_MID_G, WORK_TIMER_MID_B};
static const rgb_color_t TIMER_END_COLOR = {WORK_TIMER_END_R, WORK_TIMER_END_G, WORK_TIMER_END_B};
static const rgb_color_t TIMER_BREAK_COLOR = {WORK_TIMER_LUNCH_R, WORK_TIMER_LUNCH_G, WORK_TIMER_LUNCH_B};
// Function declarations
static void save_timer_state(void);
static void load_timer_state(void);
static void display_progress_bar(uint8_t num_leds, float progress, float brightness);
/**
* Configure a timer with the specified type
*/
static void configure_timer(work_timer_type_t type) {
timer.type = type;
// Set break duration based on timer type
switch (type) {
case TIMER_TYPE_30MIN:
timer.break_time = MID_BREAK_30MIN_DURATION;
break;
case TIMER_TYPE_1HR:
timer.break_time = MID_BREAK_1HR_DURATION;
break;
case TIMER_TYPE_4HR:
timer.break_time = MID_BREAK_4HR_DURATION;
break;
case TIMER_TYPE_8HR:
timer.break_time = LUNCH_BREAK_DURATION;
break;
case TIMER_TYPE_10HR:
timer.break_time = LUNCH_BREAK_DURATION;
break;
default:
timer.break_time = LUNCH_BREAK_DURATION;
break;
}
// Calculate timer duration (excluding break)
uint32_t work_duration = 0;
switch (type) {
case TIMER_TYPE_30MIN: work_duration = TIMER_30MIN_DURATION; break;
case TIMER_TYPE_1HR: work_duration = TIMER_1HR_DURATION; break;
case TIMER_TYPE_4HR: work_duration = TIMER_4HR_DURATION; break;
case TIMER_TYPE_8HR: work_duration = TIMER_8HR_DURATION; break;
case TIMER_TYPE_10HR: work_duration = TIMER_10HR_DURATION; break;
default: work_duration = TIMER_8HR_DURATION; break;
}
// Calculate end time and midpoint
timer.end_time = timer.start_time + work_duration;
timer.mid_point = timer.start_time + (work_duration / 2);
timer.mid_point_end = timer.mid_point + timer.break_time;
}
/**
* Save timer state to EEPROM
*/
static void save_timer_state(void) {
uint8_t buffer[24] = {0};
// Basic state
buffer[0] = timer.active ? 1 : 0;
buffer[1] = timer.paused ? 1 : 0;
buffer[2] = (uint8_t)timer.type;
// Time values
memcpy(&buffer[3], &timer.start_time, sizeof(uint32_t));
memcpy(&buffer[7], &timer.end_time, sizeof(uint32_t));
memcpy(&buffer[11], &timer.pause_time, sizeof(uint32_t));
memcpy(&buffer[15], &timer.break_time, sizeof(uint32_t));
memcpy(&buffer[19], &timer.mid_point, sizeof(uint32_t));
// Write to EEPROM
eeprom_update_block(buffer, (void *)EEPROM_WORK_TIMER_ACTIVE, sizeof(buffer));
}
/**
* Load timer state from EEPROM
*/
static void load_timer_state(void) {
uint8_t buffer[24] = {0};
// Read from EEPROM
eeprom_read_block(buffer, (const void *)EEPROM_WORK_TIMER_ACTIVE, sizeof(buffer));
// Basic state
timer.active = buffer[0] ? true : false;
timer.paused = buffer[1] ? true : false;
timer.type = (work_timer_type_t)buffer[2];
// Time values
memcpy(&timer.start_time, &buffer[3], sizeof(uint32_t));
memcpy(&timer.end_time, &buffer[7], sizeof(uint32_t));
memcpy(&timer.pause_time, &buffer[11], sizeof(uint32_t));
memcpy(&timer.break_time, &buffer[15], sizeof(uint32_t));
memcpy(&timer.mid_point, &buffer[19], sizeof(uint32_t));
// Calculate mid_point_end from loaded values
if (timer.active) {
timer.mid_point_end = timer.mid_point + timer.break_time;
// Verify timer is still valid
if (!timer.paused) {
uint32_t current_time = timer_read32();
if (current_time > timer.end_time) {
timer.active = false;
save_timer_state();
}
}
}
}
/**
* Display progress bar with gradient colors
*/
static void display_progress_bar(uint8_t num_leds, float progress, float brightness) {
// Progress is a value from 0.0 to 1.0
float hours_per_led = 1.0f / (float)num_leds;
// Determine how many LEDs should be lit
uint8_t leds_lit = (uint8_t)(progress / hours_per_led);
if (leds_lit > num_leds) leds_lit = num_leds;
// Calculate progress for the partially-lit LED
float current_led_progress = (progress - (leds_lit * hours_per_led)) / hours_per_led;
// Set colors for each LED in the progress bar
for (uint8_t i = 0; i < num_leds; i++) {
rgb_color_t color = {0, 0, 0}; // Default to off
if (i < leds_lit) {
// Fully lit LED - calculate gradient color based on position
float position = (float)i / (float)(num_leds - 1);
// Use green to orange gradient for most of bar, orange to red at end
if (position < 0.7f) {
// First 70% is green to orange
float adj_progress = position / 0.7f;
color = calculate_gradient_color(TIMER_START_COLOR, TIMER_MID_COLOR, adj_progress);
} else {
// Last 30% is orange to red
float adj_progress = (position - 0.7f) / 0.3f;
color = calculate_gradient_color(TIMER_MID_COLOR, TIMER_END_COLOR, adj_progress);
}
// Apply brightness
color.r = (uint8_t)((float)color.r * brightness);
color.g = (uint8_t)((float)color.g * brightness);
color.b = (uint8_t)((float)color.b * brightness);
}
else if (i == leds_lit && current_led_progress > 0.0f) {
// Partially lit LED
float position = (float)i / (float)(num_leds - 1);
rgb_color_t full_color;
if (position < 0.7f) {
float adj_progress = position / 0.7f;
full_color = calculate_gradient_color(TIMER_START_COLOR, TIMER_MID_COLOR, adj_progress);
} else {
float adj_progress = (position - 0.7f) / 0.3f;
full_color = calculate_gradient_color(TIMER_MID_COLOR, TIMER_END_COLOR, adj_progress);
}
// Dim by progress and brightness
color.r = (uint8_t)((float)full_color.r * current_led_progress * brightness);
color.g = (uint8_t)((float)full_color.g * current_led_progress * brightness);
color.b = (uint8_t)((float)full_color.b * current_led_progress * brightness);
}
// Set the LED color
rgb_matrix_set_color(WORK_TIMER_LED_START + i, color.r, color.g, color.b);
}
}
/**
* Initialize the work timer
*/
void work_timer_init(void) {
load_timer_state();
}
/**
* Toggle the work timer on/off
*/
void toggle_work_timer(void) {
if (timer.active) {
// Turn off timer
timer.active = false;
timer.paused = false;
save_timer_state();
// Force RGB refresh
rgb_matrix_mode_noeeprom(rgb_matrix_get_mode());
} else {
// Start a new timer
timer.active = true;
timer.paused = false;
timer.start_time = timer_read32();
timer.pause_time = 0;
// Configure for current timer type
configure_timer(timer.type);
save_timer_state();
}
}
/**
* Start a specific timer type
*/
void start_timer(work_timer_type_t type) {
// Start fresh
timer.active = true;
timer.paused = false;
timer.start_time = timer_read32();
timer.pause_time = 0;
// Configure based on type
configure_timer(type);
save_timer_state();
}
/**
* Pause or resume the timer
*/
void toggle_pause_work_timer(void) {
if (!timer.active) return;
if (timer.paused) {
// Resume - calculate time paused and adjust timers
uint32_t current_time = timer_read32();
uint32_t pause_duration = current_time - timer.pause_time;
// Extend all time points by pause duration
timer.end_time += pause_duration;
timer.mid_point += pause_duration;
timer.mid_point_end += pause_duration;
timer.paused = false;
} else {
// Pause
timer.pause_time = timer_read32();
timer.paused = true;
}
save_timer_state();
}
/**
* Check if the blue pulse effect should be active
*/
bool is_timer_pulse_active(void) {
if (!timer.active || timer.paused) return false;
uint32_t current_time = timer_read32();
// Only return true when we're in the mid-point break window
return (current_time >= timer.mid_point && current_time < timer.mid_point_end);
}
/**
* Update timer state
*/
void update_work_timer(void) {
if (!timer.active || timer.paused) return;
uint32_t current_time = timer_read32();
// Check if timer has ended
if (current_time >= timer.end_time) {
timer.active = false;
save_timer_state();
rgb_matrix_mode_noeeprom(rgb_matrix_get_mode());
}
}
/**
* Handle work timer visualization
*/
void handle_work_timer(void) {
static bool was_active = false;
if (!timer.active) {
if (was_active) {
rgb_matrix_mode_noeeprom(rgb_matrix_get_mode());
was_active = false;
}
return;
}
was_active = true;
// Get current time and brightness
uint32_t current_time = timer_read32();
uint8_t rgb_brightness = rgb_matrix_get_val();
float brightness_factor = (float)rgb_brightness / 255.0f;
// Number of LEDs in the progress bar
const uint8_t num_leds = WORK_TIMER_LED_END - WORK_TIMER_LED_START + 1;
// Calculate progress based on active timer duration
float progress;
if (timer.paused) {
// When paused, use pause time for progress calculation
progress = (float)(timer.pause_time - timer.start_time) /
(float)(timer.end_time - timer.start_time);
} else {
// When active, use current time for progress calculation
progress = (float)(current_time - timer.start_time) /
(float)(timer.end_time - timer.start_time);
}
// Clamp progress to valid range
if (progress > 1.0f) progress = 1.0f;
if (progress < 0.0f) progress = 0.0f;
// Check if we're in the break window (midpoint of the timer)
if (!timer.paused && current_time >= timer.mid_point && current_time < timer.mid_point_end) {
// In break time - show blue pulse
rgb_color_t color = TIMER_BREAK_COLOR;
// Slow pulse for break time
uint8_t break_pulse = timer_read() / 10;
float break_intensity = (float)(abs(break_pulse % 510 - 255)) / 255.0f;
// Apply blue pulse to all LEDs in progress bar
for (uint8_t i = 0; i < num_leds; i++) {
rgb_matrix_set_color(WORK_TIMER_LED_START + i,
(uint8_t)((float)color.r * break_intensity * brightness_factor),
(uint8_t)((float)color.g * break_intensity * brightness_factor),
(uint8_t)((float)color.b * break_intensity * brightness_factor));
}
} else {
// Normal progress display
display_progress_bar(num_leds, progress, brightness_factor);
}
}
/**
* Periodic timer task
*/
void work_timer_task(void) {
update_work_timer();
}

View File

@ -0,0 +1,89 @@
/* Copyright 2025 TS Design Works LLC
*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "quantum.h"
// Work Timer type enum
typedef enum {
TIMER_TYPE_30MIN,
TIMER_TYPE_1HR,
TIMER_TYPE_4HR,
TIMER_TYPE_8HR,
TIMER_TYPE_10HR
} work_timer_type_t;
// Work Timer state enum
typedef enum {
TIMER_STATE_INACTIVE,
TIMER_STATE_ACTIVE,
TIMER_STATE_PAUSED,
TIMER_STATE_LUNCH,
TIMER_STATE_MID_BREAK,
TIMER_STATE_COMPLETED
} work_timer_state_t;
// Work Timer Color definitions - modified for more yellow/orange focus
// Start point color (green)
#define WORK_TIMER_START_R 0
#define WORK_TIMER_START_G 255
#define WORK_TIMER_START_B 0
// Mid point color (orange)
#define WORK_TIMER_MID_R 255
#define WORK_TIMER_MID_G 112
#define WORK_TIMER_MID_B 0
// End point color (red)
#define WORK_TIMER_END_R 255
#define WORK_TIMER_END_G 0
#define WORK_TIMER_END_B 0
// Lunch break color (blue)
#define WORK_TIMER_LUNCH_R 0
#define WORK_TIMER_LUNCH_G 0
#define WORK_TIMER_LUNCH_B 255
// Standard timer durations (in milliseconds)
// IMPORTANT: These represent actual working time (excluding breaks)
#define TIMER_30MIN_DURATION 1800000 // 30 minutes
#define TIMER_1HR_DURATION 3600000 // 1 hour
#define TIMER_4HR_DURATION 14400000 // 4 hours
#define TIMER_8HR_DURATION 28800000 // 8 hours (real world 9 hours with lunch)
#define TIMER_10HR_DURATION 36000000 // 10 hours (real world 11 hours with lunch)
// Break durations
#define BREAK_WARNING_TIME 60000 // 60 seconds (kept for calculations but not used for warnings)
#define MID_BREAK_30MIN_DURATION 30000 // 30 seconds
#define MID_BREAK_1HR_DURATION 45000 // 45 seconds
#define MID_BREAK_4HR_DURATION 60000 // 60 seconds
#define LUNCH_BREAK_DURATION 3600000 // 1 hour default
// Define maximum valid duration for sanity checking
#define TIMER_MAX_DURATION 50400000 // 14 hours - sanity check
// Function declarations for work timer
void work_timer_init(void);
void start_timer(work_timer_type_t timer_type);
void toggle_pause_work_timer(void);
void update_work_timer(void);
void handle_work_timer(void);
void toggle_work_timer(void);
void work_timer_task(void);
// Functions for checking timer states
bool is_timer_pulse_active(void);

View File

@ -0,0 +1,5 @@
# This is the rules.mk file for the RGB Effects firmware.
SRC += rgb_effects/rgb_effects.c rgb_effects/work_timer.c
# Enable encoder functionality
ENCODER_ENABLE = yes

View File

@ -0,0 +1,88 @@
/* Copyright 2025 TS Design Works LLC
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "tssouthpaw.h"
#include "rgb_effects/rgb_effects.h"
#include "rgb_effects/work_timer.h"
/**
* Keyboard initialization
* Called once at startup
*/
void keyboard_post_init_kb(void) {
// Initialize RGB lighting effects
rgb_matrix_mode_noeeprom(RGB_MATRIX_SOLID_COLOR);
rgb_matrix_sethsv_noeeprom(HSV_GREEN); // Set default color to green
// Enable N-Key Rollover
keymap_config.nkro = true;
// Load saved work timer state from EEPROM if applicable
work_timer_init();
// Continue with any user-level initialization
keyboard_post_init_user();
}
/**
* Power management function - Called when system is going to sleep
*/
void suspend_power_down_kb(void) {
// If timer pulse is active, ensure RGB matrix remains active
if (is_timer_pulse_active()) {
// Ensure RGB matrix has reasonable brightness for notifications
uint8_t val = rgb_matrix_get_val();
if (val < RGB_MATRIX_MINIMUM_BRIGHTNESS) {
rgb_matrix_sethsv_noeeprom(rgb_matrix_get_hue(), rgb_matrix_get_sat(), RGB_MATRIX_MINIMUM_BRIGHTNESS);
}
// Return early to skip deep sleep routines
return;
}
// Only disable RGB effects when no timer pulse is active
rgb_matrix_set_suspend_state(true);
suspend_power_down_user();
}
/**
* Power management function - Called when system is waking from sleep
*/
void suspend_wakeup_init_kb(void) {
// Make sure the timer pulse state is properly reflected
// This ensures the timer visuals are immediately visible
if (is_timer_pulse_active()) {
// Ensure RGB matrix effect is enabled with proper brightness
uint8_t val = rgb_matrix_get_val();
if (val < RGB_MATRIX_MINIMUM_BRIGHTNESS) {
rgb_matrix_sethsv_noeeprom(rgb_matrix_get_hue(), rgb_matrix_get_sat(), RGB_MATRIX_MINIMUM_BRIGHTNESS);
}
}
suspend_wakeup_init_user();
}
/**
* Regular task hook to ensure timer updates even during suspend
*/
void housekeeping_task_kb(void) {
// Update work timer even during suspend
if (is_timer_pulse_active()) {
update_work_timer();
handle_work_timer();
}
}

View File

@ -0,0 +1,21 @@
/* Copyright 2025 TS Design Works LLC
*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "quantum.h"
// Include to ensure backward compatibility with existing user keymaps