diff --git a/.github/workflows/build_moonlight.yml b/.github/workflows/build_moonlight.yml new file mode 100644 index 0000000..8961f62 --- /dev/null +++ b/.github/workflows/build_moonlight.yml @@ -0,0 +1,41 @@ +name: Build moonlight examples + +on: + push: + branches: [main, "feat/**"] + paths: ["moonlight/**", ".github/workflows/build_moonlight.yml"] + pull_request: + paths: ["moonlight/**", ".github/workflows/build_moonlight.yml"] + +jobs: + build: + runs-on: ubuntu-latest + container: ghcr.io/qmk/qmk_cli:latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + example: [voyager_lamp, moonlander_lamp] + steps: + - name: Checkout firmware (firmware25) + run: | + git clone --depth 1 --branch firmware25 \ + https://github.com/zsa/qmk_firmware.git /qmk_firmware + cd /qmk_firmware + qmk config user.qmk_home=/qmk_firmware + qmk git-submodule + + - name: Checkout this repo as modules/zsa + uses: actions/checkout@v4 + with: + path: modules_checkout + + - name: Overlay module checkout + run: | + rm -rf /qmk_firmware/modules/zsa + cp -r "$GITHUB_WORKSPACE/modules_checkout" /qmk_firmware/modules/zsa + + - name: Build example + run: | + cd /qmk_firmware + qmk compile modules/zsa/moonlight/examples/${{ matrix.example }}.json diff --git a/README.md b/README.md index 4ed3bfa..3193783 100644 --- a/README.md +++ b/README.md @@ -10,3 +10,5 @@ Available modules: - `oryx`: Includes the raw hid protocol to connect keyboards to Oryx's live training / Keymapp live view / typ.ing live view - `keycolors`: Includes per-layer key color change +- `moonlight`: Turns a (broken) keyboard into a USB-powered room light — + standalone power support, light-control keycodes, lamp-only by default diff --git a/moonlight/README.md b/moonlight/README.md new file mode 100644 index 0000000..c5ec54e --- /dev/null +++ b/moonlight/README.md @@ -0,0 +1,152 @@ +# moonlight + +Moonlight turns a keyboard — typically one whose matrix is too broken to +type on — into a USB-powered room light. That's the whole feature. + +**This is not a keyboard-lighting or typing-feedback module.** It doesn't +react to keypresses, it doesn't animate on your typing, and by default it +never sends a single keystroke to a computer. It just lights up and stays +lit, like a lamp. + +## How it works + +- The LEDs turn on as soon as the board gets USB power — a wall charger, a + power bank, or a computer. No enumeration required, no host needed. +- Color, brightness, and animation state persist across power cycles + (stored in the same EEPROM-backed `rgb_matrix_config` QMK already uses). +- The light always comes on at power-up, even if it was switched off before + the board was last unplugged. A lamp on a wall switch doesn't remember + "off". + +## Keycodes + +All keycodes act on key-down only; the release is swallowed. + +| Keycode | Alias | Behavior | +|---|---|---| +| `MOONLIGHT_ON` | `MNL_ON` | Turn the light on | +| `MOONLIGHT_OFF` | `MNL_OFF` | Turn the light off | +| `MOONLIGHT_BRIGHTER` | `MNL_BRI` | Brightness up one step | +| `MOONLIGHT_DIMMER` | `MNL_DIM` | Brightness down one step, floored (see `MOONLIGHT_MIN_BRIGHTNESS` below — dim is not the same as off, use `MNL_OFF` for that) | +| `MOONLIGHT_HUE_UP` | `MNL_HUU` | Hue wheel forward | +| `MOONLIGHT_HUE_DOWN` | `MNL_HUD` | Hue wheel backward | +| `MOONLIGHT_ANIM_START` | `MNL_AST` | Switch to the last-used animation (default: breathing) | +| `MOONLIGHT_ANIM_STOP` | `MNL_ASP` | Freeze into steady solid color at the current HSV | +| `MOONLIGHT_ANIM_NEXT` | `MNL_ANX` | Next animation, skipping reactive/keypress-driven effects | +| `MOONLIGHT_ANIM_FASTER` | `MNL_FST` | Animation speed up | +| `MOONLIGHT_ANIM_SLOWER` | `MNL_SLW` | Animation speed down | +| `MOONLIGHT_PRESET_1` … `MOONLIGHT_PRESET_8` | `MNL_P1` … `MNL_P8` | Steady mode + jump to preset hue/sat (turns light on; brightness preserved) | + +Notes: + +- Hue and brightness changes apply whether the light is steady or + animating — they recolor animations too, wherever the effect uses the + base HSV. +- There's no on/off toggle keycode; `MNL_ON` and `MNL_OFF` are discrete by + design. +- Presets change **color only**: pressing a preset applies its hue and + saturation and always lands in steady mode, but keeps whatever brightness + the light is currently at. The `v` (brightness) component in an + `MOONLIGHT_PRESET_n_HSV` definition is accepted for convenience with + QMK's `HSV_*` macros but is otherwise ignored — presets never cause a + brightness jump. +- Presets also turn the light on, the same way `MNL_AST` does: pressing a + preset while the light is off still lands you in that color, steady and + lit, rather than leaving the light off. + +## Quick start + +From a `qmk_firmware` checkout on branch `firmware25`, with this repo +checked out at `modules/zsa` (i.e. `modules/zsa/moonlight/` is this +directory): + +```sh +qmk compile modules/zsa/moonlight/examples/voyager_lamp.json +``` + +or, for a Moonlander (note the `revb` — bare `zsa/moonlander` isn't a +buildable target): + +```sh +qmk compile modules/zsa/moonlight/examples/moonlander_lamp.json +``` + +Flash the resulting firmware with Keymapp, or directly with +`qmk flash modules/zsa/moonlight/examples/voyager_lamp.json`. + +Both example keymaps lay out the full keycode set on a single layer (see +"Layer-switching keys" below for why it has to be one layer): + +- Row 1 (top): `MNL_ON`, `MNL_OFF`, `MNL_BRI`, `MNL_DIM`, with `QK_BOOT` + on the far top-right corner key so the board stays reflashable +- Row 2: `MNL_HUU`, `MNL_HUD` +- Row 3: `MNL_AST`, `MNL_ASP`, `MNL_ANX`, `MNL_FST`, `MNL_SLW` +- Row 4: `MNL_P1` through `MNL_P8` +- Every other position is `KC_NO` + +In `examples/moonlander_lamp.json`, the flat `layers` array's grouping +around indices 54–71 (grouped 5/1/1/5/3/3 entries per line) looks odd if +you're skimming it — that's not a mistake. It mirrors the physical +argument order of the Moonlander's `LAYOUT` macro, including two +single-entry lines for the interleaved thumb keys. Leave the shape alone +if you hand-edit the file; only the values matter. + +## Customization + +Config defines, set in a keymap's own `config.h`. The module's own +`config.h` only carries `NO_USB_STARTUP_CHECK`; these tunables default in +`moonlight.c` behind `#ifndef` guards specifically so that a keymap-level +`config.h` define (processed after the module's) overrides them: + +| Define | Default | Meaning | +|---|---|---| +| `MOONLIGHT_LAMP_ONLY` | `1` | Swallow every non-moonlight keycode (see below). Set to `0` on a working keyboard to allow normal typing alongside light control. | +| `MOONLIGHT_MIN_BOOT_BRIGHTNESS` | `40` | Brightness floor applied at power-up — the lamp never boots dark. | +| `MOONLIGHT_MIN_BRIGHTNESS` | `16` | Floor for `MOONLIGHT_DIMMER` — dimming stops here rather than going fully dark; use `MNL_OFF` to actually turn the light off. | +| `MOONLIGHT_DEFAULT_ANIMATION` | breathing | Animation used the first time `MNL_AST` runs in a power session. Falls back to the first enabled non-reactive animation if breathing isn't enabled on the board. | +| `MOONLIGHT_PRESET_1_HSV` … `MOONLIGHT_PRESET_8_HSV` | built-in palette (red, coral, gold, green, azure, blue, purple, white) | Per-keymap preset colors, e.g. `#define MOONLIGHT_PRESET_1_HSV {HSV_TEAL}`. Any `{h, s, v}` triple works, including `HSV_*` macros; only hue and saturation are actually applied (see the preset note above) — the `v` is accepted but ignored. Unset slots keep their default. | + +If you hand-roll a keymap.json instead of starting from an example, keep +`"modules": ["zsa/defaults", "zsa/moonlight"]` — `zsa/defaults` is +required by ZSA board code and isn't optional, even on a lamp-only build. + +To remap keys, edit the `layers` array in your keymap json (or in +`keymap.c` if you're not using QMK Configurator json format) and place any +of the keycodes/aliases above wherever a physical key still works — you +don't need to fill every position, unused keys can stay `KC_NO`. + +### Layer-switching keys + +In lamp-only mode (the default), **every** non-moonlight keycode is +swallowed — including layer-switching keys like `MO()`, `TT()`, or `DF()`. +A second layer is simply unreachable, so don't design a layout that +depends on one. Put every moonlight keycode you want on a single layer. + +## Caveats + +- **Ignores USB suspend.** When plugged into a computer that goes to + sleep, the lamp stays lit and keeps drawing full LED current instead of + powering down. This is intentional — it's what makes standalone + operation on a dumb charger work at all. +- **Exceeds the USB unconfigured-device power budget by design.** Drawing + LED-level current without ever enumerating goes over the 100 mA budget + a compliant USB device is supposed to observe before configuration. + Chargers and power banks don't enforce that budget, so this is fine in + practice — this is a lamp, not a certified USB peripheral. +- **Reflashing still works normally.** Plugged into a real host, USB + enumerates as usual, so `QK_BOOT` (included in both example keymaps) or + the board's physical reset button both work exactly as they would on + any other build. + +## Hardware test checklist + +After flashing, verify on real hardware: + +1. Boots lit from a dumb USB charger / power bank (no computer attached). +2. Every keycode in the table above behaves as described. +3. Power-cycling restores color, brightness, and animation state, and the + light is ON after power-up even if it was turned off before unplugging. +4. Plugged into a computer: no keystrokes are ever registered (lamp-only + mode), and the light stays on when the computer goes to sleep. +5. Reflashing works, both via the `QK_BOOT` key and via the board's + physical reset button. diff --git a/moonlight/config.h b/moonlight/config.h new file mode 100644 index 0000000..2614ea8 --- /dev/null +++ b/moonlight/config.h @@ -0,0 +1,8 @@ +// Copyright 2026 ZSA Technology Labs, Inc <@zsa> +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +// Keep running when USB never enumerates (dumb charger / power bank). +// Compiles out the suspend trap in tmk_core/protocol/chibios/chibios.c that +// would otherwise park the board in suspend_power_down() forever. +#define NO_USB_STARTUP_CHECK diff --git a/moonlight/examples/moonlander_lamp.json b/moonlight/examples/moonlander_lamp.json new file mode 100644 index 0000000..2a1014d --- /dev/null +++ b/moonlight/examples/moonlander_lamp.json @@ -0,0 +1,24 @@ +{ + "keyboard": "zsa/moonlander/revb", + "keymap": "moonlight_lamp", + "layout": "LAYOUT", + "modules": ["zsa/defaults", "zsa/moonlight"], + "layers": [ + [ + "MNL_ON", "MNL_OFF", "MNL_BRI", "MNL_DIM", "KC_NO", "KC_NO", "KC_NO", + "KC_NO", "KC_NO", "KC_NO", "KC_NO", "KC_NO", "KC_NO", "QK_BOOT", + "MNL_HUU", "MNL_HUD", "KC_NO", "KC_NO", "KC_NO", "KC_NO", "KC_NO", + "KC_NO", "KC_NO", "KC_NO", "KC_NO", "KC_NO", "KC_NO", "KC_NO", + "MNL_AST", "MNL_ASP", "MNL_ANX", "MNL_FST", "MNL_SLW", "KC_NO", "KC_NO", + "KC_NO", "KC_NO", "KC_NO", "KC_NO", "KC_NO", "KC_NO", "KC_NO", + "MNL_P1", "MNL_P2", "MNL_P3", "MNL_P4", "MNL_P5", "MNL_P6", + "MNL_P7", "MNL_P8", "KC_NO", "KC_NO", "KC_NO", "KC_NO", + "KC_NO", "KC_NO", "KC_NO", "KC_NO", "KC_NO", + "KC_NO", + "KC_NO", + "KC_NO", "KC_NO", "KC_NO", "KC_NO", "KC_NO", + "KC_NO", "KC_NO", "KC_NO", + "KC_NO", "KC_NO", "KC_NO" + ] + ] +} diff --git a/moonlight/examples/voyager_lamp.json b/moonlight/examples/voyager_lamp.json new file mode 100644 index 0000000..71f44bf --- /dev/null +++ b/moonlight/examples/voyager_lamp.json @@ -0,0 +1,20 @@ +{ + "keyboard": "zsa/voyager", + "keymap": "moonlight_lamp", + "layout": "LAYOUT", + "modules": ["zsa/defaults", "zsa/moonlight"], + "layers": [ + [ + "MNL_ON", "MNL_OFF", "MNL_BRI", "MNL_DIM", "KC_NO", "KC_NO", + "KC_NO", "KC_NO", "KC_NO", "KC_NO", "KC_NO", "QK_BOOT", + "MNL_HUU", "MNL_HUD", "KC_NO", "KC_NO", "KC_NO", "KC_NO", + "KC_NO", "KC_NO", "KC_NO", "KC_NO", "KC_NO", "KC_NO", + "MNL_AST", "MNL_ASP", "MNL_ANX", "MNL_FST", "MNL_SLW", "KC_NO", + "KC_NO", "KC_NO", "KC_NO", "KC_NO", "KC_NO", "KC_NO", + "MNL_P1", "MNL_P2", "MNL_P3", "MNL_P4", "MNL_P5", "MNL_P6", + "MNL_P7", "MNL_P8", "KC_NO", "KC_NO", "KC_NO", "KC_NO", + "KC_NO", "KC_NO", + "KC_NO", "KC_NO" + ] + ] +} diff --git a/moonlight/moonlight.c b/moonlight/moonlight.c new file mode 100644 index 0000000..a5d9102 --- /dev/null +++ b/moonlight/moonlight.c @@ -0,0 +1,279 @@ +// Copyright 2026 ZSA Technology Labs, Inc <@zsa> +// SPDX-License-Identifier: GPL-2.0-or-later +// +// Moonlight: light control for upcycled keyboards. Purely a lamp module — +// see README.md. All behavior is driven by rgb_matrix. + +#include QMK_KEYBOARD_H + +ASSERT_COMMUNITY_MODULES_MIN_API_VERSION(1, 0, 0); + +#ifndef RGB_MATRIX_ENABLE +# error "The moonlight module requires an rgb_matrix-enabled keyboard (RGB_MATRIX_ENABLE = yes)." +#endif + +// Swallow every non-moonlight keycode so a broken matrix can never type +// into a host. Keymaps for working keyboards may set this to 0. +// +// These tunables default here (not in config.h) because a module's +// config.h is included before the keymap's config.h in QMK's config +// chain: an #ifndef default here lets a keymap's #define override it, +// whereas the same #ifndef in config.h would already have "won" by the +// time the keymap's config.h is processed, and a bare #define would +// collide with it under -Werror. +#ifndef MOONLIGHT_LAMP_ONLY +# define MOONLIGHT_LAMP_ONLY 1 +#endif + +// A lamp never boots dark: brightness floor applied at power-up. +#ifndef MOONLIGHT_MIN_BOOT_BRIGHTNESS +# define MOONLIGHT_MIN_BOOT_BRIGHTNESS 40 +#endif + +// MOONLIGHT_DIMMER floor (dim ≠ off; MOONLIGHT_OFF turns the light off). +#ifndef MOONLIGHT_MIN_BRIGHTNESS +# define MOONLIGHT_MIN_BRIGHTNESS 16 +#endif + +// Modes that must never run on a lamp: reactive/keypress-driven effects, +// plus NONE and out-of-range. SOLID_COLOR is excluded from the carousel +// too — it is the "steady" state reached via MOONLIGHT_ANIM_STOP. +static bool moonlight_mode_is_lamp_safe(uint8_t mode) { + switch (mode) { + case RGB_MATRIX_NONE: + case RGB_MATRIX_SOLID_COLOR: +#ifdef ENABLE_RGB_MATRIX_SOLID_REACTIVE_SIMPLE + case RGB_MATRIX_SOLID_REACTIVE_SIMPLE: +#endif +#ifdef ENABLE_RGB_MATRIX_SOLID_REACTIVE + case RGB_MATRIX_SOLID_REACTIVE: +#endif +#ifdef ENABLE_RGB_MATRIX_SOLID_REACTIVE_WIDE + case RGB_MATRIX_SOLID_REACTIVE_WIDE: +#endif +#ifdef ENABLE_RGB_MATRIX_SOLID_REACTIVE_MULTIWIDE + case RGB_MATRIX_SOLID_REACTIVE_MULTIWIDE: +#endif +#ifdef ENABLE_RGB_MATRIX_SOLID_REACTIVE_CROSS + case RGB_MATRIX_SOLID_REACTIVE_CROSS: +#endif +#ifdef ENABLE_RGB_MATRIX_SOLID_REACTIVE_MULTICROSS + case RGB_MATRIX_SOLID_REACTIVE_MULTICROSS: +#endif +#ifdef ENABLE_RGB_MATRIX_SOLID_REACTIVE_NEXUS + case RGB_MATRIX_SOLID_REACTIVE_NEXUS: +#endif +#ifdef ENABLE_RGB_MATRIX_SOLID_REACTIVE_MULTINEXUS + case RGB_MATRIX_SOLID_REACTIVE_MULTINEXUS: +#endif +#ifdef ENABLE_RGB_MATRIX_SPLASH + case RGB_MATRIX_SPLASH: +#endif +#ifdef ENABLE_RGB_MATRIX_MULTISPLASH + case RGB_MATRIX_MULTISPLASH: +#endif +#ifdef ENABLE_RGB_MATRIX_SOLID_SPLASH + case RGB_MATRIX_SOLID_SPLASH: +#endif +#ifdef ENABLE_RGB_MATRIX_SOLID_MULTISPLASH + case RGB_MATRIX_SOLID_MULTISPLASH: +#endif +#ifdef ENABLE_RGB_MATRIX_TYPING_HEATMAP + case RGB_MATRIX_TYPING_HEATMAP: // framebuffer effect, keypress-driven +#endif + return false; + default: + return mode < RGB_MATRIX_EFFECT_MAX; + } +} + +// Next lamp-safe animation after `from`, wrapping. Falls back to +// SOLID_COLOR if the board somehow has no lamp-safe animations. +static uint8_t moonlight_next_anim(uint8_t from) { + uint8_t mode = from; + for (uint8_t i = 0; i < RGB_MATRIX_EFFECT_MAX; i++) { + mode = (mode + 1 < RGB_MATRIX_EFFECT_MAX) ? mode + 1 : 1; + if (moonlight_mode_is_lamp_safe(mode)) { + return mode; + } + } + return RGB_MATRIX_SOLID_COLOR; +} + +#if !defined(MOONLIGHT_DEFAULT_ANIMATION) && defined(ENABLE_RGB_MATRIX_BREATHING) +# define MOONLIGHT_DEFAULT_ANIMATION RGB_MATRIX_BREATHING +#endif + +static uint8_t moonlight_default_anim(void) { +#ifdef MOONLIGHT_DEFAULT_ANIMATION + if (moonlight_mode_is_lamp_safe(MOONLIGHT_DEFAULT_ANIMATION)) { + return MOONLIGHT_DEFAULT_ANIMATION; + } +#endif + return moonlight_next_anim(RGB_MATRIX_SOLID_COLOR); +} + +// Last animation used this power session (0 = none yet), so +// stop-then-start resumes the same animation. +static uint8_t moonlight_last_anim = 0; + +// Preset palette. Keymaps override any slot in their config.h, e.g. +// #define MOONLIGHT_PRESET_1_HSV {HSV_TEAL} +// Only hue and saturation are applied; current brightness is preserved +// (the v component is accepted for HSV_* macro convenience but ignored). +#ifndef MOONLIGHT_PRESET_1_HSV +# define MOONLIGHT_PRESET_1_HSV {HSV_RED} +#endif +#ifndef MOONLIGHT_PRESET_2_HSV +# define MOONLIGHT_PRESET_2_HSV {HSV_CORAL} +#endif +#ifndef MOONLIGHT_PRESET_3_HSV +# define MOONLIGHT_PRESET_3_HSV {HSV_GOLD} +#endif +#ifndef MOONLIGHT_PRESET_4_HSV +# define MOONLIGHT_PRESET_4_HSV {HSV_GREEN} +#endif +#ifndef MOONLIGHT_PRESET_5_HSV +# define MOONLIGHT_PRESET_5_HSV {HSV_AZURE} +#endif +#ifndef MOONLIGHT_PRESET_6_HSV +# define MOONLIGHT_PRESET_6_HSV {HSV_BLUE} +#endif +#ifndef MOONLIGHT_PRESET_7_HSV +# define MOONLIGHT_PRESET_7_HSV {HSV_PURPLE} +#endif +#ifndef MOONLIGHT_PRESET_8_HSV +# define MOONLIGHT_PRESET_8_HSV {HSV_WHITE} +#endif + +static const HSV moonlight_presets[] = { + MOONLIGHT_PRESET_1_HSV, MOONLIGHT_PRESET_2_HSV, MOONLIGHT_PRESET_3_HSV, MOONLIGHT_PRESET_4_HSV, + MOONLIGHT_PRESET_5_HSV, MOONLIGHT_PRESET_6_HSV, MOONLIGHT_PRESET_7_HSV, MOONLIGHT_PRESET_8_HSV, +}; + +// Lamp-only: nothing but moonlight controls (and QK_BOOT, for flashing) +// gets processed — a broken matrix can never type into a host. +bool pre_process_record_moonlight(uint16_t keycode, keyrecord_t *record) { + if (!pre_process_record_moonlight_kb(keycode, record)) { + return false; + } +#if MOONLIGHT_LAMP_ONLY + switch (keycode) { + case MOONLIGHT_ON ... MOONLIGHT_PRESET_8: + case QK_BOOT: + return true; + default: + return false; + } +#else + return true; +#endif +} + +bool process_record_moonlight(uint16_t keycode, keyrecord_t *record) { + if (!process_record_moonlight_kb(keycode, record)) { + return false; + } + + // Moonlight keycodes act on press only; consume the release too. + if (!record->event.pressed) { + switch (keycode) { + case MOONLIGHT_ON ... MOONLIGHT_PRESET_8: + return false; + default: + return true; + } + } + + switch (keycode) { + case MOONLIGHT_ON: + rgb_matrix_enable(); + return false; + case MOONLIGHT_OFF: + rgb_matrix_disable(); + return false; + case MOONLIGHT_BRIGHTER: + rgb_matrix_increase_val(); // clamps at RGB_MATRIX_MAXIMUM_BRIGHTNESS + return false; + case MOONLIGHT_DIMMER: { + HSV hsv = rgb_matrix_get_hsv(); + uint8_t v = hsv.v > MOONLIGHT_MIN_BRIGHTNESS + RGB_MATRIX_VAL_STEP + ? hsv.v - RGB_MATRIX_VAL_STEP + : MOONLIGHT_MIN_BRIGHTNESS; + rgb_matrix_sethsv(hsv.h, hsv.s, v); + return false; + } + case MOONLIGHT_HUE_UP: + rgb_matrix_increase_hue(); + return false; + case MOONLIGHT_HUE_DOWN: + rgb_matrix_decrease_hue(); + return false; + case MOONLIGHT_ANIM_START: { + uint8_t target = moonlight_last_anim ? moonlight_last_anim : moonlight_default_anim(); + rgb_matrix_enable(); + rgb_matrix_mode(target); + moonlight_last_anim = target; + return false; + } + case MOONLIGHT_ANIM_STOP: { + uint8_t cur = rgb_matrix_get_mode(); + if (moonlight_mode_is_lamp_safe(cur)) { + moonlight_last_anim = cur; // resume point for the next START + } + rgb_matrix_mode(RGB_MATRIX_SOLID_COLOR); + return false; + } + case MOONLIGHT_ANIM_NEXT: { + uint8_t cur = rgb_matrix_get_mode(); + uint8_t base = moonlight_mode_is_lamp_safe(cur) + ? cur + : (moonlight_last_anim ? moonlight_last_anim : RGB_MATRIX_SOLID_COLOR); + uint8_t next = moonlight_next_anim(base); + rgb_matrix_enable(); + rgb_matrix_mode(next); + moonlight_last_anim = next; + return false; + } + case MOONLIGHT_ANIM_FASTER: + rgb_matrix_increase_speed(); + return false; + case MOONLIGHT_ANIM_SLOWER: + rgb_matrix_decrease_speed(); + return false; + case MOONLIGHT_PRESET_1 ... MOONLIGHT_PRESET_8: { + HSV preset = moonlight_presets[keycode - MOONLIGHT_PRESET_1]; + rgb_matrix_enable(); // presets turn the light on, like MOONLIGHT_ANIM_START + rgb_matrix_mode(RGB_MATRIX_SOLID_COLOR); // presets always land steady + rgb_matrix_sethsv(preset.h, preset.s, rgb_matrix_get_hsv().v); + return false; + } + default: + return true; + } +} + +void keyboard_post_init_moonlight(void) { + keyboard_post_init_moonlight_kb(); + + // A lamp on a wall switch always comes on. + rgb_matrix_enable(); + + // Never boot dark. + HSV hsv = rgb_matrix_get_hsv(); + if (hsv.v < MOONLIGHT_MIN_BOOT_BRIGHTNESS) { + rgb_matrix_sethsv(hsv.h, hsv.s, MOONLIGHT_MIN_BOOT_BRIGHTNESS); + } + + // EEPROM may hold a reactive or out-of-range mode (previous firmware, + // fewer animations, etc.). Snap those to steady; remember valid + // animations as the START resume point. + uint8_t mode = rgb_matrix_get_mode(); + if (mode == RGB_MATRIX_SOLID_COLOR) { + // steady — nothing to do + } else if (moonlight_mode_is_lamp_safe(mode)) { + moonlight_last_anim = mode; // restored mid-animation; keep animating + } else { + rgb_matrix_mode(RGB_MATRIX_SOLID_COLOR); + } +} diff --git a/moonlight/qmk_module.json b/moonlight/qmk_module.json new file mode 100644 index 0000000..4130de1 --- /dev/null +++ b/moonlight/qmk_module.json @@ -0,0 +1,29 @@ +{ + "module_name": "Moonlight", + "maintainer": "ZSA", + "license": "GPL-2.0-or-later", + "features": { + "rgb_matrix": true + }, + "keycodes": [ + { "key": "MOONLIGHT_ON", "aliases": ["MNL_ON"] }, + { "key": "MOONLIGHT_OFF", "aliases": ["MNL_OFF"] }, + { "key": "MOONLIGHT_BRIGHTER", "aliases": ["MNL_BRI"] }, + { "key": "MOONLIGHT_DIMMER", "aliases": ["MNL_DIM"] }, + { "key": "MOONLIGHT_HUE_UP", "aliases": ["MNL_HUU"] }, + { "key": "MOONLIGHT_HUE_DOWN", "aliases": ["MNL_HUD"] }, + { "key": "MOONLIGHT_ANIM_START", "aliases": ["MNL_AST"] }, + { "key": "MOONLIGHT_ANIM_STOP", "aliases": ["MNL_ASP"] }, + { "key": "MOONLIGHT_ANIM_NEXT", "aliases": ["MNL_ANX"] }, + { "key": "MOONLIGHT_ANIM_FASTER", "aliases": ["MNL_FST"] }, + { "key": "MOONLIGHT_ANIM_SLOWER", "aliases": ["MNL_SLW"] }, + { "key": "MOONLIGHT_PRESET_1", "aliases": ["MNL_P1"] }, + { "key": "MOONLIGHT_PRESET_2", "aliases": ["MNL_P2"] }, + { "key": "MOONLIGHT_PRESET_3", "aliases": ["MNL_P3"] }, + { "key": "MOONLIGHT_PRESET_4", "aliases": ["MNL_P4"] }, + { "key": "MOONLIGHT_PRESET_5", "aliases": ["MNL_P5"] }, + { "key": "MOONLIGHT_PRESET_6", "aliases": ["MNL_P6"] }, + { "key": "MOONLIGHT_PRESET_7", "aliases": ["MNL_P7"] }, + { "key": "MOONLIGHT_PRESET_8", "aliases": ["MNL_P8"] } + ] +}