diff --git a/NEWS.md b/NEWS.md index 9d96084f..500ccd15 100644 --- a/NEWS.md +++ b/NEWS.md @@ -98,7 +98,7 @@ Historically, Kaleidoscope used the dimensional array `keymaps` to map between l ## `PER_KEY_DATA` macros -New `PER_KEY_DATA` and `PER_KEY_DATA_STACKED` macros are available (when defined by a hardware implementation). These macros make it easier to build features like `KEYMAPS` that track some data about each key on a keyboard. +New `PER_KEY_DATA` and `PER_KEY_DATA_STACKED` macros are available (when defined by a hardware implementation). These macros make it easier to build features like `KEYMAPS` that track some data about each key on a keyboard. ## New hardware support @@ -115,6 +115,10 @@ To make it easier to port Kaleidoscope, we introduced the [ATMegaKeyboard](doc/p ## New plugins +### DynamicMacros + +The [DynamicMacros](doc/plugin/DynamicMacros.md) plugin provides a way to use and update macros via the Focus API, through Chrysalis. + ### IdleLEDs The [IdleLEDs](doc/plugin/IdleLEDs.md) plugin is a simple, yet, useful one: it will turn the keyboard LEDs off after a period of inactivity, and back on upon the next key event. diff --git a/doc/plugin/DynamicMacros.md b/doc/plugin/DynamicMacros.md new file mode 100644 index 00000000..a7fe6bf3 --- /dev/null +++ b/doc/plugin/DynamicMacros.md @@ -0,0 +1,100 @@ +# Kaleidoscope-DynamicMacros + +Dynamic macros are similar to [Macros][plugin:macros], but unlike them, they can +be re-defined without compiling and flashing new firmware: one can change +dynamic macros via [Focus][plugin:focus], using a tool like +[Chrysalis][chrysalis]. + + [plugin:macros]: Macros.md + [plugin:focus]: FocusSerial.md + [chrysalis]: https://github.com/keyboardio/Chrysalis + +Dynamic macros come with certain limitations, however: unlike the built-in +macros, dynamic ones do not support running custom code, they can only play back +a sequence of events (keys, mousekeys, etc), and do so whenever one presses the +dynamic macro key. + +You can define up to 32 dynamic macros, there is no limit on their length, +except the amount of storage available on the keyboard. + +## Using the plugin + +To use the plugin, we need to include the header, tell the firmware to `use` the +plugin, and reserve storage space for the macros. This is best illustrated with +an example: + +```c++ +#include +#include +#include +#include + +KALEIDOSCOPE_INIT_PLUGINS( + EEPROMSettings, + Focus, + DynamicMacros +); + +void setup() { + Kaleidoscope.setup(); + + DynamicMacros.reserve_storage(128); +} +``` + +## Keymap markup + +### `DM(id)` + +> Places a dynamic macro key on the keymap, with the `id` number (0 to 31) as +> identifier. Pressing the key will immediately run the associated dynamic +> macro. + +## Plugin methods + +The plugin provides a `DynamicMacros` object, with the following methods and properties available: + +### `.reserve_storage(size)` + +> Reserves `size` bytes of storage for dynamic macros. This must be called from +> the `setup()` method of your sketch, otherwise dynamic macros will not +> function. + +### `.play(macro_id)` + +> Plays back a macro, specified by `macro_id`. + +## `MACRO` steps + +The plugin supports the same [macro steps][doc:steps] as the Macros plugin, +please refer to the documentation therein. + + [doc:steps]: Macros.md#macro-steps + +## Focus commands + +The plugin provides two Focus commands: `macros.map` and `macros.trigger`. + +### `macros.map [macros...]` + +> Without arguments, displays all the stored macros. Each macro is terminated by +> an end marker (`MACRO_ACTION_END`), and the last macro is followed by an +> additional marker. The plugin will send back the entire dynamic macro storage +> space, even the data after the final marker. + +> With arguments, it replaces the current set of dynamic macros with the newly +> given ones. Macros are terminated by an end marker, and the last macro must be +> terminated by an additional one. + +> In both cases, the data sent or expected is a sequence of 8-bit values, a +> memory dump. + +### `macros.trigger macro_id` + +> Runs the dynamic macro associated with `macro_id` immediately. This can be +> used to test macros without having to place them on the keymap. + +## Dependencies + +* [Kaleidoscope-EEPROM-Settings](EEPROM-Settings.md) +* [Kaleidoscope-FocusSerial](FocusSerial.md) diff --git a/examples/Features/EEPROM/DynamicMacros/DynamicMacros.ino b/examples/Features/EEPROM/DynamicMacros/DynamicMacros.ino new file mode 100644 index 00000000..60bc9f24 --- /dev/null +++ b/examples/Features/EEPROM/DynamicMacros/DynamicMacros.ino @@ -0,0 +1,61 @@ +/* -*- mode: c++ -*- +* DynamicMacros - Dynamic macro support for Kaleidoscope. +* Copyright (C) 2019 Keyboard.io, Inc. + * + * 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, version 3. + * + * 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 . + */ + +#include +#include +#include +#include +#include + +// *INDENT-OFF* +KEYMAPS( + [0] = KEYMAP_STACKED + (Key_NoKey, Key_1, Key_2, Key_3, Key_4, Key_5, Key_NoKey, + Key_Backtick, Key_Q, Key_W, Key_E, Key_R, Key_T, Key_Tab, + Key_PageUp, Key_A, Key_S, Key_D, Key_F, Key_G, + Key_PageDown, Key_Z, Key_X, Key_C, Key_V, Key_B, Key_Escape, + + Key_LeftControl, Key_Backspace, Key_LeftGui, Key_LeftShift, + Key_skip, + + DM(0), Key_6, Key_7, Key_8, Key_9, Key_0, Key_skip, + Key_Enter, Key_Y, Key_U, Key_I, Key_O, Key_P, Key_Equals, + Key_H, Key_J, Key_K, Key_L, Key_Semicolon, Key_Quote, + Key_skip, Key_N, Key_M, Key_Comma, Key_Period, Key_Slash, Key_Minus, + + Key_RightShift, Key_RightAlt, Key_Spacebar, Key_RightControl, + Key_skip), +) +// *INDENT-ON* + +KALEIDOSCOPE_INIT_PLUGINS( + EEPROMSettings, + EEPROMKeymap, + DynamicMacros, + Focus +); + +void setup() { + Kaleidoscope.setup(); + + EEPROMKeymap.setup(1); + DynamicMacros.reserve_storage(128); +} + +void loop() { + Kaleidoscope.loop(); +} diff --git a/src/Kaleidoscope-DynamicMacros.h b/src/Kaleidoscope-DynamicMacros.h new file mode 100644 index 00000000..7d0a72ef --- /dev/null +++ b/src/Kaleidoscope-DynamicMacros.h @@ -0,0 +1,19 @@ +/* DynamicMacros - Dynamic macro support for Kaleidoscope. + * Copyright (C) 2019 Keyboard.io, Inc. + * + * 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, version 3. + * + * 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 . + */ + +#pragma once + +#include "kaleidoscope/plugin/DynamicMacros.h" diff --git a/src/Kaleidoscope-Ranges.h b/src/Kaleidoscope-Ranges.h index 9c12111d..d0135173 100644 --- a/src/Kaleidoscope-Ranges.h +++ b/src/Kaleidoscope-Ranges.h @@ -49,6 +49,8 @@ enum : uint16_t { SC_LAST, REDIAL, TURBO, + DYNAMIC_MACRO_FIRST, + DYNAMIC_MACRO_LAST = DYNAMIC_MACRO_FIRST + 31, SAFE_START, KALEIDOSCOPE_SAFE_START = SAFE_START diff --git a/src/kaleidoscope/plugin/DynamicMacros.cpp b/src/kaleidoscope/plugin/DynamicMacros.cpp new file mode 100644 index 00000000..41b7b268 --- /dev/null +++ b/src/kaleidoscope/plugin/DynamicMacros.cpp @@ -0,0 +1,259 @@ +/* DynamicMacros - Dynamic macro support for Kaleidoscope. + * Copyright (C) 2019 Keyboard.io, Inc. + * + * 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, version 3. + * + * 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 . + */ + +#include "Kaleidoscope-DynamicMacros.h" +#include "kaleidoscope/hid.h" +#include "Kaleidoscope-FocusSerial.h" + +using namespace kaleidoscope::ranges; + +namespace kaleidoscope { +namespace plugin { + +uint16_t DynamicMacros::storage_base_; +uint16_t DynamicMacros::storage_size_; +uint16_t DynamicMacros::map_[]; + +static void playMacroKeyswitchEvent(Key key, uint8_t keyswitch_state, bool explicit_report) { + handleKeyswitchEvent(key, UnknownKeyswitchLocation, keyswitch_state | INJECTED); + + if (explicit_report) + return; + + kaleidoscope::hid::sendKeyboardReport(); + kaleidoscope::hid::sendMouseReport(); +} + +static void playKeyCode(Key key, uint8_t keyStates, bool explicit_report) { + if (keyIsPressed(keyStates)) { + playMacroKeyswitchEvent(key, IS_PRESSED, explicit_report); + } + if (keyWasPressed(keyStates)) { + playMacroKeyswitchEvent(key, WAS_PRESSED, explicit_report); + } +} + +static void readKeyCodeAndPlay(uint16_t pos, uint8_t flags, uint8_t keyStates, bool explicit_report) { + Key key; + key.flags = flags; + key.keyCode = EEPROM.read(pos++); + + playKeyCode(key, keyStates, explicit_report); +} + +void DynamicMacros::updateDynamicMacroCache(void) { + uint16_t pos = storage_base_; + uint8_t current_id = 0; + macro_t macro = MACRO_ACTION_END; + bool previous_macro_ended = false; + + map_[0] = 0; + + while (pos < storage_base_ + storage_size_) { + macro = EEPROM.read(pos++); + switch (macro) { + case MACRO_ACTION_STEP_EXPLICIT_REPORT: + case MACRO_ACTION_STEP_IMPLICIT_REPORT: + case MACRO_ACTION_STEP_SEND_REPORT: + previous_macro_ended = false; + break; + + case MACRO_ACTION_STEP_INTERVAL: + case MACRO_ACTION_STEP_WAIT: + case MACRO_ACTION_STEP_KEYCODEDOWN: + case MACRO_ACTION_STEP_KEYCODEUP: + case MACRO_ACTION_STEP_TAPCODE: + previous_macro_ended = false; + pos++; + break; + + case MACRO_ACTION_STEP_KEYDOWN: + case MACRO_ACTION_STEP_KEYUP: + case MACRO_ACTION_STEP_TAP: + previous_macro_ended = false; + pos += 2; + break; + + case MACRO_ACTION_STEP_TAP_SEQUENCE: { + previous_macro_ended = false; + uint8_t keyCode, flags; + do { + flags = EEPROM.read(pos++); + keyCode = EEPROM.read(pos++); + } while (!(flags == 0 && keyCode == 0)); + break; + } + + case MACRO_ACTION_STEP_TAP_CODE_SEQUENCE: { + previous_macro_ended = false; + uint8_t keyCode, flags; + do { + keyCode = EEPROM.read(pos++); + } while (keyCode != 0); + break; + } + + case MACRO_ACTION_END: + map_[++current_id] = pos - storage_base_; + + if (previous_macro_ended) + return; + + previous_macro_ended = true; + break; + } + } +} + +void DynamicMacros::play(uint8_t macro_id) { + macro_t macro = MACRO_ACTION_END; + uint8_t interval = 0; + uint8_t flags; + bool explicit_report = false; + uint16_t pos; + + pos = storage_base_ + map_[macro_id]; + + while (true) { + switch (macro = EEPROM.read(pos++)) { + case MACRO_ACTION_STEP_EXPLICIT_REPORT: + explicit_report = true; + break; + case MACRO_ACTION_STEP_IMPLICIT_REPORT: + explicit_report = false; + break; + case MACRO_ACTION_STEP_SEND_REPORT: + kaleidoscope::hid::sendKeyboardReport(); + kaleidoscope::hid::sendMouseReport(); + break; + case MACRO_ACTION_STEP_INTERVAL: + interval = EEPROM.read(pos++); + break; + case MACRO_ACTION_STEP_WAIT: { + uint8_t wait = EEPROM.read(pos++); + delay(wait); + break; + } + case MACRO_ACTION_STEP_KEYDOWN: + flags = EEPROM.read(pos++); + readKeyCodeAndPlay(pos++, flags, IS_PRESSED, explicit_report); + break; + case MACRO_ACTION_STEP_KEYUP: + flags = EEPROM.read(pos++); + readKeyCodeAndPlay(pos++, flags, WAS_PRESSED, explicit_report); + break; + case MACRO_ACTION_STEP_TAP: + flags = EEPROM.read(pos++); + readKeyCodeAndPlay(pos++, flags, IS_PRESSED | WAS_PRESSED, false); + break; + + case MACRO_ACTION_STEP_KEYCODEDOWN: + readKeyCodeAndPlay(pos++, 0, IS_PRESSED, explicit_report); + break; + case MACRO_ACTION_STEP_KEYCODEUP: + readKeyCodeAndPlay(pos++, 0, WAS_PRESSED, explicit_report); + break; + case MACRO_ACTION_STEP_TAPCODE: + readKeyCodeAndPlay(pos++, 0, IS_PRESSED | WAS_PRESSED, false); + break; + + case MACRO_ACTION_STEP_TAP_SEQUENCE: { + uint8_t keyCode; + do { + flags = EEPROM.read(pos++); + keyCode = EEPROM.read(pos++); + playKeyCode(Key(keyCode, flags), IS_PRESSED | WAS_PRESSED, false); + delay(interval); + } while (!(flags == 0 && keyCode == 0)); + break; + } + case MACRO_ACTION_STEP_TAP_CODE_SEQUENCE: { + uint8_t keyCode; + do { + keyCode = EEPROM.read(pos++); + playKeyCode(Key(keyCode, 0), IS_PRESSED | WAS_PRESSED, false); + delay(interval); + } while (keyCode != 0); + break; + } + + case MACRO_ACTION_END: + default: + return; + } + + delay(interval); + } +} + +EventHandlerResult DynamicMacros::onKeyswitchEvent(Key &mappedKey, KeyAddr key_addr, uint8_t keyState) { + if (mappedKey.raw < DYNAMIC_MACRO_FIRST || mappedKey.raw > DYNAMIC_MACRO_LAST) + return EventHandlerResult::OK; + + if (keyToggledOn(keyState)) { + play(mappedKey.raw - DYNAMIC_MACRO_FIRST); + } + + return EventHandlerResult::EVENT_CONSUMED; +} + +EventHandlerResult DynamicMacros::onFocusEvent(const char *command) { + if (::Focus.handleHelp(command, PSTR("macros.map\nmacros.trigger"))) + return EventHandlerResult::OK; + + if (strncmp_P(command, PSTR("macros."), 7) != 0) + return EventHandlerResult::OK; + + if (strcmp_P(command + 7, PSTR("map")) == 0) { + if (::Focus.isEOL()) { + for (uint16_t i = 0; i < storage_size_; i++) { + uint8_t b; + b = EEPROM.read(storage_base_ + i); + ::Focus.send(b); + } + } else { + uint16_t pos = 0; + + while (!::Focus.isEOL()) { + uint8_t b; + ::Focus.read(b); + + EEPROM.update(storage_base_ + pos++, b); + } + updateDynamicMacroCache(); + } + } + + if (strcmp_P(command + 7, PSTR("trigger")) == 0) { + uint8_t id = 0; + ::Focus.read(id); + play(id); + } + + return EventHandlerResult::EVENT_CONSUMED; +} + +void DynamicMacros::reserve_storage(uint16_t size) { + storage_base_ = ::EEPROMSettings.requestSlice(size); + storage_size_ = size; + updateDynamicMacroCache(); +} + + +} +} + +kaleidoscope::plugin::DynamicMacros DynamicMacros; diff --git a/src/kaleidoscope/plugin/DynamicMacros.h b/src/kaleidoscope/plugin/DynamicMacros.h new file mode 100644 index 00000000..17ab19ad --- /dev/null +++ b/src/kaleidoscope/plugin/DynamicMacros.h @@ -0,0 +1,51 @@ +/* DynamicMacros - Dynamic macro support for Kaleidoscope. + * Copyright (C) 2019 Keyboard.io, Inc. + * + * 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, version 3. + * + * 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 . + */ + +#pragma once + +#include +#include +#include + +#include "kaleidoscope/plugin/Macros/MacroSteps.h" + +#define DM(n) Key(kaleidoscope::ranges::DYNAMIC_MACRO_FIRST + n) + +namespace kaleidoscope { +namespace plugin { + +class DynamicMacros : public kaleidoscope::Plugin { + public: + DynamicMacros(void) {} + + EventHandlerResult onKeyswitchEvent(Key &mappedKey, KeyAddr key_addr, uint8_t keyState); + EventHandlerResult onFocusEvent(const char *command); + + static void reserve_storage(uint16_t size); + + void play(uint8_t seq_id); + + private: + static uint16_t storage_base_; + static uint16_t storage_size_; + static uint16_t map_[31]; + static void updateDynamicMacroCache(void); +}; + +} +} + +extern kaleidoscope::plugin::DynamicMacros DynamicMacros;