First pass at a port for the Keyboardio Space Cadet

jesse/space-cadet
Jesse Vincent 3 years ago
parent 54573a9b8c
commit 6b8355a7ba
No known key found for this signature in database
GPG Key ID: 122F5DF7108E4046

@ -0,0 +1,146 @@
/* -*- mode: c++ -*-
* Atreus -- Chrysalis-enabled Sketch for the Keyboardio Atreus
* Copyright (C) 2018, 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; 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, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#ifndef BUILD_INFORMATION
#define BUILD_INFORMATION "locally built on " __DATE__ " at " __TIME__
#endif
#include "Kaleidoscope.h"
#include "Kaleidoscope-EEPROM-Settings.h"
#include "Kaleidoscope-EEPROM-Keymap.h"
#include "Kaleidoscope-FocusSerial.h"
#include "Kaleidoscope-Macros.h"
#include "Kaleidoscope-MouseKeys.h"
#include "Kaleidoscope-OneShot.h"
#include "Kaleidoscope-Qukeys.h"
#include "Kaleidoscope-SpaceCadet.h"
#include "Kaleidoscope-Hardware-Keyboardio-SpaceCadet.h"
#define MO(n) ShiftToLayer(n)
#define TG(n) LockLayer(n)
enum {
MACRO_QWERTY,
MACRO_VERSION_INFO
};
#define Key_Exclamation LSHIFT(Key_1)
#define Key_At LSHIFT(Key_2)
#define Key_Hash LSHIFT(Key_3)
#define Key_Dollar LSHIFT(Key_4)
#define Key_Percent LSHIFT(Key_5)
#define Key_Caret LSHIFT(Key_6)
#define Key_And LSHIFT(Key_7)
#define Key_Star LSHIFT(Key_8)
#define Key_Plus LSHIFT(Key_Equals)
enum {
QWERTY,
FUN,
UPPER
};
/* *INDENT-OFF* */
KEYMAPS(
[QWERTY] = KEYMAP_STACKED
(
Key_Q ,Key_W ,Key_E ,Key_R ,Key_T
,Key_A ,Key_S ,Key_D ,Key_F ,Key_G
,Key_Z ,Key_X ,Key_C ,Key_V ,Key_B, Key_Backtick
,Key_Esc ,Key_Tab ,Key_LeftGui ,Key_LeftShift ,Key_Backspace ,Key_LeftControl
,Key_Y ,Key_U ,Key_I ,Key_O ,Key_P
,Key_H ,Key_J ,Key_K ,Key_L ,Key_Semicolon
,Key_Backslash,Key_N ,Key_M ,Key_Comma ,Key_Period ,Key_Slash
,Key_LeftAlt ,Key_Space ,MO(FUN) ,Key_Minus ,Key_Quote ,Key_Enter
),
[FUN] = KEYMAP_STACKED
(
Key_Exclamation ,Key_At ,Key_UpArrow ,Key_Dollar ,Key_Percent
,Key_LeftParen ,Key_LeftArrow ,Key_DownArrow ,Key_RightArrow ,Key_RightParen
,Key_LeftBracket ,Key_RightBracket ,Key_Hash ,Key_LeftCurlyBracket ,Key_RightCurlyBracket ,Key_Caret
,TG(UPPER) ,Key_Insert ,Key_LeftGui ,Key_LeftShift ,Key_Delete ,Key_LeftControl
,Key_PageUp ,Key_7 ,Key_8 ,Key_9 ,Key_Backspace
,Key_PageDown ,Key_4 ,Key_5 ,Key_6 ,___
,Key_And ,Key_Star ,Key_1 ,Key_2 ,Key_3 ,Key_Plus
,Key_LeftAlt ,Key_Space ,___ ,Key_Period ,Key_0 ,Key_Equals
),
[UPPER] = KEYMAP_STACKED
(
Key_Insert ,Key_Home ,Key_UpArrow ,Key_End ,Key_PageUp
,Key_Delete ,Key_LeftArrow ,Key_DownArrow ,Key_RightArrow ,Key_PageDown
,M(MACRO_VERSION_INFO) ,Consumer_VolumeIncrement ,XXX ,XXX ,___ ,___
,MoveToLayer(QWERTY) ,Consumer_VolumeDecrement ,___ ,___ ,___ ,___
,Key_UpArrow ,Key_F7 ,Key_F8 ,Key_F9 ,Key_F10
,Key_DownArrow ,Key_F4 ,Key_F5 ,Key_F6 ,Key_F11
,___ ,XXX ,Key_F1 ,Key_F2 ,Key_F3 ,Key_F12
,___ ,___ ,MoveToLayer(QWERTY) ,Key_PrintScreen ,Key_ScrollLock ,Consumer_PlaySlashPause
)
)
/* *INDENT-ON* */
KALEIDOSCOPE_INIT_PLUGINS(
EEPROMSettings,
EEPROMKeymap,
Focus,
FocusEEPROMCommand,
FocusSettingsCommand,
Qukeys,
SpaceCadet,
OneShot,
Macros,
MouseKeys
);
const macro_t *macroAction(uint8_t macro_id, KeyEvent &event) {
if (keyToggledOn(event.state)) {
switch (macro_id) {
case MACRO_QWERTY:
// This macro is currently unused, but is kept around for compatibility
// reasons. We used to use it in place of `MoveToLayer(QWERTY)`, but no
// longer do. We keep it so that if someone still has the old layout with
// the macro in EEPROM, it will keep working after a firmware update.
Layer.move(QWERTY);
break;
case MACRO_VERSION_INFO:
Macros.type(PSTR("Keyboardio Atreus - Kaleidoscope "));
Macros.type(PSTR(BUILD_INFORMATION));
break;
default:
break;
}
}
return MACRO_NONE;
}
void setup() {
Kaleidoscope.setup();
SpaceCadet.disable();
EEPROMKeymap.setup(10);
}
void loop() {
Kaleidoscope.loop();
}

@ -0,0 +1,7 @@
name=Kaleidoscope-Hardware-Keyboardio-Spacecadet
version=0.0.0
sentence=Keyboardio Spacecadet hardware support for Kaleidoscope
maintainer=Kaleidoscope's Developers <jesse@keyboard.io>
url=https://github.com/keyboardio/Kaleidoscope
author=Keyboardio
paragraph=

@ -0,0 +1,19 @@
/* -*- mode: c++ -*-
* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "kaleidoscope/device/keyboardio/Spaceadet.h"

@ -0,0 +1,234 @@
/* -*- mode: c++ -*-
* kaleidoscope::driver::keyscanner::GD32 -- AVR GD32-based keyscanner component
* Copyright (C) 2018-2020 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <Arduino.h>
#include "kaleidoscope/macro_helpers.h"
#include "kaleidoscope/driver/keyscanner/Base.h"
#include "kaleidoscope/driver/keyscanner/None.h"
namespace kaleidoscope {
namespace driver {
namespace keyscanner {
struct GD32Props: kaleidoscope::driver::keyscanner::BaseProps {
static const uint16_t keyscan_interval = 1500;
static const uint16_t next_scan_after = 0;
typedef uint32_t RowState;
/*
* The following two lines declare an empty array. Both of these must be
* shadowed by the descendant keyscanner description class.
*/
static constexpr uint8_t matrix_row_pins[] = {};
static constexpr uint8_t matrix_col_pins[] = {};
};
template <typename _KeyScannerProps>
class GD32: public kaleidoscope::driver::keyscanner::Base<_KeyScannerProps> {
private:
typedef GD32<_KeyScannerProps> ThisType;
public:
void setup() {
static_assert(
sizeof(_KeyScannerProps::matrix_row_pins) > 0,
"The key scanner description has an empty array of matrix row pins."
);
static_assert(
sizeof(_KeyScannerProps::matrix_col_pins) > 0,
"The key scanner description has an empty array of matrix column pins."
);
for (uint8_t i = 0; i < _KeyScannerProps::matrix_columns; i++) {
pinmode(_KeyScannerProps::matrix_col_pins[i], INPUT_PULLUP);
}
for (uint8_t i = 0; i < _KeyScannerProps::matrix_rows; i++) {
pinMode(_KeyScannerProps::matrix_row_pins[i], OUTPUT);
digitalWrite(_KeyScannerProps::matrix_row_pins[i], HIGH);
}
setScanCycleTime(_KeyScannerProps::keyscan_interval);
}
/* setScanCycleTime takes a value of between 0 and 8192. This corresponds (roughly) to the number of microseconds to wait between scanning the key matrix. Our debouncing algorithm does four checks before deciding that a result is valid. Most normal mechanical switches specify a 5ms debounce period. On an ATMega32U4, 1700 gets you about 5ms of debouncing.
Because keycanning is triggered by an interrupt but not run in that interrupt, the actual amount of time between scans is prone to a little bit of jitter.
*/
void setScanCycleTime(uint16_t c) {
TCCR1B = _BV(WGM13);
TCCR1A = 0;
const uint32_t cycles = (F_CPU / 2000000) * c;
ICR1 = cycles;
TCCR1B = _BV(WGM13) | _BV(CS10);
TIMSK1 = _BV(TOIE1);
}
__attribute__((optimize(3)))
void readMatrix(void) {
typename _KeyScannerProps::RowState any_debounced_changes = 0;
for (uint8_t current_row = 0; current_row < _KeyScannerProps::matrix_rows; current_row++) {
digitalWrite(_KeyScannerProps::matrix_row_pins[current_row], LOW);
typename _KeyScannerProps::RowState hot_pins = readCols();
digitalWrite(_KeyScannerProps::matrix_row_pins[current_row], HIGH);
any_debounced_changes |= debounce(hot_pins, &matrix_state_[current_row].debouncer);
if (any_debounced_changes) {
for (uint8_t current_row = 0; current_row < _KeyScannerProps::matrix_rows; current_row++) {
matrix_state_[current_row].current = matrix_state_[current_row].debouncer.debounced_state;
}
}
}
}
void scanMatrix() {
if (micros() > _KeyScannerProps::next_scan_after || micros < _KeyScannerProps::keyscan_interval) {
_KeyScannerProps::next_scan_after = micros() + _KeyScannerProps::keyscan_interval;
readMatrix();
}
actOnMatrixScan();
}
void __attribute__((optimize(3))) actOnMatrixScan() {
for (byte row = 0; row < _KeyScannerProps::matrix_rows; row++) {
for (byte col = 0; col < _KeyScannerProps::matrix_columns; col++) {
uint8_t keyState = (bitRead(matrix_state_[row].previous, col) << 0) | (bitRead(matrix_state_[row].current, col) << 1);
if (keyState) {
ThisType::handleKeyswitchEvent(Key_NoKey, typename _KeyScannerProps::KeyAddr(row, col), keyState);
}
}
matrix_state_[row].previous = matrix_state_[row].current;
}
}
uint8_t pressedKeyswitchCount() {
uint8_t count = 0;
for (int8_t r = 0; r < _KeyScannerProps::matrix_rows; r++) {
count += __builtin_popcount(matrix_state_[r].current);
}
return count;
}
bool isKeyswitchPressed(typename _KeyScannerProps::KeyAddr key_addr) {
return (bitRead(matrix_state_[key_addr.row()].current, key_addr.col()) != 0);
}
uint8_t previousPressedKeyswitchCount() {
uint8_t count = 0;
for (int8_t r = 0; r < _KeyScannerProps::matrix_rows; r++) {
count += __builtin_popcount(matrix_state_[r].previous);
}
return count;
}
bool wasKeyswitchPressed(typename _KeyScannerProps::KeyAddr key_addr) {
return (bitRead(matrix_state_[key_addr.row()].previous,
key_addr.col()) != 0);
}
bool do_scan_;
protected:
/*
each of these variables are storing the state for a row of keys
so for key 0, the counter is represented by db0[0] and db1[0]
and the state in debounced_state[0].
*/
struct debounce_t {
typename _KeyScannerProps::RowState db0; // counter bit 0
typename _KeyScannerProps::RowState db1; // counter bit 1
typename _KeyScannerProps::RowState debounced_state; // debounced state
};
struct row_state_t {
typename _KeyScannerProps::RowState previous;
typename _KeyScannerProps::RowState current;
debounce_t debouncer;
};
private:
typedef _KeyScannerProps KeyScannerProps_;
static row_state_t matrix_state_[_KeyScannerProps::matrix_rows];
/*
* This function has loop unrolling disabled on purpose: we want to give the
* hardware enough time to produce stable PIN reads for us. If we unroll the
* loop, we will not have that, because even with the NOP, the codepath is too
* fast. If we don't have stable reads, then entire rows or columns will behave
* erratically.
*
* For this reason, we ask the compiler to not unroll our loop, which in turn,
* gives hardware enough time to produce stable reads, at the cost of a little
* bit of speed.
*
* Do not remove the attribute!
*/
__attribute__((optimize("no-unroll-loops")))
typename _KeyScannerProps::RowState readCols() {
typename _KeyScannerProps::RowState hot_pins = 0;
for (uint8_t i = 0; i < _KeyScannerProps::matrix_columns; i++) {
// TODO - do we need this on gd32?
asm("NOP"); // We need to pause a beat before reading or we may read before the pin is hot
hot_pins |= (!READ_PIN(_KeyScannerProps::matrix_col_pins[i]) << i);
}
return hot_pins;
}
static inline typename _KeyScannerProps::RowState debounce(
typename _KeyScannerProps::RowState sample, debounce_t *debouncer
) {
typename _KeyScannerProps::RowState delta, changes;
// Use xor to detect changes from last stable state:
// if a key has changed, it's bit will be 1, otherwise 0
delta = sample ^ debouncer->debounced_state;
// Increment counters and reset any unchanged bits:
// increment bit 1 for all changed keys
debouncer->db1 = ((debouncer->db1) ^ (debouncer->db0)) & delta;
// increment bit 0 for all changed keys
debouncer->db0 = ~(debouncer->db0) & delta;
// Calculate returned change set: if delta is still true
// and the counter has wrapped back to 0, the key is changed.
changes = ~(~delta | (debouncer->db0) | (debouncer->db1));
// Update state: in this case use xor to flip any bit that is true in changes.
debouncer->debounced_state ^= changes;
return changes;
}
};
}
}
}

@ -0,0 +1,83 @@
/* -*- mode: c++ -*-
* Kaleidoscope-Hardware-Spacecadet -- Keyboardio Model 100 hardware support for Kaleidoscope
* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
*/
#ifdef ARDUINO_keyboardio_spacecadet
#include "Arduino.h" // for PROGMEM
#include "kaleidoscope/device/keyboardio/Spacecadet.h"
#include "kaleidoscope/device/keyboardio/GD32Keyscanner.h"
#include "kaleidoscope/key_events.h"
#include "kaleidoscope/driver/keyscanner/Base_Impl.h"
// Here, we set up aliases to the device's KeyScanner and KeyScannerProps
// in the global namespace within the scope of this file. We'll use these
// aliases to simplify some template initialization code below.
using KeyScannerProps = typename kaleidoscope::device::keyboardio::AtreusProps::KeyScannerProps;
using KeyScanner = typename kaleidoscope::device::keyboardio::AtreusProps::KeyScanner;
namespace kaleidoscope {
namespace device {
namespace keyboardio {
// `KeyScannerProps` here refers to the alias set up above. We do not need to
// prefix the `matrix_rows` and `matrix_columns` names within the array
// declaration, because those are resolved within the context of the class, so
// the `matrix_rows` in `KeyScannerProps::matrix_row_pins[matrix_rows]` gets
// resolved as `KeyScannerProps::matrix_rows`.
const uint8_t KeyScannerProps::matrix_rows;
const uint8_t KeyScannerProps::matrix_columns;
constexpr uint8_t KeyScannerProps::matrix_row_pins[matrix_rows];
constexpr uint8_t KeyScannerProps::matrix_col_pins[matrix_columns];
// `KeyScanner` here refers to the alias set up above, just like in the
// `KeyScannerProps` case above.
template<> KeyScanner::row_state_t KeyScanner::matrix_state_[KeyScannerProps::matrix_rows] = {};
// We set up the TIMER1 interrupt vector here. Due to dependency reasons, this
// cannot be in a header-only driver, and must be placed here.
//
// Timer1 is responsible for setting a property on the KeyScanner, which will
// tell it to do a scan. We use this to make sure that scans happen at roughly
// the intervals we want. We do the scan outside of the interrupt scope for
// practical reasons: guarding every codepath against interrupts that can be
// reached from the scan is far too tedious, for very little gain.
ISR(TIMER1_OVF_vect) {
Runtime.device().keyScanner().do_scan_ = true;
}
/********* Hardware plugin *********/
void Spacecadet::setup() {
SpacecadetKeyScanner::setup();
kaleidoscope::device::Base<SpacecadetProps>::setup();
}
}
}
}
#endif

@ -0,0 +1,106 @@
/* -*- mode: c++ -*-
* Kaleidoscope-Hardware-Model100 -- Keyboardio Model100 hardware support for Kaleidoscope
* Copyright (C) 2017-2021 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#ifdef ARDUINO_keyboardio_spacecadet
#ifndef EEPROM_EMULATION_SIZE
#define EEPROM_EMULATION_SIZE 4096
#endif
#include <Arduino.h>
#include "kaleidoscope/driver/keyscanner/Base.h"
#include "kaleidoscope/driver/storage/GD32Flash.h"
#include "kaleidoscope/driver/keyboardio/Model100Side.h"
#include "kaleidoscope/driver/led/Base.h"
#include "kaleidoscope/device/Base.h"
#include "kaleidoscope/driver/hid/Keyboardio.h"
#include "kaleidoscope/driver/bootloader/gd32/Base.h"
namespace kaleidoscope {
namespace device {
namespace keyboardio {
struct Model100StorageProps: public kaleidoscope::driver::storage::GD32FlashProps {
static constexpr uint16_t length = EEPROM_EMULATION_SIZE;
};
class Model100LEDDriver;
struct Model100Props : public kaleidoscope::device::BaseProps {
typedef kaleidoscope::driver::hid::KeyboardioProps HIDProps;
typedef kaleidoscope::driver::hid::Keyboardio<HIDProps> HID;
struct KeyScannerProps : public kaleidoscope::driver::keyscanner::GD32Props {
static constexpr uint8_t matrix_rows = 6;
static constexpr uint8_t matrix_columns = 21;
typedef MatrixAddr<matrix_rows, matrix_columns> KeyAddr;
static constexpr uint8_t matrix_row_pins[matrix_rows] = {PB15, PB14, PB13, PB12, PB11, PB10};
static constexpr uint8_t matrix_col_pins[matrix_columns] = {PB1, PB0,PA7,PA6,PA5,PA4,PA3,PA2,PA1,PA0,PD1,PD0,PC15,PC14,PC13, PA9,PA10, PB8, PB7, PB6,PB5, PB9};
};
typedef kaleidoscope::driver::keyscanner::GD32<KeyScannerProps> KeyScanner;
typedef Model100StorageProps StorageProps;
typedef kaleidoscope::driver::storage::GD32Flash<StorageProps> Storage;
typedef kaleidoscope::driver::bootloader::gd32::Base BootLoader;
static constexpr const char *short_name = "spacecadet";
};
class Model100 : public kaleidoscope::device::Base<Model100Props> {
public:
void setup();
static void enableHardwareTestMode();
};
} // namespace keyboardio
} // namespace device
EXPORT_DEVICE(kaleidoscope::device::keyboardio::Model100)
}
#define PER_KEY_DATA(dflt, \
r0c0, r0c2, r0c3, r0c5, r0c7, r0c9, r0c11, r0c13, r0c15, r0c17, r0c18, r0c19, \
r1c0, r1c1, r1c2, r1c3, r1c4, r1c5, r1c6, r1c7, r1c8, r1c9, r1c10, r1c11, r1c12, r1c13, r1c14, r1c15, r1c16, r1c17, r1c18, r1c19, r1c20, \
r2c0, r2c1, r2c2, r2c3, r2c4, r2c5, r2c6, r2c7, r2c8, r2c9, r2c10, r2c11, r2c12, r2c13, r2c14, r2c15, r2c16, r2c17, r2c18, r2c19, \
r3c0, r3c2, r3c3, r3c4, r3c5, r3c6, r3c7, r3c8, r3c9, r3c10, r3c11, r3c12, r3c13, r3c14, r3c15, r3c17, r3c18, r3c20, \
r4c0, r4c1, r4c2, r4c3, r4c5, r4c6, r4c7, r4c8, r4c9, r4c10, r4c11, r4c12, r4c13, r4c14, r4c15, r4c17, r4c18, r4c19, r4c20, \
r5c0, r5c1, r5c2, r5c3, r5c9, r5c15, r5c17, r5c18, r5c19, ...) \
\
\
r0c0, xxx, r0c2, r0c3, xxx, r0c5, xxx, r0c7, xxx, r0c9, xxx, r0c11, xxx, r0c13, xxx, r0c15, xxx, r0c17, r0c18, r0c19, xxx, \
r1c0, r1c1, r1c2, r1c3, r1c4, r1c5, r1c6, r1c7, r1c8, r1c9, r1c10, r1c11, r1c12, r1c13, r1c14, r1c15, r1c16, r1c17, r1c18, r1c19, r1c20, \
r2c0, r2c1, r2c2, r2c3, r2c4, r2c5, r2c6, r2c7, r2c8, r2c9, r2c10, r2c11, r2c12, r2c13, r2c14, r2c15, r2c16, r2c17, r2c18, r2c19, xxx, \
r3c0, xxx, r3c2, r3c3, r3c4, r3c5, r3c6, r3c7, r3c8, r3c9, r3c10, r3c11, r3c12, r3c13, r3c14, r3c15, xxx, r3c17, r3c18, xxx, r3c20, \
r4c0, r4c1, r4c2, r4c3, xxx, r4c5, r4c6, r4c7, r4c8, r4c9, r4c10, r4c11, r4c12, r4c13, r4c14, r4c15, xxx, r4c17, r4c18, r4c19, r4c20, \
r5c0, r5c1, r5c2, r5c3, xxx, xxx, xxx, xxx, xxx, r5c9, xxx, xxx, xxx, xxx, xxx, xxx, r5c15, xxx, r5c17, r5c18, RESTRICT_ARGS_COUNT((r5c19), 99, KEYMAP, ##__VA_ARGS__)
#endif
Loading…
Cancel
Save