# Kaleidoscope Device API internals This document is aimed at people interested in working on adding new devices - or improving support for existing ones - to Kaleidoscope. The APIs detailed here are a little bit more complex than most of the APIs our plugins provide. Nevertheless, we hope they're still reasonably easy to use, and this document is an attempt to explain some of the more intricate parts of it. ## Overview The core idea of the APIs is that to build up a device, we compose various components together, by describing their _properties_, and using fairly generic, templated helper classes with the properties as template parameters. This way, we can assemble together a device with a given _MCU_, which uses a particular _Bootloader_, some kind of _Storage_, perhaps some _LEDs_, and it will more than likely have a _key scanner_ component too. The base and helper classes provide a lot of the functionality themselves, so for a device built up from components already supported by Kaleidoscope, the amount of custom code one has to write will be minimal. ## Component details ### Device A `Device` is the topmost level component, it is the interface the rest of Kaleidoscope will work with. The [`kaleidoscope::device::Base`][k:d:Base] class is the ancestor of _all_ devices, everything derives from this. Devices that use an `ATMega32U4` MCU we also have the [`kaleidoscope::device::ATMega32U4Keyboard`][k:d:a32u4] class, which sets up some of the components that is common to all `ATMega32U4`-based devices (such as the _MCU_ and the _Storage_). [k:d:Base]:../src/kaleidoscope/device/Base.h [k:d:a32u4]: ../src/kaleidoscope/device/ATMega32U4.h As hinted at above, a device - or rather, it's `Props` - describe the components used for the device, such as the MCU, the Bootloader, the Storage driver, LEDs, and the key scanner. If any of that is unneeded, there's no need to specify them in `Props` - the defaults are all no-ops. All devices must also come with a `Props` struct, deriving from [`kaleidoscope::device::BaseProps`][k:d:BaseProps]. [k:d:BaseProps]: ../src/kaleidoscope/device/Base.h As an example, the most basic device we can have, that does nothing, would look like this: ```c++ class ExampleDevice : public kaleidoscope::device::Base {}; ``` That's not very useful, though. More often than not, we want to override at least some of the properties. In some cases, even override some of the pre-defined methods of the device. See the [base class][k:d:Base] for an up-to-date list of methods and interfaces it provides. The most often changed methods are likely to be `setup()` and the constructor, and `enableHardwareTestMode()` if the device implements a hardware test mode. The rest are wrappers around the various components described by the `Props`. In other words, the majority of customisation is in the `Props`, and in what components the device ends up using. ### MCU The heart of any device will be the main controller unit, or _MCU_ for short. The [`kaleidoscope::driver::mcu::Base`][k:d:m:Base] class is the ancestor of our MCU drivers, including [`mcu::ATMega32U4`][k:d:m:a32u4]. [k:d:m:Base]: ../src/kaleidoscope/driver/mcu/Base.h [k:d:m:a32u4]: ../src/kaleidoscope/driver/mcu/ATMega32U4.h The core firmware will use the `detachFromHost()` and `attachToHost()` methods of the MCU driver, along with `setup()`, but the driver - like any other driver - is free to have other methods, to be used by individual devices. For example, the [`ATMega32U4`][k:d:m:a32u4] driver implements a `disableJTAG()` and a `disableClockDivision()` method, which some of our devices use in their constructors. Unlike some other components, the `MCU` component has no properties. ### Bootloader Another important component of a device is a bootloader. The bootloader is the thing that allows us to re-program the keyboard without additional hardware (aptly called a programmer). As such, the [`base class`][k:d:b:Base] has a single method, `rebootBootloader()`, which our bootloader components implement. [k:d:b:Base]: ../src/kaleidoscope/bootloader/Base.h Kaleidoscope currently supports [`Catalina`][k:d:b:Catalina], [`HalfKay`][k:d:b:HalfKay], and [`FLIP`][k:d:b:FLIP] bootloaders. Please consult them for more information. In many cases, setting up the bootloader in the device props is all one needs to do. [k:d:b:Catalina]: ../src/kaleidoscope/driver/bootloader/avr/Catalina.h [k:d:b:HalfKay]: ../src/kaleidoscope/driver/bootloader/avr/HalfKay.h [k:d:b:FLIP]: ../src/kaleidoscope/driver/bootloader/avr/FLIP.h Like the _MCU_ component, the _bootloader_ does not use Props, either. ### Storage Not nearly as essential for a device is the [`Storage`][k:d:s:Base] component. Storage is for persistent storage of configuration data, such as key maps, colormaps, feature toggles, and so on. It's not a required component, but a recommended one nevertheless. This storage component is what allows apps like [Chrysalis][chrysalis] to configure some aspects of the keyboard without having to flash new firmware. The Storage API resembles the Arduino EEPROM API very closely. In fact, our [`AVREEPROM`][k:d:s:AVREEPROM] class is but a thin wrapper around that! [k:d:s:Base]: ../src/kaleidoscope/driver/storage/Base.h [chrysalis]: https://github.com/keyboardio/Chrysalis [k:d:s:AVREEPROM]: ../src/kaleidoscope/driver/storage/AVREEPROM.h The `Storage` component does use Props, one that describes the length - or size - of it. We provide an [`ATMega32U4EEPROMProps`][k:d:s:a32u4props] helper, which is preconfigured for the 1k EEPROM size of the ATMega32U4. [k:d:s:a32u4props]: ../src/kaleidoscope/driver/storage/ATMega32U4EEPROMProps.h ### LEDs [`kaleidoscope::driver::led::Base`][k:d:l:Base] [k:d:l:Base]: ../src/kaleidoscope/driver/led/Base.h ### Keyscanner [`kaleidoscope::driver::keyscanner::Base`][k:d:ks:Base] [k:d:ks:Base]: ../src/kaleidoscope/driver/keyscanner/Base.h ## Helpers [`kaleidoscope::device::ATMega32U4Keyboard`][k:d:a32u4k] [`kaleidoscope::driver::keyscanner::AVR`][k:d:ks:avr] [k:d:a32u4k]: ../src/kaleidoscope/device/ATMega32U4Keyboard.h [k:d:ks:avr]: ../src/kaleidoscope/driver/keyscanner/AVR.h ## Putting it all together To put things into perspective, and show a simple example, we'll build an imaginary mini keypad: `ATMega32U4` with `Caterina` as bootloader, no LEDs, and four keys only. ### `ImaginaryKeypad.h` ```c++ #pragma once #ifdef ARDUINO_AVR_IMAGINARY_KEYPAD #include #include "kaleidoscope/driver/keyscanner/AVR.h" #include "kaleidoscope/driver/bootloader/avr/Caterina.h" #include "kaleidoscope/device/ATMega32U4Keyboard.h" namespace kaleidoscope { namespace device { namespace imaginary { struct KeypadProps : kaleidoscope::device::ATmega32U4KeyboardProps { struct KeyScannerProps : public kaleidoscope::driver::keyscanner::ATmegaProps { static constexpr uint8_t matrix_rows = 2; static constexpr uint8_t matrix_columns = 2; typedef MatrixAddr KeyAddr; static constexpr uint8_t matrix_row_pins[matrix_rows] = {PIN_D0, PIN_D1}; static constexpr uint8_t matrix_col_pins[matrix_columns] = {PIN_C0, PIN_C1}; }; typedef kaleidoscope::driver::keyscanner::ATmega KeyScanner; typedef kaleidoscope::driver::bootloader::avr::Caterina BootLoader; static constexpr const char *short_name = "imaginary-keypad"; }; class Keypad: public kaleidoscope::device::ATmega32U4Keyboard {}; #define PER_KEY_DATA(dflt, \ R0C0, R0C1, \ R1C0, R1C1 \ ) \ R0C0, R0C1, R1C0, R1C1 } } EXPORT_DEVICE(kaleidoscope::device::imaginary::Keypad); } #endif ``` ### `ImaginaryKeypad.cpp` ```c++ #ifdef ARDUINO_AVR_IMAGINARY_KEYPAD #include // 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::imaginary::KeypadProps::KeyScannerProps; using KeyScanner = typename kaleidoscope::device::imaginary::KeypadProps::KeyScanner; namespace kaleidoscope { namespace device { namespace imaginary { // `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::state_t KeyScanner::state_ = {}; // 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; } } } } #endif ``` That's it.