├── .gitattributes ├── LICENSE ├── README.md ├── img ├── bottomprinted1.jpg ├── bottomprinted2.jpg ├── bottomprinted3.jpg ├── columns1.jpg ├── enc1.jpg ├── finished1.jpg ├── inkscape.png ├── ips1.jpg ├── ips2.jpg ├── ips3.jpg ├── ipscon.png ├── matrixcols.png ├── matrixenc.png ├── matrixrows.png ├── rows1.jpg ├── switchesencoders.jpg └── usb.png ├── src ├── L0.bmp ├── L1.bmp ├── L2.bmp ├── boot.py ├── kmk │ ├── __init__.py │ ├── ble.py │ ├── boards │ │ ├── __init__.mpy │ │ └── macropact.py │ ├── consts.py │ ├── handlers │ │ ├── __init__.mpy │ │ ├── __init__.py │ │ ├── layers.mpy │ │ ├── layers.py │ │ ├── modtap.mpy │ │ ├── modtap.py │ │ ├── sequences.mpy │ │ ├── sequences.py │ │ ├── stock.mpy │ │ └── stock.py │ ├── hid.py │ ├── internal_state.py │ ├── ips.py │ ├── key_validators.py │ ├── keys.py │ ├── kmk_keyboard.py │ ├── kmktime.py │ ├── led.py │ ├── matrix.py │ ├── preload_imports.py │ ├── rgb.py │ ├── rotary_encoder.py │ └── types.py ├── lib │ ├── adafruit_ble │ │ ├── __init__.mpy │ │ ├── advertising │ │ │ ├── __init__.mpy │ │ │ ├── adafruit.mpy │ │ │ ├── apple.mpy │ │ │ └── standard.mpy │ │ ├── attributes │ │ │ └── __init__.mpy │ │ ├── characteristics │ │ │ ├── __init__.mpy │ │ │ ├── float.mpy │ │ │ ├── int.mpy │ │ │ ├── stream.mpy │ │ │ └── string.mpy │ │ ├── services │ │ │ ├── __init__.mpy │ │ │ ├── circuitpython.mpy │ │ │ ├── microbit.py │ │ │ ├── midi.mpy │ │ │ ├── nordic.mpy │ │ │ ├── sphero.mpy │ │ │ └── standard │ │ │ │ ├── __init__.mpy │ │ │ │ ├── device_info.mpy │ │ │ │ └── hid.mpy │ │ └── uuid │ │ │ └── __init__.mpy │ ├── adafruit_st7789.mpy │ └── pulseio.py ├── main.py └── neopixel.py ├── stl ├── Bottom.stl ├── GlowInsert.stl └── TopPlate.stl └── svg └── layerstemplate.svg /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 kbjunky 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MacroPact 2 | ## rPico KMK powered macropad with IPS screen 3 | ![MacroPact](/img/finished1.jpg) 4 | Idea/Desing: [Sean Yin](https://www.coroflot.com/sean_yin/profile) 5 | 6 | Build/Coding: [kbjunky](https://github.com/kbjunky) ( In case of any problems hit me up on **Discord kbjunky#6476** or **Reddit /u/kbjunky**) 7 | 8 | ## **BOM** 9 | |Item |Count|Example| 10 | |:--- |:---:|:---:| 11 | |USB-C Female Breakout |1|[Link](https://www.aliexpress.com/item/4000314458731.html?spm=a2g0o.productlist.0.0.5fc34d38lyT0Dp&algo_pvid=bcc1cc44-d24a-45d0-9bc5-987888faddf1&aem_p4p_detail=2021072423494813891420020531900015357713&algo_exp_id=bcc1cc44-d24a-45d0-9bc5-987888faddf1-0)| 12 | |28AWG 10 Pin Flat Ribbon cable (Rainbow) |5m|[Link](https://www.aliexpress.com/item/4000512709968.html?spm=a2g0o.productlist.0.0.a1aa3503ZKJAk9&algo_pvid=f8420484-4057-4393-8598-2d7b5b90c93d&algo_exp_id=f8420484-4057-4393-8598-2d7b5b90c93d-0)| 13 | |M2/4mm heat insert |8|[Link](https://www.aliexpress.com/item/4000232858343.html?spm=a2g0o.productlist.0.0.744c388bzpfmK2&algo_pvid=b730dfaa-adef-48bd-9c4c-6387c7b94556&algo_exp_id=b730dfaa-adef-48bd-9c4c-6387c7b94556-0)| 14 | |Raspberry Pico |1|[Link](https://sg.rs-online.com/web/p/raspberry-pi/2122162/)| 15 | |WS2812 RGB Strip |min 7 diodes|[Link](https://www.aliexpress.com/item/2036819167.html?spm=a2g0o.productlist.0.0.76c874cauoQVho&algo_pvid=fdeec7a4-5fae-426a-8993-e6341adc3cab&algo_exp_id=fdeec7a4-5fae-426a-8993-e6341adc3cab-2)| 16 | |240x240 IPS/TFT 1.3" screen |1|[Link](https://www.aliexpress.com/item/32914468153.html?spm=a2g0o.productlist.0.0.2e233718vnQs0e&algo_pvid=bfdf2fcd-f326-4817-aad5-5e5e639df674&algo_exp_id=bfdf2fcd-f326-4817-aad5-5e5e639df674-13)| 17 | |12mm Rotary Encoder with switch |2|[Link](https://sg.rs-online.com/web/p/mechanical-rotary-encoders/7377742/)| 18 | |Kailh Choc V1 Switch |17|[Link](https://kailh.aliexpress.com/store/4670072?spm=a2g0o.store_pc_groupList.pcShopHead_12560924.0)| 19 | |Choc V1 Keycaps |17|[Link](https://kailh.aliexpress.com/store/4670072?spm=a2g0o.store_pc_groupList.pcShopHead_12560924.0)| 20 | |1N4148 Fast switching diode through hole |19|[Link](https://www.aliexpress.com/item/32729204179.html?spm=a2g0o.detail.1000023.37.5b88e47dXeHixn)| 21 | |0.1mm copper jumper wire |1|[Link](https://www.aliexpress.com/item/32848033421.html?spm=a2g0o.productlist.0.0.494a1553LgDudN&algo_pvid=38dbe147-1108-472d-aff1-d27e6126209c&algo_exp_id=38dbe147-1108-472d-aff1-d27e6126209c-2)| 22 | |M2/4mm flat head screw |4|[Link](https://www.aliexpress.com/item/10000181218734.html?spm=a2g0o.productlist.0.0.6020582dfAOH2E&algo_pvid=249be230-5d20-47ee-bfab-177bfda0f1ef&algo_exp_id=249be230-5d20-47ee-bfab-177bfda0f1ef-1)| 23 | |M2/8mm flat head screw |4|[Link](https://www.aliexpress.com/item/10000181218734.html?spm=a2g0o.productlist.0.0.6020582dfAOH2E&algo_pvid=249be230-5d20-47ee-bfab-177bfda0f1ef&algo_exp_id=249be230-5d20-47ee-bfab-177bfda0f1ef-1)| 24 | |15x16 Knob |1|[Link](https://www.aliexpress.com/item/1005001323668563.html?spm=a2g0o.detail.1000014.23.248e794eVvCEnx&gps-id=pcDetailBottomMoreOtherSeller&scm=1007.14976.204930.0&scm_id=1007.14976.204930.0&scm-url=1007.14976.204930.0&pvid=a1382086-ef33-42ef-a9ab-94a2c085269f&_t=gps-id:pcDetailBottomMoreOtherSeller,scm-url:1007.14976.204930.0,pvid:a1382086-ef33-42ef-a9ab-94a2c085269f,tpp_buckets:668%230%23131923%2371_668%23888%233325%231_4976%230%23204930%239_4976%232711%237538%23796_4976%233104%239653%236_4976%234052%2321623%2388_4976%233141%239887%234_668%232846%238109%231935_668%232717%237564%23648_668%231000022185%231000066059%230_668%233422%2315392%23340_4452%230%23189847%230_4452%233474%2315675%23228_4452%234862%2322449%23630_4452%233098%239599%23666_4452%233564%2316062%23426)| 25 | |25x17 Knob |1|[Link](https://www.aliexpress.com/item/1005001323668563.html?spm=a2g0o.detail.1000014.23.248e794eVvCEnx&gps-id=pcDetailBottomMoreOtherSeller&scm=1007.14976.204930.0&scm_id=1007.14976.204930.0&scm-url=1007.14976.204930.0&pvid=a1382086-ef33-42ef-a9ab-94a2c085269f&_t=gps-id:pcDetailBottomMoreOtherSeller,scm-url:1007.14976.204930.0,pvid:a1382086-ef33-42ef-a9ab-94a2c085269f,tpp_buckets:668%230%23131923%2371_668%23888%233325%231_4976%230%23204930%239_4976%232711%237538%23796_4976%233104%239653%236_4976%234052%2321623%2388_4976%233141%239887%234_668%232846%238109%231935_668%232717%237564%23648_668%231000022185%231000066059%230_668%233422%2315392%23340_4452%230%23189847%230_4452%233474%2315675%23228_4452%234862%2322449%23630_4452%233098%239599%23666_4452%233564%2316062%23426)| 26 | |6x2 antislip pads |4|[Link](https://www.aliexpress.com/item/1005001834060269.html?spm=a2g0o.productlist.0.0.1b4de9abYDw7VQ&algo_pvid=7e52f7bd-74d2-497d-89a9-5126675db393&algo_exp_id=7e52f7bd-74d2-497d-89a9-5126675db393-11)| 27 | 28 | On top of that you'll be needing: 29 | * Soldering iron 30 | * Rosin core solder wire (anything between 0.5mm to 1mm, preferable with lead 'Pb') 31 | * Sharp tool to remove supports from 3D printed parts 32 | * Wire stripper but a normal knife will also do the job 33 | * [Hot glue gun](https://www.aliexpress.com/item/1005001393578323.html?spm=a2g0o.productlist.0.0.30283cf9bNBeBF&algo_pvid=874a8176-6f37-49d3-bb75-4a4656cfd7f0&algo_exp_id=874a8176-6f37-49d3-bb75-4a4656cfd7f0-1) 34 | * [Soldering flux](https://www.aliexpress.com/item/32828595199.html?spm=a2g0o.productlist.0.0.f7cb377fEQC0uC&algo_pvid=7c56fd1b-76bf-451a-b90b-2ee1365edd5c&algo_exp_id=7c56fd1b-76bf-451a-b90b-2ee1365edd5c-0) or [flux marker](https://www.aliexpress.com/item/32828595199.html?spm=a2g0o.productlist.0.0.f7cb377fEQC0uC&algo_pvid=7c56fd1b-76bf-451a-b90b-2ee1365edd5c&algo_exp_id=7c56fd1b-76bf-451a-b90b-2ee1365edd5c-0) 35 | 36 | ### **Remarks** 37 | Be sure to order same shaft type encoder/knob. Either Knurl or Flat(D-Type). 38 | 39 | 0.1mm copper wire is used to wire the switch matrix. It's enameled so there's no risk of shorting when crossed at the same time being very easy to solder unlike tranformer core enameled wires. 40 | 41 | Keycaps can be ordered from Kailh Official store on [Aliexpress](https://kailh.aliexpress.com/store/4670072?spm=a2g0o.home.1000002.3.650c2145xJL0oJ). A better alternative is [MKUltra Set](https://mkultra.click/mbk-choc-keycaps). 42 | 43 | Use flux on wires before soldering the tip. 44 | 45 | # **Build guide** 46 | ### *Setup CircuitPython* 47 | 48 | Follow [this](https://circuitpython.org/board/raspberry_pi_pico/) guide in order to have CircuitPython up and running on your Raspberry Pico. When it's installed correctly after plugin it in you should be able to see an additional drive named CIRCUIT_PYTHON or similar. At this point you can just copy paste the content of ***src*** directory onto the newly installed drive. This will cause rPico to reboot and keyboard firmware should be running. 49 | 50 | 51 | ### *3D Printing* 52 | Print the pieces in any color that you like, but best if: 53 | * Bottom is non shine through color (only the glow insert is meant to pass the light) 54 | * Glow insert is transparent filament 55 | * Top plate is also non shine through (if you opt for a white or similar color then you'll have to cover the bottom side with tape/paint to prevent the light from shinning through) 56 | 57 | **Top plate:** 58 | 59 | * Infill 100% 60 | * Layer height 0.2 61 | * Print facing down to get a nice smooth surface (especially if you're printing on a mirror) 62 | 63 | **Bottom:** 64 | 65 | * Infill 30% 66 | * Layer height 0.1 67 | * Support **ON** 68 | 69 | **Glow insert:** 70 | 71 | * Infill 100% 72 | * Layer height 0.1 73 | 74 | ![](/img/bottomprinted1.jpg) 75 | 76 | Printed bottom part should look like this. Use a sharp tool to remove supports from USB port and glow insert slot. Use glue/hot glue to fix the the glow insert in place. 77 | 78 | ![](/img/bottomprinted2.jpg) 79 | 80 | Use soldering iron and push in the heat inserts into designated spots. 81 | 82 | At this point you can also try attaching top plate. It should clip in. Check if all holes align etc. 83 | 84 | ![](/img/bottomprinted3.jpg) 85 | 86 | ### *Wiring* 87 | Start with connecting USB-C extension. 88 | ![](/img/usb.png) 89 | 90 | Connection diagram: 91 | |rPico |USB-C| 92 | |:---: |:---:| 93 | |VBUS |V+| 94 | |TP2 |D-| 95 | |TP3 |D+| 96 | |TP1 |GND| 97 | 98 | Check if wiring is correct by plugging in the rPico and checking if the internal drive has showed up. If it's OK then screw in the MCU into the bottom part of the macropad with M2/5mm screws and insert the USB-C socket into the hole at the back. Secure it with hot glue. 99 | 100 | Insert switches, encoders into the top plate. Secure the encoders with washers and screws that came with it. Here I have used my own amoebas but you will be fine with just bare switches. 101 | 102 | ![](/img/switchesencoders.jpg) 103 | 104 | Follow the diagrams below for wiring the matrix columns. 105 | 106 | ![](/img/matrixcols.png) 107 | 108 | And matrix rows. 109 | 110 | ![](/img/matrixrows.png) 111 | 112 | This is how it looked like in my case. 113 | ![](/img/rows1.jpg) 114 | ![](/img/matrixenc.png) 115 | 116 | Now is a good time to attach the IPS screen to the top plate. Follow the below diagram in order to connect the IPS with rPico (Dotted wire is white). 117 | ![](/img/ipscon.png) 118 | 119 | Once this is done it's time to test it and align it properly. After booting the keyboard it should display visual help for the first layer (I've used a different image, just a white rectangle to mark the edges of the screen, but now when firmware is all done you can use default 1st layer visual guide). 120 | 121 | ![](/img/ips2.jpg) 122 | 123 | Align the display part of the screen with the slot and secure it with hot glue from he bottom. (Don't mind the diffrent colors on the photo it was a temporary connector back then). 124 | 125 | ![](/img/ips1.jpg) 126 | 127 | With that out of the way we can proceed and connect our matrix with the MCU. Follow the below table and diagrams posted above for proper wiring. 128 | ### **Columns** 129 | 130 | |Matrix |MCU| 131 | |:--- |:---:| 132 | |COL_0 Red |GP15| 133 | |COL_1 Green |GP14| 134 | |COL_2 Yellow |GP13| 135 | |COL_3 Black |GP12| 136 | |COL_4 White |GP11| 137 | 138 | ### **Rows** 139 | |Matrix |MCU| 140 | |:--- |:---:| 141 | |Row_0 Red |GP16| 142 | |Row_1 Green |GP17| 143 | |Row_2 Black |GP18| 144 | |Row_3 White |GP19| 145 | 146 | Last thing that is left are encoders. Clip the mounting legs on the sides, we won't be needing them (thick ones). Connect 'C' pads (the one in the middle of the side with three legs) to any GND on the MCU. Then connect pads A and B from each encoder according to this table. 147 | 148 | |Row_1 Green Encoder |MCU| 149 | |:--- |:---:| 150 | |Pad A |GP1| 151 | |Pad B |GP0| 152 | 153 | 154 | |Row_2 Black Encoder |MCU| 155 | |:--- |:---:| 156 | |Pad A |GP2| 157 | |Pad B |GP3| 158 | 159 | And last but not least RGB. Cut the amount of diodes that suits you best. I would recommend anything between 7 to 9 or 11. Wiring is quite simple. Use the GND near the data pin on the MCU. 160 | 161 | |RGB |MCU| 162 | |:--- |:---:| 163 | |VCC |VBUS| 164 | |Data |GP28| 165 | |GND |GND(AGND)| 166 | 167 | And that's it. If all went well your MacroPact should be functional. Connect it and check. If it's all good secure remaining parts with hot glue (ie. encoders, LED strip). Attach the top plate to the bottom part and screw it in with the 8mm screws. Put on the keycaps and enjoy! 168 | 169 | You can customize the layout anyway you want. You might want to check [KMK Manual](https://github.com/KMKfw/kmk_firmware/tree/master/docs) before you do. Check **'SVG'** folder for a template for visual guide that is displayed per layer. You can use [**Inkscape**](https://inkscape.org/) for editing it and then export to PNG. In order to save space you can convert exported PNG files to low color BMP. Also note that the final picture has to be rotated 90 deg CCW. 170 | 171 | You can edit the text for key/modifier(bottom left of each cell). Some modifiers are not visible because the stroke/fill is set to the background color. Change the color if you want the modifier to be visible. Use Unicode characters for the icons, there's plenty to choose from. 172 | 173 | ![](/img/inkscape.png) 174 | 175 | As a last step I recommend using rubber feet to prevent the macropad from sliding on the desk. Also a magnetic USB-C cable can come handy as it won't put this much stress on the socket and is very neat. 176 | 177 | ## And this is it, hope this guide is detailed enough. Enjoy your MacroPact and please share your build on [r/MK](https://www.reddit.com/r/MechanicalKeyboards)! 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | -------------------------------------------------------------------------------- /img/bottomprinted1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/img/bottomprinted1.jpg -------------------------------------------------------------------------------- /img/bottomprinted2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/img/bottomprinted2.jpg -------------------------------------------------------------------------------- /img/bottomprinted3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/img/bottomprinted3.jpg -------------------------------------------------------------------------------- /img/columns1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/img/columns1.jpg -------------------------------------------------------------------------------- /img/enc1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/img/enc1.jpg -------------------------------------------------------------------------------- /img/finished1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/img/finished1.jpg -------------------------------------------------------------------------------- /img/inkscape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/img/inkscape.png -------------------------------------------------------------------------------- /img/ips1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/img/ips1.jpg -------------------------------------------------------------------------------- /img/ips2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/img/ips2.jpg -------------------------------------------------------------------------------- /img/ips3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/img/ips3.jpg -------------------------------------------------------------------------------- /img/ipscon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/img/ipscon.png -------------------------------------------------------------------------------- /img/matrixcols.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/img/matrixcols.png -------------------------------------------------------------------------------- /img/matrixenc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/img/matrixenc.png -------------------------------------------------------------------------------- /img/matrixrows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/img/matrixrows.png -------------------------------------------------------------------------------- /img/rows1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/img/rows1.jpg -------------------------------------------------------------------------------- /img/switchesencoders.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/img/switchesencoders.jpg -------------------------------------------------------------------------------- /img/usb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/img/usb.png -------------------------------------------------------------------------------- /src/L0.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/src/L0.bmp -------------------------------------------------------------------------------- /src/L1.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/src/L1.bmp -------------------------------------------------------------------------------- /src/L2.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/src/L2.bmp -------------------------------------------------------------------------------- /src/boot.py: -------------------------------------------------------------------------------- 1 | import supervisor 2 | 3 | supervisor.set_next_stack_limit(4096 + 1024) 4 | -------------------------------------------------------------------------------- /src/kmk/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/src/kmk/__init__.py -------------------------------------------------------------------------------- /src/kmk/ble.py: -------------------------------------------------------------------------------- 1 | from adafruit_ble import BLERadio 2 | from adafruit_ble.advertising.standard import ProvideServicesAdvertisement 3 | from adafruit_ble.services.standard.hid import HIDService 4 | from kmk.hid import AbstractHID 5 | 6 | BLE_APPEARANCE_HID_KEYBOARD = 961 7 | # Hardcoded in CPy 8 | MAX_CONNECTIONS = 2 9 | 10 | 11 | class BLEHID(AbstractHID): 12 | def post_init(self, ble_name='KMK Keyboard', **kwargs): 13 | self.conn_id = -1 14 | 15 | self.ble = BLERadio() 16 | self.ble.name = ble_name 17 | self.hid = HIDService() 18 | self.hid.protocol_mode = 0 # Boot protocol 19 | 20 | # Security-wise this is not right. While you're away someone turns 21 | # on your keyboard and they can pair with it nice and clean and then 22 | # listen to keystrokes. 23 | # On the other hand we don't have LESC so it's like shouting your 24 | # keystrokes in the air 25 | if not self.ble.connected or not self.hid.devices: 26 | self.start_advertising() 27 | 28 | self.conn_id = 0 29 | 30 | @property 31 | def devices(self): 32 | '''Search through the provided list of devices to find the ones with the 33 | send_report attribute.''' 34 | if not self.ble.connected: 35 | return [] 36 | 37 | result = [] 38 | # Security issue: 39 | # This introduces a race condition. Let's say you have 2 active 40 | # connections: Alice and Bob - Alice is connection 1 and Bob 2. 41 | # Now Chuck who has already paired with the device in the past 42 | # (this assumption is needed only in the case of LESC) 43 | # wants to gather the keystrokes you send to Alice. You have 44 | # selected right now to talk to Alice (1) and you're typing a secret. 45 | # If Chuck kicks Alice off and is quick enough to connect to you, 46 | # which means quicker than the running interval of this function, 47 | # he'll be earlier in the `self.hid.devices` so will take over the 48 | # selected 1 position in the resulted array. 49 | # If no LESC is in place, Chuck can sniff the keystrokes anyway 50 | for device in self.hid.devices: 51 | if hasattr(device, 'send_report'): 52 | result.append(device) 53 | 54 | return result 55 | 56 | def _check_connection(self): 57 | devices = self.devices 58 | if not devices: 59 | return False 60 | 61 | if self.conn_id >= len(devices): 62 | self.conn_id = len(devices) - 1 63 | 64 | if self.conn_id < 0: 65 | return False 66 | 67 | if not devices[self.conn_id]: 68 | return False 69 | 70 | return True 71 | 72 | def hid_send(self, evt): 73 | if not self._check_connection(): 74 | return 75 | 76 | device = self.devices[self.conn_id] 77 | 78 | while len(evt) < len(device._characteristic.value) + 1: 79 | evt.append(0) 80 | 81 | return device.send_report(evt[1:]) 82 | 83 | def clear_bonds(self): 84 | import _bleio 85 | 86 | _bleio.adapter.erase_bonding() 87 | 88 | def next_connection(self): 89 | self.conn_id = (self.conn_id + 1) % len(self.devices) 90 | 91 | def previous_connection(self): 92 | self.conn_id = (self.conn_id - 1) % len(self.devices) 93 | 94 | def start_advertising(self): 95 | advertisement = ProvideServicesAdvertisement(self.hid) 96 | advertisement.appearance = BLE_APPEARANCE_HID_KEYBOARD 97 | 98 | self.ble.start_advertising(advertisement) 99 | 100 | def stop_advertising(self): 101 | self.ble.stop_advertising() 102 | -------------------------------------------------------------------------------- /src/kmk/boards/__init__.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/src/kmk/boards/__init__.mpy -------------------------------------------------------------------------------- /src/kmk/boards/macropact.py: -------------------------------------------------------------------------------- 1 | import board 2 | 3 | from kmk.kmk_keyboard import KMKKeyboard as _KMKKeyboard 4 | from kmk.matrix import DiodeOrientation 5 | from kmk.matrix import intify_coordinate as ic 6 | 7 | 8 | class KMKKeyboard(_KMKKeyboard): 9 | col_pins = (board.GP15, board.GP14, board.GP13, board.GP12, board.GP11) 10 | row_pins = (board.GP16, board.GP17, board.GP18, board.GP19) 11 | diode_orientation = DiodeOrientation.COLUMNS 12 | coord_mapping = [] 13 | coord_mapping.extend(ic(0, x) for x in range(5)) 14 | coord_mapping.extend(ic(1, x) for x in range(5)) 15 | coord_mapping.extend(ic(2, x) for x in range(5)) 16 | coord_mapping.extend(ic(3, x) for x in range(5)) 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/kmk/consts.py: -------------------------------------------------------------------------------- 1 | try: 2 | from kmk.release_info import KMK_RELEASE 3 | except Exception: 4 | KMK_RELEASE = 'copied-from-git' 5 | 6 | 7 | class UnicodeMode: 8 | NOOP = 0 9 | LINUX = IBUS = 1 10 | MACOS = OSX = RALT = 2 11 | WINC = 3 12 | 13 | 14 | class LeaderMode: 15 | TIMEOUT = 0 16 | TIMEOUT_ACTIVE = 1 17 | ENTER = 2 18 | ENTER_ACTIVE = 3 19 | -------------------------------------------------------------------------------- /src/kmk/handlers/__init__.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/src/kmk/handlers/__init__.mpy -------------------------------------------------------------------------------- /src/kmk/handlers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/src/kmk/handlers/__init__.py -------------------------------------------------------------------------------- /src/kmk/handlers/layers.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/src/kmk/handlers/layers.mpy -------------------------------------------------------------------------------- /src/kmk/handlers/layers.py: -------------------------------------------------------------------------------- 1 | from kmk.kmktime import ticks_diff, ticks_ms 2 | 3 | 4 | def df_pressed(key, state, *args, **kwargs): 5 | ''' 6 | Switches the default layer 7 | ''' 8 | state.active_layers[-1] = key.meta.layer 9 | return state 10 | 11 | 12 | def mo_pressed(key, state, *args, **kwargs): 13 | ''' 14 | Momentarily activates layer, switches off when you let go 15 | ''' 16 | state.active_layers.insert(0, key.meta.layer) 17 | return state 18 | 19 | 20 | def mo_released(key, state, KC, *args, **kwargs): 21 | # remove the first instance of the target layer 22 | # from the active list 23 | # under almost all normal use cases, this will 24 | # disable the layer (but preserve it if it was triggered 25 | # as a default layer, etc.) 26 | # this also resolves an issue where using DF() on a layer 27 | # triggered by MO() and then defaulting to the MO()'s layer 28 | # would result in no layers active 29 | try: 30 | del_idx = state.active_layers.index(key.meta.layer) 31 | del state.active_layers[del_idx] 32 | except ValueError: 33 | pass 34 | 35 | return state 36 | 37 | 38 | def lm_pressed(key, state, *args, **kwargs): 39 | ''' 40 | As MO(layer) but with mod active 41 | ''' 42 | state.hid_pending = True 43 | # Sets the timer start and acts like MO otherwise 44 | state.start_time['lm'] = ticks_ms() 45 | state.keys_pressed.add(key.meta.kc) 46 | return mo_pressed(key, state, *args, **kwargs) 47 | 48 | 49 | def lm_released(key, state, *args, **kwargs): 50 | ''' 51 | As MO(layer) but with mod active 52 | ''' 53 | state.hid_pending = True 54 | state.keys_pressed.discard(key.meta.kc) 55 | state.start_time['lm'] = None 56 | return mo_released(key, state, *args, **kwargs) 57 | 58 | 59 | def lt_pressed(key, state, *args, **kwargs): 60 | # Sets the timer start and acts like MO otherwise 61 | state.start_time['lt'] = ticks_ms() 62 | return mo_pressed(key, state, *args, **kwargs) 63 | 64 | 65 | def lt_released(key, state, *args, **kwargs): 66 | # On keyup, check timer, and press key if needed. 67 | if state.start_time['lt'] and ( 68 | ticks_diff(ticks_ms(), state.start_time['lt']) < state.config.tap_time 69 | ): 70 | state.hid_pending = True 71 | state.tap_key(key.meta.kc) 72 | 73 | mo_released(key, state, *args, **kwargs) 74 | state.start_time['lt'] = None 75 | return state 76 | 77 | 78 | def tg_pressed(key, state, *args, **kwargs): 79 | ''' 80 | Toggles the layer (enables it if not active, and vise versa) 81 | ''' 82 | # See mo_released for implementation details around this 83 | try: 84 | del_idx = state.active_layers.index(key.meta.layer) 85 | del state.active_layers[del_idx] 86 | except ValueError: 87 | state.active_layers.insert(0, key.meta.layer) 88 | 89 | return state 90 | 91 | 92 | def to_pressed(key, state, *args, **kwargs): 93 | ''' 94 | Activates layer and deactivates all other layers 95 | ''' 96 | state.active_layers.clear() 97 | state.active_layers.insert(0, key.meta.layer) 98 | 99 | return state 100 | 101 | 102 | def tt_pressed(key, state, *args, **kwargs): 103 | ''' 104 | Momentarily activates layer if held, toggles it if tapped repeatedly 105 | ''' 106 | # TODO Make this work with tap dance to function more correctly, but technically works. 107 | if state.start_time['tt'] is None: 108 | # Sets the timer start and acts like MO otherwise 109 | state.start_time['tt'] = ticks_ms() 110 | return mo_pressed(key, state, *args, **kwargs) 111 | elif ticks_diff(ticks_ms(), state.start_time['tt']) < state.config.tap_time: 112 | state.start_time['tt'] = None 113 | return tg_pressed(key, state, *args, **kwargs) 114 | 115 | 116 | def tt_released(key, state, *args, **kwargs): 117 | tap_timed_out = ( 118 | ticks_diff(ticks_ms(), state.start_time['tt']) >= state.config.tap_time 119 | ) 120 | if state.start_time['tt'] is None or tap_timed_out: 121 | # On first press, works like MO. On second press, does nothing unless let up within 122 | # time window, then acts like TG. 123 | state.start_time['tt'] = None 124 | return mo_released(key, state, *args, **kwargs) 125 | 126 | return state 127 | -------------------------------------------------------------------------------- /src/kmk/handlers/modtap.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/src/kmk/handlers/modtap.mpy -------------------------------------------------------------------------------- /src/kmk/handlers/modtap.py: -------------------------------------------------------------------------------- 1 | from kmk.kmktime import ticks_diff, ticks_ms 2 | 3 | 4 | def mt_pressed(key, state, *args, **kwargs): 5 | # Sets the timer start and acts like a modifier otherwise 6 | state.keys_pressed.add(key.meta.mods) 7 | 8 | state.start_time['mod_tap'] = ticks_ms() 9 | return state 10 | 11 | 12 | def mt_released(key, state, *args, **kwargs): 13 | # On keyup, check timer, and press key if needed. 14 | state.keys_pressed.discard(key.meta.mods) 15 | timer_name = 'mod_tap' 16 | if state.start_time[timer_name] and ( 17 | ticks_diff(ticks_ms(), state.start_time[timer_name]) < state.config.tap_time 18 | ): 19 | state.hid_pending = True 20 | state.tap_key(key.meta.kc) 21 | 22 | state.start_time[timer_name] = None 23 | return state 24 | -------------------------------------------------------------------------------- /src/kmk/handlers/sequences.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/src/kmk/handlers/sequences.mpy -------------------------------------------------------------------------------- /src/kmk/handlers/sequences.py: -------------------------------------------------------------------------------- 1 | from kmk.consts import UnicodeMode 2 | from kmk.handlers.stock import passthrough 3 | from kmk.keys import KC, make_key 4 | from kmk.types import AttrDict, KeySequenceMeta 5 | 6 | 7 | def get_wide_ordinal(char): 8 | if len(char) != 2: 9 | return ord(char) 10 | 11 | return 0x10000 + (ord(char[0]) - 0xD800) * 0x400 + (ord(char[1]) - 0xDC00) 12 | 13 | 14 | def sequence_press_handler(key, state, KC, *args, **kwargs): 15 | old_keys_pressed = state.keys_pressed 16 | state.keys_pressed = set() 17 | 18 | for ikey in key.meta.seq: 19 | if not getattr(ikey, 'no_press', None): 20 | state.process_key(ikey, True) 21 | state.config._send_hid() 22 | if not getattr(ikey, 'no_release', None): 23 | state.process_key(ikey, False) 24 | state.config._send_hid() 25 | 26 | state.keys_pressed = old_keys_pressed 27 | 28 | return state 29 | 30 | 31 | def simple_key_sequence(seq): 32 | return make_key( 33 | meta=KeySequenceMeta(seq), 34 | on_press=sequence_press_handler, 35 | on_release=passthrough, 36 | ) 37 | 38 | 39 | def send_string(message): 40 | seq = [] 41 | 42 | for char in message: 43 | kc = KC[char] 44 | 45 | if char.isupper(): 46 | kc = KC.LSHIFT(kc) 47 | 48 | seq.append(kc) 49 | 50 | return simple_key_sequence(seq) 51 | 52 | 53 | IBUS_KEY_COMBO = simple_key_sequence((KC.LCTRL(KC.LSHIFT(KC.U)),)) 54 | RALT_KEY = simple_key_sequence((KC.RALT,)) 55 | U_KEY = simple_key_sequence((KC.U,)) 56 | ENTER_KEY = simple_key_sequence((KC.ENTER,)) 57 | RALT_DOWN_NO_RELEASE = simple_key_sequence((KC.RALT(no_release=True),)) 58 | RALT_UP_NO_PRESS = simple_key_sequence((KC.RALT(no_press=True),)) 59 | 60 | 61 | def compile_unicode_string_sequences(string_table): 62 | for k, v in string_table.items(): 63 | string_table[k] = unicode_string_sequence(v) 64 | 65 | return AttrDict(string_table) 66 | 67 | 68 | def unicode_string_sequence(unistring): 69 | ''' 70 | Allows sending things like (╯°□°)╯︵ ┻━┻ directly, without 71 | manual conversion to Unicode codepoints. 72 | ''' 73 | return unicode_codepoint_sequence([hex(get_wide_ordinal(s))[2:] for s in unistring]) 74 | 75 | 76 | def generate_codepoint_keysym_seq(codepoint, expected_length=4): 77 | # To make MacOS and Windows happy, always try to send 78 | # sequences that are of length 4 at a minimum 79 | # On Linux systems, we can happily send longer strings. 80 | # They will almost certainly break on MacOS and Windows, 81 | # but this is a documentation problem more than anything. 82 | # Not sure how to send emojis on Mac/Windows like that, 83 | # though, since (for example) the Canadian flag is assembled 84 | # from two five-character codepoints, 1f1e8 and 1f1e6 85 | # 86 | # As a bonus, this function can be pretty useful for 87 | # leader dictionary keys as strings. 88 | seq = [KC.N0 for _ in range(max(len(codepoint), expected_length))] 89 | 90 | for idx, codepoint_fragment in enumerate(reversed(codepoint)): 91 | seq[-(idx + 1)] = KC.get(codepoint_fragment) 92 | 93 | return seq 94 | 95 | 96 | def generate_leader_dictionary_seq(string): 97 | return tuple(generate_codepoint_keysym_seq(string, 1)) 98 | 99 | 100 | def unicode_codepoint_sequence(codepoints): 101 | kc_seqs = (generate_codepoint_keysym_seq(codepoint) for codepoint in codepoints) 102 | 103 | kc_macros = [simple_key_sequence(kc_seq) for kc_seq in kc_seqs] 104 | 105 | def _unicode_sequence(key, state, *args, **kwargs): 106 | if state.config.unicode_mode == UnicodeMode.IBUS: 107 | state.process_key( 108 | simple_key_sequence(_ibus_unicode_sequence(kc_macros, state)), True 109 | ) 110 | elif state.config.unicode_mode == UnicodeMode.RALT: 111 | state.process_key( 112 | simple_key_sequence(_ralt_unicode_sequence(kc_macros, state)), True 113 | ) 114 | elif state.config.unicode_mode == UnicodeMode.WINC: 115 | state.process_key( 116 | simple_key_sequence(_winc_unicode_sequence(kc_macros, state)), True 117 | ) 118 | 119 | return make_key(on_press=_unicode_sequence) 120 | 121 | 122 | def _ralt_unicode_sequence(kc_macros, state): 123 | for kc_macro in kc_macros: 124 | yield RALT_DOWN_NO_RELEASE 125 | yield kc_macro 126 | yield RALT_UP_NO_PRESS 127 | 128 | 129 | def _ibus_unicode_sequence(kc_macros, state): 130 | for kc_macro in kc_macros: 131 | yield IBUS_KEY_COMBO 132 | yield kc_macro 133 | yield ENTER_KEY 134 | 135 | 136 | def _winc_unicode_sequence(kc_macros, state): 137 | ''' 138 | Send unicode sequence using WinCompose: 139 | 140 | http://wincompose.info/ 141 | https://github.com/SamHocevar/wincompose 142 | ''' 143 | for kc_macro in kc_macros: 144 | yield RALT_KEY 145 | yield U_KEY 146 | yield kc_macro 147 | -------------------------------------------------------------------------------- /src/kmk/handlers/stock.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/src/kmk/handlers/stock.mpy -------------------------------------------------------------------------------- /src/kmk/handlers/stock.py: -------------------------------------------------------------------------------- 1 | from kmk.kmktime import sleep_ms 2 | 3 | 4 | def passthrough(key, state, *args, **kwargs): 5 | return state 6 | 7 | 8 | def default_pressed(key, state, KC, coord_int=None, coord_raw=None): 9 | state.hid_pending = True 10 | 11 | if coord_int is not None: 12 | state.coord_keys_pressed[coord_int] = key 13 | 14 | state.keys_pressed.add(key) 15 | 16 | return state 17 | 18 | 19 | def default_released(key, state, KC, coord_int=None, coord_raw=None): 20 | state.hid_pending = True 21 | state.keys_pressed.discard(key) 22 | 23 | if coord_int is not None: 24 | state.keys_pressed.discard(state.coord_keys_pressed.get(coord_int, None)) 25 | state.coord_keys_pressed[coord_int] = None 26 | 27 | return state 28 | 29 | 30 | def reset(*args, **kwargs): 31 | try: 32 | import machine 33 | 34 | machine.reset() 35 | 36 | except ImportError: 37 | import microcontroller 38 | 39 | microcontroller.reset() 40 | 41 | 42 | def bootloader(*args, **kwargs): 43 | try: 44 | import machine 45 | 46 | machine.bootloader() 47 | 48 | except ImportError: 49 | import microcontroller 50 | 51 | microcontroller.on_next_reset(microcontroller.RunMode.BOOTLOADER) 52 | microcontroller.reset() 53 | 54 | 55 | def debug_pressed(key, state, KC, *args, **kwargs): 56 | if state.config.debug_enabled: 57 | print('DebugDisable()') 58 | else: 59 | print('DebugEnable()') 60 | 61 | state.config.debug_enabled = not state.config.debug_enabled 62 | 63 | return state 64 | 65 | 66 | def gesc_pressed(key, state, KC, *args, **kwargs): 67 | GESC_TRIGGERS = {KC.LSHIFT, KC.RSHIFT, KC.LGUI, KC.RGUI} 68 | 69 | if GESC_TRIGGERS.intersection(state.keys_pressed): 70 | # First, release GUI if already pressed 71 | state.config._send_hid() 72 | # if Shift is held, KC_GRAVE will become KC_TILDE on OS level 73 | state.keys_pressed.add(KC.GRAVE) 74 | state.hid_pending = True 75 | return state 76 | 77 | # else return KC_ESC 78 | state.keys_pressed.add(KC.ESCAPE) 79 | state.hid_pending = True 80 | 81 | return state 82 | 83 | 84 | def gesc_released(key, state, KC, *args, **kwargs): 85 | state.keys_pressed.discard(KC.ESCAPE) 86 | state.keys_pressed.discard(KC.GRAVE) 87 | state.hid_pending = True 88 | return state 89 | 90 | 91 | def bkdl_pressed(key, state, KC, *args, **kwargs): 92 | BKDL_TRIGGERS = {KC.LGUI, KC.RGUI} 93 | 94 | if BKDL_TRIGGERS.intersection(state.keys_pressed): 95 | state.config._send_hid() 96 | state.keys_pressed.add(KC.DEL) 97 | state.hid_pending = True 98 | return state 99 | 100 | # else return KC_ESC 101 | state.keys_pressed.add(KC.BKSP) 102 | state.hid_pending = True 103 | 104 | return state 105 | 106 | 107 | def bkdl_released(key, state, KC, *args, **kwargs): 108 | state.keys_pressed.discard(KC.BKSP) 109 | state.keys_pressed.discard(KC.DEL) 110 | state.hid_pending = True 111 | return state 112 | 113 | 114 | def sleep_pressed(key, state, KC, *args, **kwargs): 115 | sleep_ms(key.meta.ms) 116 | return state 117 | 118 | 119 | def uc_mode_pressed(key, state, *args, **kwargs): 120 | state.config.unicode_mode = key.meta.mode 121 | 122 | return state 123 | 124 | 125 | def leader_pressed(key, state, *args, **kwargs): 126 | return state._begin_leader_mode() 127 | 128 | 129 | def td_pressed(key, state, *args, **kwargs): 130 | return state._process_tap_dance(key, True) 131 | 132 | 133 | def td_released(key, state, *args, **kwargs): 134 | return state._process_tap_dance(key, False) 135 | 136 | 137 | def rgb_tog(key, state, *args, **kwargs): 138 | if state.config.pixels.animation_mode == 'static_standby': 139 | state.config.pixels.animation_mode = 'static' 140 | state.config.pixels.enabled = not state.config.pixels.enabled 141 | return state 142 | 143 | 144 | def rgb_hui(key, state, *args, **kwargs): 145 | state.config.pixels.increase_hue() 146 | return state 147 | 148 | 149 | def rgb_hud(key, state, *args, **kwargs): 150 | state.config.pixels.decrease_hue() 151 | return state 152 | 153 | 154 | def rgb_sai(key, state, *args, **kwargs): 155 | state.config.pixels.increase_sat() 156 | return state 157 | 158 | 159 | def rgb_sad(key, state, *args, **kwargs): 160 | state.config.pixels.decrease_sat() 161 | return state 162 | 163 | 164 | def rgb_vai(key, state, *args, **kwargs): 165 | state.config.pixels.increase_val() 166 | return state 167 | 168 | 169 | def rgb_vad(key, state, *args, **kwargs): 170 | state.config.pixels.decrease_val() 171 | return state 172 | 173 | 174 | def rgb_ani(key, state, *args, **kwargs): 175 | state.config.pixels.increase_ani() 176 | return state 177 | 178 | 179 | def rgb_and(key, state, *args, **kwargs): 180 | state.config.pixels.decrease_ani() 181 | return state 182 | 183 | 184 | def rgb_mode_static(key, state, *args, **kwargs): 185 | state.config.pixels.effect_init = True 186 | state.config.pixels.animation_mode = 'static' 187 | return state 188 | 189 | 190 | def rgb_mode_breathe(key, state, *args, **kwargs): 191 | state.config.pixels.effect_init = True 192 | state.config.pixels.animation_mode = 'breathing' 193 | return state 194 | 195 | 196 | def rgb_mode_breathe_rainbow(key, state, *args, **kwargs): 197 | state.config.pixels.effect_init = True 198 | state.config.pixels.animation_mode = 'breathing_rainbow' 199 | return state 200 | 201 | 202 | def rgb_mode_rainbow(key, state, *args, **kwargs): 203 | state.config.pixels.effect_init = True 204 | state.config.pixels.animation_mode = 'rainbow' 205 | return state 206 | 207 | 208 | def rgb_mode_swirl(key, state, *args, **kwargs): 209 | state.config.pixels.effect_init = True 210 | state.config.pixels.animation_mode = 'swirl' 211 | return state 212 | 213 | 214 | def rgb_mode_knight(key, state, *args, **kwargs): 215 | state.config.pixels.effect_init = True 216 | state.config.pixels.animation_mode = 'knight' 217 | return state 218 | 219 | 220 | def led_tog(key, state, *args, **kwargs): 221 | if state.config.led.animation_mode == 'static_standby': 222 | state.config.led.animation_mode = 'static' 223 | state.config.led.enabled = not state.config.led.enabled 224 | return state 225 | 226 | 227 | def led_inc(key, state, *args, **kwargs): 228 | state.config.led.increase_brightness() 229 | return state 230 | 231 | 232 | def led_dec(key, state, *args, **kwargs): 233 | state.config.led.decrease_brightness() 234 | return state 235 | 236 | 237 | def led_ani(key, state, *args, **kwargs): 238 | state.config.led.increase_ani() 239 | return state 240 | 241 | 242 | def led_and(key, state, *args, **kwargs): 243 | state.config.led.decrease_ani() 244 | return state 245 | 246 | 247 | def led_mode_static(key, state, *args, **kwargs): 248 | state.config.led.effect_init = True 249 | state.config.led.animation_mode = 'static' 250 | return state 251 | 252 | 253 | def led_mode_breathe(key, state, *args, **kwargs): 254 | state.config.led.effect_init = True 255 | state.config.led.animation_mode = 'breathing' 256 | return state 257 | 258 | 259 | def bt_clear_bonds(key, state, *args, **kwargs): 260 | state.config._hid_helper_inst.clear_bonds() 261 | return state 262 | 263 | 264 | def bt_next_conn(key, state, *args, **kwargs): 265 | state.config._hid_helper_inst.next_connection() 266 | return state 267 | 268 | 269 | def bt_prev_conn(key, state, *args, **kwargs): 270 | state.config._hid_helper_inst.previous_connection() 271 | return state 272 | -------------------------------------------------------------------------------- /src/kmk/hid.py: -------------------------------------------------------------------------------- 1 | import usb_hid 2 | 3 | from kmk.keys import FIRST_KMK_INTERNAL_KEY, ConsumerKey, ModifierKey 4 | 5 | 6 | class HIDModes: 7 | NOOP = 0 # currently unused; for testing? 8 | USB = 1 9 | BLE = 2 # currently unused; for bluetooth 10 | 11 | ALL_MODES = (NOOP, USB, BLE) 12 | 13 | 14 | class HIDReportTypes: 15 | KEYBOARD = 1 16 | MOUSE = 2 17 | CONSUMER = 3 18 | SYSCONTROL = 4 19 | 20 | 21 | class HIDUsage: 22 | KEYBOARD = 0x06 23 | MOUSE = 0x02 24 | CONSUMER = 0x01 25 | SYSCONTROL = 0x80 26 | 27 | 28 | class HIDUsagePage: 29 | CONSUMER = 0x0C 30 | KEYBOARD = MOUSE = SYSCONTROL = 0x01 31 | 32 | 33 | HID_REPORT_SIZES = { 34 | HIDReportTypes.KEYBOARD: 8, 35 | HIDReportTypes.MOUSE: 4, 36 | HIDReportTypes.CONSUMER: 2, 37 | HIDReportTypes.SYSCONTROL: 8, # TODO find the correct value for this 38 | } 39 | 40 | 41 | class AbstractHID: 42 | REPORT_BYTES = 8 43 | 44 | def __init__(self, **kwargs): 45 | self._evt = bytearray(self.REPORT_BYTES) 46 | self.report_device = memoryview(self._evt)[0:1] 47 | self.report_device[0] = HIDReportTypes.KEYBOARD 48 | 49 | # Landmine alert for HIDReportTypes.KEYBOARD: byte index 1 of this view 50 | # is "reserved" and evidently (mostly?) unused. However, other modes (or 51 | # at least consumer, so far) will use this byte, which is the main reason 52 | # this view exists. For KEYBOARD, use report_mods and report_non_mods 53 | self.report_keys = memoryview(self._evt)[1:] 54 | 55 | self.report_mods = memoryview(self._evt)[1:2] 56 | self.report_non_mods = memoryview(self._evt)[3:] 57 | 58 | self.post_init(**kwargs) 59 | 60 | def __repr__(self): 61 | return '{}(REPORT_BYTES={})'.format(self.__class__.__name__, self.REPORT_BYTES) 62 | 63 | def post_init(self): 64 | pass 65 | 66 | def create_report(self, keys_pressed): 67 | self.clear_all() 68 | 69 | consumer_key = None 70 | for key in keys_pressed: 71 | if isinstance(key, ConsumerKey): 72 | consumer_key = key 73 | break 74 | 75 | reporting_device = self.report_device[0] 76 | needed_reporting_device = HIDReportTypes.KEYBOARD 77 | 78 | if consumer_key: 79 | needed_reporting_device = HIDReportTypes.CONSUMER 80 | 81 | if reporting_device != needed_reporting_device: 82 | # If we are about to change reporting devices, release 83 | # all keys and close our proverbial tab on the existing 84 | # device, or keys will get stuck (mostly when releasing 85 | # media/consumer keys) 86 | self.send() 87 | 88 | self.report_device[0] = needed_reporting_device 89 | 90 | if consumer_key: 91 | self.add_key(consumer_key) 92 | else: 93 | for key in keys_pressed: 94 | if key.code >= FIRST_KMK_INTERNAL_KEY: 95 | continue 96 | 97 | if isinstance(key, ModifierKey): 98 | self.add_modifier(key) 99 | else: 100 | self.add_key(key) 101 | 102 | if key.has_modifiers: 103 | for mod in key.has_modifiers: 104 | self.add_modifier(mod) 105 | 106 | return self 107 | 108 | def hid_send(self, evt): 109 | # Don't raise a NotImplementedError so this can serve as our "dummy" HID 110 | # when MCU/board doesn't define one to use (which should almost always be 111 | # the CircuitPython-targeting one, except when unit testing or doing 112 | # something truly bizarre. This will likely change eventually when Bluetooth 113 | # is added) 114 | pass 115 | 116 | def send(self): 117 | self.hid_send(self._evt) 118 | 119 | return self 120 | 121 | def clear_all(self): 122 | for idx, _ in enumerate(self.report_keys): 123 | self.report_keys[idx] = 0x00 124 | 125 | return self 126 | 127 | def clear_non_modifiers(self): 128 | for idx, _ in enumerate(self.report_non_mods): 129 | self.report_non_mods[idx] = 0x00 130 | 131 | return self 132 | 133 | def add_modifier(self, modifier): 134 | if isinstance(modifier, ModifierKey): 135 | if modifier.code == ModifierKey.FAKE_CODE: 136 | for mod in modifier.has_modifiers: 137 | self.report_mods[0] |= mod 138 | else: 139 | self.report_mods[0] |= modifier.code 140 | else: 141 | self.report_mods[0] |= modifier 142 | 143 | return self 144 | 145 | def remove_modifier(self, modifier): 146 | if isinstance(modifier, ModifierKey): 147 | if modifier.code == ModifierKey.FAKE_CODE: 148 | for mod in modifier.has_modifiers: 149 | self.report_mods[0] ^= mod 150 | else: 151 | self.report_mods[0] ^= modifier.code 152 | else: 153 | self.report_mods[0] ^= modifier 154 | 155 | return self 156 | 157 | def add_key(self, key): 158 | # Try to find the first empty slot in the key report, and fill it 159 | placed = False 160 | 161 | where_to_place = self.report_non_mods 162 | 163 | if self.report_device[0] == HIDReportTypes.CONSUMER: 164 | where_to_place = self.report_keys 165 | 166 | for idx, _ in enumerate(where_to_place): 167 | if where_to_place[idx] == 0x00: 168 | where_to_place[idx] = key.code 169 | placed = True 170 | break 171 | 172 | if not placed: 173 | # TODO what do we do here?...... 174 | pass 175 | 176 | return self 177 | 178 | def remove_key(self, key): 179 | where_to_place = self.report_non_mods 180 | 181 | if self.report_device[0] == HIDReportTypes.CONSUMER: 182 | where_to_place = self.report_keys 183 | 184 | for idx, _ in enumerate(where_to_place): 185 | if where_to_place[idx] == key.code: 186 | where_to_place[idx] = 0x00 187 | 188 | return self 189 | 190 | 191 | class USBHID(AbstractHID): 192 | REPORT_BYTES = 9 193 | 194 | def post_init(self, **kwargs): 195 | self.devices = {} 196 | 197 | for device in usb_hid.devices: 198 | us = device.usage 199 | up = device.usage_page 200 | 201 | if up == HIDUsagePage.CONSUMER and us == HIDUsage.CONSUMER: 202 | self.devices[HIDReportTypes.CONSUMER] = device 203 | continue 204 | 205 | if up == HIDUsagePage.KEYBOARD and us == HIDUsage.KEYBOARD: 206 | self.devices[HIDReportTypes.KEYBOARD] = device 207 | continue 208 | 209 | if up == HIDUsagePage.MOUSE and us == HIDUsage.MOUSE: 210 | self.devices[HIDReportTypes.MOUSE] = device 211 | continue 212 | 213 | if up == HIDUsagePage.SYSCONTROL and us == HIDUsage.SYSCONTROL: 214 | self.devices[HIDReportTypes.SYSCONTROL] = device 215 | continue 216 | 217 | def hid_send(self, evt): 218 | # int, can be looked up in HIDReportTypes 219 | reporting_device_const = self.report_device[0] 220 | 221 | return self.devices[reporting_device_const].send_report( 222 | evt[1 : HID_REPORT_SIZES[reporting_device_const] + 1] 223 | ) 224 | -------------------------------------------------------------------------------- /src/kmk/internal_state.py: -------------------------------------------------------------------------------- 1 | from kmk.consts import LeaderMode 2 | from kmk.keys import KC 3 | from kmk.kmktime import ticks_ms 4 | from kmk.matrix import intify_coordinate 5 | from kmk.types import TapDanceKeyMeta 6 | 7 | 8 | class InternalState: 9 | keys_pressed = set() 10 | coord_keys_pressed = {} 11 | leader_pending = None 12 | leader_last_len = 0 13 | hid_pending = False 14 | leader_mode_history = [] 15 | 16 | # this should almost always be PREpended to, replaces 17 | # former use of reversed_active_layers which had pointless 18 | # overhead (the underlying list was never used anyway) 19 | active_layers = [0] 20 | 21 | start_time = {'lt': None, 'tg': None, 'tt': None, 'lm': None, 'leader': None} 22 | timeouts = {} 23 | tapping = False 24 | tap_dance_counts = {} 25 | tap_side_effects = {} 26 | 27 | def __init__(self, config): 28 | self.config = config 29 | 30 | def __repr__(self): 31 | return ( 32 | 'InternalState(' 33 | 'keys_pressed={} ' 34 | 'coord_keys_pressed={} ' 35 | 'leader_pending={} ' 36 | 'leader_last_len={} ' 37 | 'hid_pending={} ' 38 | 'leader_mode_history={} ' 39 | 'active_layers={} ' 40 | 'start_time={} ' 41 | 'timeouts={} ' 42 | 'tapping={} ' 43 | 'tap_dance_counts={} ' 44 | 'tap_side_effects={}' 45 | ')' 46 | ).format( 47 | self.keys_pressed, 48 | self.coord_keys_pressed, 49 | self.leader_pending, 50 | self.leader_last_len, 51 | self.hid_pending, 52 | self.leader_mode_history, 53 | self.active_layers, 54 | self.start_time, 55 | self.timeouts, 56 | self.tapping, 57 | self.tap_dance_counts, 58 | self.tap_side_effects, 59 | ) 60 | 61 | def _find_key_in_map(self, row, col): 62 | ic = intify_coordinate(row, col) 63 | 64 | try: 65 | idx = self.config.coord_mapping.index(ic) 66 | except ValueError: 67 | if self.config.debug_enabled: 68 | print( 69 | 'CoordMappingNotFound(ic={}, row={}, col={})'.format(ic, row, col) 70 | ) 71 | 72 | return None 73 | 74 | for layer in self.active_layers: 75 | layer_key = self.config.keymap[layer][idx] 76 | 77 | if not layer_key or layer_key == KC.TRNS: 78 | continue 79 | 80 | if self.config.debug_enabled: 81 | print('KeyResolution(key={})'.format(layer_key)) 82 | 83 | return layer_key 84 | 85 | def set_timeout(self, after_ticks, callback): 86 | if after_ticks is False: 87 | # We allow passing False as an implicit "run this on the next process timeouts cycle" 88 | timeout_key = ticks_ms() 89 | else: 90 | timeout_key = ticks_ms() + after_ticks 91 | 92 | while timeout_key in self.timeouts: 93 | timeout_key += 1 94 | 95 | self.timeouts[timeout_key] = callback 96 | return timeout_key 97 | 98 | def cancel_timeout(self, timeout_key): 99 | if timeout_key in self.timeouts: 100 | del self.timeouts[timeout_key] 101 | 102 | def process_timeouts(self): 103 | if not self.timeouts: 104 | return self 105 | 106 | current_time = ticks_ms() 107 | 108 | # cast this to a tuple to ensure that if a callback itself sets 109 | # timeouts, we do not handle them on the current cycle 110 | timeouts = tuple(self.timeouts.items()) 111 | 112 | for k, v in timeouts: 113 | if k <= current_time: 114 | v() 115 | del self.timeouts[k] 116 | 117 | return self 118 | 119 | def matrix_changed(self, row, col, is_pressed): 120 | if self.config.debug_enabled: 121 | print('MatrixChange(col={} row={} pressed={})'.format(col, row, is_pressed)) 122 | 123 | int_coord = intify_coordinate(row, col) 124 | kc_changed = self._find_key_in_map(row, col) 125 | 126 | if kc_changed is None: 127 | print('MatrixUndefinedCoordinate(col={} row={})'.format(col, row)) 128 | return self 129 | 130 | return self.process_key(kc_changed, is_pressed, int_coord, (row, col)) 131 | 132 | def process_key(self, key, is_pressed, coord_int=None, coord_raw=None): 133 | if self.tapping and not isinstance(key.meta, TapDanceKeyMeta): 134 | self._process_tap_dance(key, is_pressed) 135 | else: 136 | if is_pressed: 137 | key._on_press(self, coord_int, coord_raw) 138 | else: 139 | key._on_release(self, coord_int, coord_raw) 140 | 141 | if self.config.leader_mode % 2 == 1: 142 | self._process_leader_mode() 143 | 144 | return self 145 | 146 | def remove_key(self, keycode): 147 | self.keys_pressed.discard(keycode) 148 | return self.process_key(keycode, False) 149 | 150 | def add_key(self, keycode): 151 | self.keys_pressed.add(keycode) 152 | return self.process_key(keycode, True) 153 | 154 | def tap_key(self, keycode): 155 | self.add_key(keycode) 156 | # On the next cycle, we'll remove the key. 157 | self.set_timeout(False, lambda: self.remove_key(keycode)) 158 | 159 | return self 160 | 161 | def resolve_hid(self): 162 | self.hid_pending = False 163 | return self 164 | 165 | def _process_tap_dance(self, changed_key, is_pressed): 166 | if is_pressed: 167 | if not isinstance(changed_key.meta, TapDanceKeyMeta): 168 | # If we get here, changed_key is not a TapDanceKey and thus 169 | # the user kept typing elsewhere (presumably). End ALL of the 170 | # currently outstanding tap dance runs. 171 | for k, v in self.tap_dance_counts.items(): 172 | if v: 173 | self._end_tap_dance(k) 174 | 175 | return self 176 | 177 | if ( 178 | changed_key not in self.tap_dance_counts 179 | or not self.tap_dance_counts[changed_key] 180 | ): 181 | self.tap_dance_counts[changed_key] = 1 182 | self.set_timeout( 183 | self.config.tap_time, lambda: self._end_tap_dance(changed_key) 184 | ) 185 | self.tapping = True 186 | else: 187 | self.tap_dance_counts[changed_key] += 1 188 | 189 | if changed_key not in self.tap_side_effects: 190 | self.tap_side_effects[changed_key] = None 191 | else: 192 | has_side_effects = self.tap_side_effects[changed_key] is not None 193 | hit_max_defined_taps = self.tap_dance_counts[changed_key] == len( 194 | changed_key.codes 195 | ) 196 | 197 | if has_side_effects or hit_max_defined_taps: 198 | self._end_tap_dance(changed_key) 199 | 200 | return self 201 | 202 | def _end_tap_dance(self, td_key): 203 | v = self.tap_dance_counts[td_key] - 1 204 | 205 | if v >= 0: 206 | if td_key in self.keys_pressed: 207 | key_to_press = td_key.codes[v] 208 | self.add_key(key_to_press) 209 | self.tap_side_effects[td_key] = key_to_press 210 | self.hid_pending = True 211 | else: 212 | if self.tap_side_effects[td_key]: 213 | self.remove_key(self.tap_side_effects[td_key]) 214 | self.tap_side_effects[td_key] = None 215 | self.hid_pending = True 216 | self._cleanup_tap_dance(td_key) 217 | else: 218 | self.tap_key(td_key.codes[v]) 219 | self._cleanup_tap_dance(td_key) 220 | 221 | return self 222 | 223 | def _cleanup_tap_dance(self, td_key): 224 | self.tap_dance_counts[td_key] = 0 225 | self.tapping = any(count > 0 for count in self.tap_dance_counts.values()) 226 | return self 227 | 228 | def _begin_leader_mode(self): 229 | if self.config.leader_mode % 2 == 0: 230 | self.keys_pressed.discard(KC.LEAD) 231 | # All leader modes are one number higher when activating 232 | self.config.leader_mode += 1 233 | 234 | if self.config.leader_mode == LeaderMode.TIMEOUT_ACTIVE: 235 | self.set_timeout( 236 | self.config.leader_timeout, self._handle_leader_sequence 237 | ) 238 | 239 | return self 240 | 241 | def _handle_leader_sequence(self): 242 | lmh = tuple(self.leader_mode_history) 243 | # Will get caught in infinite processing loops if we don't 244 | # exit leader mode before processing the target key 245 | self._exit_leader_mode() 246 | 247 | if lmh in self.config.leader_dictionary: 248 | # Stack depth exceeded if try to use add_key here with a unicode sequence 249 | self.process_key(self.config.leader_dictionary[lmh], True) 250 | 251 | self.set_timeout( 252 | False, lambda: self.remove_key(self.config.leader_dictionary[lmh]) 253 | ) 254 | 255 | return self 256 | 257 | def _process_leader_mode(self): 258 | keys_pressed = self.keys_pressed 259 | 260 | if self.leader_last_len and self.leader_mode_history: 261 | history_set = set(self.leader_mode_history) 262 | 263 | keys_pressed = keys_pressed - history_set 264 | 265 | self.leader_last_len = len(self.keys_pressed) 266 | 267 | for key in keys_pressed: 268 | if self.config.leader_mode == LeaderMode.ENTER_ACTIVE and key == KC.ENT: 269 | self._handle_leader_sequence() 270 | break 271 | elif key == KC.ESC or key == KC.GESC: 272 | # Clean self and turn leader mode off. 273 | self._exit_leader_mode() 274 | break 275 | elif key == KC.LEAD: 276 | break 277 | else: 278 | # Add key if not needing to escape 279 | # This needs replaced later with a proper debounce 280 | self.leader_mode_history.append(key) 281 | 282 | self.hid_pending = False 283 | return self 284 | 285 | def _exit_leader_mode(self): 286 | self.leader_mode_history.clear() 287 | self.config.leader_mode -= 1 288 | self.leader_last_len = 0 289 | self.keys_pressed.clear() 290 | return self 291 | -------------------------------------------------------------------------------- /src/kmk/ips.py: -------------------------------------------------------------------------------- 1 | import board 2 | import displayio 3 | import busio 4 | import adafruit_st7789 5 | 6 | 7 | 8 | ips_config = { 9 | 'tft_cs' : board.GP20, 10 | 'tft_dc' : board.GP22, 11 | 'tft_res' : board.GP21, 12 | 'spi_mosi' : board.GP7, 13 | 'spi_clk' : board.GP6} 14 | 15 | class IPS: 16 | def __init__(self): 17 | displayio.release_displays() 18 | self.spi = busio.SPI(ips_config['spi_clk'], MOSI=ips_config['spi_mosi']) 19 | self.display_bus = displayio.FourWire(self.spi, command=ips_config['tft_dc'], chip_select=ips_config['tft_cs'], reset=ips_config['tft_res'], polarity=1, phase=0) 20 | self.display = adafruit_st7789.ST7789(self.display_bus, width=240, height=240, rowstart=80, colstart=0) 21 | self.splash = displayio.Group(max_size=10) 22 | self.display.show(self.splash) 23 | 24 | def load_bitmap(self, bitmap_file): 25 | with open(bitmap_file, "rb") as f: 26 | odb = displayio.OnDiskBitmap(f) 27 | face = displayio.TileGrid(odb, pixel_shader=displayio.ColorConverter()) 28 | self.splash.append(face) 29 | self.display.refresh() -------------------------------------------------------------------------------- /src/kmk/key_validators.py: -------------------------------------------------------------------------------- 1 | from kmk.types import ( 2 | KeySeqSleepMeta, 3 | LayerKeyMeta, 4 | ModTapKeyMeta, 5 | TapDanceKeyMeta, 6 | UnicodeModeKeyMeta, 7 | ) 8 | 9 | 10 | def key_seq_sleep_validator(ms): 11 | return KeySeqSleepMeta(ms) 12 | 13 | 14 | def layer_key_validator(layer, kc=None): 15 | ''' 16 | Validates the syntax (but not semantics) of a layer key call. We won't 17 | have access to the keymap here, so we can't verify much of anything useful 18 | here (like whether the target layer actually exists). The spirit of this 19 | existing is mostly that Python will catch extraneous args/kwargs and error 20 | out. 21 | ''' 22 | return LayerKeyMeta(layer=layer, kc=kc) 23 | 24 | 25 | def mod_tap_validator(kc, mods=None): 26 | ''' 27 | Validates that mod tap keys are correctly used 28 | ''' 29 | return ModTapKeyMeta(kc=kc, mods=mods) 30 | 31 | 32 | def tap_dance_key_validator(*codes): 33 | return TapDanceKeyMeta(codes) 34 | 35 | 36 | def unicode_mode_key_validator(mode): 37 | return UnicodeModeKeyMeta(mode) 38 | -------------------------------------------------------------------------------- /src/kmk/keys.py: -------------------------------------------------------------------------------- 1 | import kmk.handlers.layers as layers 2 | import kmk.handlers.modtap as modtap 3 | import kmk.handlers.stock as handlers 4 | from kmk.consts import UnicodeMode 5 | from kmk.key_validators import ( 6 | key_seq_sleep_validator, 7 | layer_key_validator, 8 | mod_tap_validator, 9 | tap_dance_key_validator, 10 | unicode_mode_key_validator, 11 | ) 12 | from kmk.types import AttrDict, UnicodeModeKeyMeta 13 | 14 | FIRST_KMK_INTERNAL_KEY = 1000 15 | NEXT_AVAILABLE_KEY = 1000 16 | 17 | KEY_SIMPLE = 0 18 | KEY_MODIFIER = 1 19 | KEY_CONSUMER = 2 20 | 21 | # Global state, will be filled in througout this file, and 22 | # anywhere the user creates custom keys 23 | KC = AttrDict() 24 | 25 | 26 | class Key: 27 | def __init__( 28 | self, 29 | code, 30 | has_modifiers=None, 31 | no_press=False, 32 | no_release=False, 33 | on_press=handlers.default_pressed, 34 | on_release=handlers.default_released, 35 | meta=object(), 36 | ): 37 | self.code = code 38 | self.has_modifiers = has_modifiers 39 | # cast to bool() in case we get a None value 40 | self.no_press = bool(no_press) 41 | self.no_release = bool(no_press) 42 | 43 | self._pre_press_handlers = [] 44 | self._post_press_handlers = [] 45 | self._pre_release_handlers = [] 46 | self._post_release_handlers = [] 47 | self._handle_press = on_press 48 | self._handle_release = on_release 49 | self.meta = meta 50 | 51 | def __call__(self, no_press=None, no_release=None): 52 | if no_press is None and no_release is None: 53 | return self 54 | 55 | return Key( 56 | code=self.code, 57 | has_modifiers=self.has_modifiers, 58 | no_press=no_press, 59 | no_release=no_release, 60 | ) 61 | 62 | def __repr__(self): 63 | return 'Key(code={}, has_modifiers={})'.format(self.code, self.has_modifiers) 64 | 65 | def _on_press(self, state, coord_int, coord_raw): 66 | for fn in self._pre_press_handlers: 67 | if not fn(self, state, KC, coord_int, coord_raw): 68 | return None 69 | 70 | ret = self._handle_press(self, state, KC, coord_int, coord_raw) 71 | 72 | for fn in self._post_press_handlers: 73 | fn(self, state, KC, coord_int, coord_raw) 74 | 75 | return ret 76 | 77 | def _on_release(self, state, coord_int, coord_raw): 78 | for fn in self._pre_release_handlers: 79 | if not fn(self, state, KC, coord_int, coord_raw): 80 | return None 81 | 82 | ret = self._handle_release(self, state, KC, coord_int, coord_raw) 83 | 84 | for fn in self._post_release_handlers: 85 | fn(self, state, KC, coord_int, coord_raw) 86 | 87 | return ret 88 | 89 | def clone(self): 90 | ''' 91 | Return a shallow clone of the current key without any pre/post press/release 92 | handlers attached. Almost exclusively useful for creating non-colliding keys 93 | to use such handlers. 94 | ''' 95 | 96 | return type(self)( 97 | code=self.code, 98 | has_modifiers=self.has_modifiers, 99 | no_press=self.no_press, 100 | no_release=self.no_release, 101 | on_press=self._handle_press, 102 | on_release=self._handle_release, 103 | meta=self.meta, 104 | ) 105 | 106 | def before_press_handler(self, fn): 107 | ''' 108 | Attach a callback to be run prior to the on_press handler for this key. 109 | Receives the following: 110 | 111 | - self (this Key instance) 112 | - state (the current InternalState) 113 | - KC (the global KC lookup table, for convenience) 114 | - coord_int (an internal integer representation of the matrix coordinate 115 | for the pressed key - this is likely not useful to end users, but is 116 | provided for consistency with the internal handlers) 117 | - coord_raw (an X,Y tuple of the matrix coordinate - also likely not useful) 118 | 119 | If return value of the provided callback is evaluated to False, press 120 | processing is cancelled. Exceptions are _not_ caught, and will likely 121 | crash KMK if not handled within your function. 122 | 123 | These handlers are run in attachment order: handlers provided by earlier 124 | calls of this method will be executed before those provided by later calls. 125 | ''' 126 | 127 | self._pre_press_handlers.append(fn) 128 | return self 129 | 130 | def after_press_handler(self, fn): 131 | ''' 132 | Attach a callback to be run after the on_release handler for this key. 133 | Receives the following: 134 | 135 | - self (this Key instance) 136 | - state (the current InternalState) 137 | - KC (the global KC lookup table, for convenience) 138 | - coord_int (an internal integer representation of the matrix coordinate 139 | for the pressed key - this is likely not useful to end users, but is 140 | provided for consistency with the internal handlers) 141 | - coord_raw (an X,Y tuple of the matrix coordinate - also likely not useful) 142 | 143 | The return value of the provided callback is discarded. Exceptions are _not_ 144 | caught, and will likely crash KMK if not handled within your function. 145 | 146 | These handlers are run in attachment order: handlers provided by earlier 147 | calls of this method will be executed before those provided by later calls. 148 | ''' 149 | 150 | self._post_press_handlers.append(fn) 151 | return self 152 | 153 | def before_release_handler(self, fn): 154 | ''' 155 | Attach a callback to be run prior to the on_release handler for this 156 | key. Receives the following: 157 | 158 | - self (this Key instance) 159 | - state (the current InternalState) 160 | - KC (the global KC lookup table, for convenience) 161 | - coord_int (an internal integer representation of the matrix coordinate 162 | for the pressed key - this is likely not useful to end users, but is 163 | provided for consistency with the internal handlers) 164 | - coord_raw (an X,Y tuple of the matrix coordinate - also likely not useful) 165 | 166 | If return value of the provided callback evaluates to False, the release 167 | processing is cancelled. Exceptions are _not_ caught, and will likely crash 168 | KMK if not handled within your function. 169 | 170 | These handlers are run in attachment order: handlers provided by earlier 171 | calls of this method will be executed before those provided by later calls. 172 | ''' 173 | 174 | self._pre_release_handlers.append(fn) 175 | return self 176 | 177 | def after_release_handler(self, fn): 178 | ''' 179 | Attach a callback to be run after the on_release handler for this key. 180 | Receives the following: 181 | 182 | - self (this Key instance) 183 | - state (the current InternalState) 184 | - KC (the global KC lookup table, for convenience) 185 | - coord_int (an internal integer representation of the matrix coordinate 186 | for the pressed key - this is likely not useful to end users, but is 187 | provided for consistency with the internal handlers) 188 | - coord_raw (an X,Y tuple of the matrix coordinate - also likely not useful) 189 | 190 | The return value of the provided callback is discarded. Exceptions are _not_ 191 | caught, and will likely crash KMK if not handled within your function. 192 | 193 | These handlers are run in attachment order: handlers provided by earlier 194 | calls of this method will be executed before those provided by later calls. 195 | ''' 196 | 197 | self._post_release_handlers.append(fn) 198 | return self 199 | 200 | 201 | class ModifierKey(Key): 202 | # FIXME this is atrocious to read. Please, please, please, strike down upon 203 | # this with great vengeance and furious anger. 204 | 205 | FAKE_CODE = -1 206 | 207 | def __call__(self, modified_code=None, no_press=None, no_release=None): 208 | if modified_code is None and no_press is None and no_release is None: 209 | return self 210 | 211 | if modified_code is not None: 212 | if isinstance(modified_code, ModifierKey): 213 | new_keycode = ModifierKey( 214 | ModifierKey.FAKE_CODE, 215 | set() if self.has_modifiers is None else self.has_modifiers, 216 | no_press=no_press, 217 | no_release=no_release, 218 | ) 219 | 220 | if self.code != ModifierKey.FAKE_CODE: 221 | new_keycode.has_modifiers.add(self.code) 222 | 223 | if modified_code.code != ModifierKey.FAKE_CODE: 224 | new_keycode.has_modifiers.add(modified_code.code) 225 | else: 226 | new_keycode = Key( 227 | modified_code.code, 228 | {self.code}, 229 | no_press=no_press, 230 | no_release=no_release, 231 | ) 232 | 233 | if modified_code.has_modifiers: 234 | new_keycode.has_modifiers |= modified_code.has_modifiers 235 | else: 236 | new_keycode = Key(self.code, no_press=no_press, no_release=no_release) 237 | 238 | return new_keycode 239 | 240 | def __repr__(self): 241 | return 'ModifierKey(code={}, has_modifiers={})'.format( 242 | self.code, self.has_modifiers 243 | ) 244 | 245 | 246 | class ConsumerKey(Key): 247 | pass 248 | 249 | 250 | def register_key_names(key, names=tuple()): # NOQA 251 | ''' 252 | Names are globally unique. If a later key is created with 253 | the same name as an existing entry in `KC`, it will overwrite 254 | the existing entry. 255 | 256 | If a name entry is only a single letter, its entry in the KC 257 | object will not be case-sensitive (meaning `names=('A',)` is 258 | sufficient to create a key accessible by both `KC.A` and `KC.a`). 259 | ''' 260 | 261 | for name in names: 262 | KC[name] = key 263 | 264 | if len(name) == 1: 265 | KC[name.upper()] = key 266 | KC[name.lower()] = key 267 | 268 | return key 269 | 270 | 271 | def make_key(code=None, names=tuple(), type=KEY_SIMPLE, **kwargs): # NOQA 272 | ''' 273 | Create a new key, aliased by `names` in the KC lookup table. 274 | 275 | If a code is not specified, the key is assumed to be a custom 276 | internal key to be handled in a state callback rather than 277 | sent directly to the OS. These codes will autoincrement. 278 | 279 | See register_key_names() for details on the assignment. 280 | 281 | All **kwargs are passed to the Key constructor 282 | ''' 283 | 284 | global NEXT_AVAILABLE_KEY 285 | 286 | if type == KEY_SIMPLE: 287 | constructor = Key 288 | elif type == KEY_MODIFIER: 289 | constructor = ModifierKey 290 | elif type == KEY_CONSUMER: 291 | constructor = ConsumerKey 292 | else: 293 | raise ValueError('Unrecognized key type') 294 | 295 | if code is None: 296 | code = NEXT_AVAILABLE_KEY 297 | NEXT_AVAILABLE_KEY += 1 298 | elif code >= FIRST_KMK_INTERNAL_KEY: 299 | # Try to ensure future auto-generated internal keycodes won't 300 | # be overridden by continuing to +1 the sequence from the provided 301 | # code 302 | NEXT_AVAILABLE_KEY = max(NEXT_AVAILABLE_KEY, code + 1) 303 | 304 | key = constructor(code=code, **kwargs) 305 | 306 | register_key_names(key, names) 307 | 308 | return key 309 | 310 | 311 | def make_mod_key(*args, **kwargs): 312 | return make_key(*args, **kwargs, type=KEY_MODIFIER) 313 | 314 | 315 | def make_shifted_key(target_name, names=tuple()): # NOQA 316 | key = KC.LSFT(KC[target_name]) 317 | 318 | register_key_names(key, names) 319 | 320 | return key 321 | 322 | 323 | def make_consumer_key(*args, **kwargs): 324 | return make_key(*args, **kwargs, type=KEY_CONSUMER) 325 | 326 | 327 | # Argumented keys are implicitly internal, so auto-gen of code 328 | # is almost certainly the best plan here 329 | def make_argumented_key( 330 | validator=lambda *validator_args, **validator_kwargs: object(), 331 | names=tuple(), # NOQA 332 | *constructor_args, 333 | **constructor_kwargs, 334 | ): 335 | global NEXT_AVAILABLE_KEY 336 | 337 | def _argumented_key(*user_args, **user_kwargs): 338 | global NEXT_AVAILABLE_KEY 339 | 340 | meta = validator(*user_args, **user_kwargs) 341 | 342 | if meta: 343 | key = Key( 344 | NEXT_AVAILABLE_KEY, meta=meta, *constructor_args, **constructor_kwargs 345 | ) 346 | 347 | NEXT_AVAILABLE_KEY += 1 348 | 349 | return key 350 | 351 | else: 352 | raise ValueError( 353 | 'Argumented key validator failed for unknown reasons. ' 354 | "This may not be the keymap's fault, as a more specific error " 355 | 'should have been raised.' 356 | ) 357 | 358 | for name in names: 359 | KC[name] = _argumented_key 360 | 361 | return _argumented_key 362 | 363 | 364 | # Modifiers 365 | make_mod_key(code=0x01, names=('LEFT_CONTROL', 'LCTRL', 'LCTL')) 366 | make_mod_key(code=0x02, names=('LEFT_SHIFT', 'LSHIFT', 'LSFT')) 367 | make_mod_key(code=0x04, names=('LEFT_ALT', 'LALT')) 368 | make_mod_key(code=0x08, names=('LEFT_SUPER', 'LGUI', 'LCMD', 'LWIN')) 369 | make_mod_key(code=0x10, names=('RIGHT_CONTROL', 'RCTRL', 'RCTL')) 370 | make_mod_key(code=0x20, names=('RIGHT_SHIFT', 'RSHIFT', 'RSFT')) 371 | make_mod_key(code=0x40, names=('RIGHT_ALT', 'RALT')) 372 | make_mod_key(code=0x80, names=('RIGHT_SUPER', 'RGUI', 'RCMD', 'RWIN')) 373 | # MEH = LCTL | LALT | LSFT 374 | make_mod_key(code=0x07, names=('MEH',)) 375 | # HYPR = LCTL | LALT | LSFT | LGUI 376 | make_mod_key(code=0x0F, names=('HYPER', 'HYPR')) 377 | 378 | # Basic ASCII letters 379 | make_key(code=4, names=('A',)) 380 | make_key(code=5, names=('B',)) 381 | make_key(code=6, names=('C',)) 382 | make_key(code=7, names=('D',)) 383 | make_key(code=8, names=('E',)) 384 | make_key(code=9, names=('F',)) 385 | make_key(code=10, names=('G',)) 386 | make_key(code=11, names=('H',)) 387 | make_key(code=12, names=('I',)) 388 | make_key(code=13, names=('J',)) 389 | make_key(code=14, names=('K',)) 390 | make_key(code=15, names=('L',)) 391 | make_key(code=16, names=('M',)) 392 | make_key(code=17, names=('N',)) 393 | make_key(code=18, names=('O',)) 394 | make_key(code=19, names=('P',)) 395 | make_key(code=20, names=('Q',)) 396 | make_key(code=21, names=('R',)) 397 | make_key(code=22, names=('S',)) 398 | make_key(code=23, names=('T',)) 399 | make_key(code=24, names=('U',)) 400 | make_key(code=25, names=('V',)) 401 | make_key(code=26, names=('W',)) 402 | make_key(code=27, names=('X',)) 403 | make_key(code=28, names=('Y',)) 404 | make_key(code=29, names=('Z',)) 405 | 406 | # Numbers 407 | # Aliases to play nicely with AttrDict, since KC.1 isn't a valid 408 | # attribute key in Python, but KC.N1 is 409 | make_key(code=30, names=('1', 'N1')) 410 | make_key(code=31, names=('2', 'N2')) 411 | make_key(code=32, names=('3', 'N3')) 412 | make_key(code=33, names=('4', 'N4')) 413 | make_key(code=34, names=('5', 'N5')) 414 | make_key(code=35, names=('6', 'N6')) 415 | make_key(code=36, names=('7', 'N7')) 416 | make_key(code=37, names=('8', 'N8')) 417 | make_key(code=38, names=('9', 'N9')) 418 | make_key(code=39, names=('0', 'N0')) 419 | 420 | # More ASCII standard keys 421 | make_key(code=40, names=('ENTER', 'ENT', '\n')) 422 | make_key(code=41, names=('ESCAPE', 'ESC')) 423 | make_key(code=42, names=('BACKSPACE', 'BSPC', 'BKSP')) 424 | make_key(code=43, names=('TAB', '\t')) 425 | make_key(code=44, names=('SPACE', 'SPC', ' ')) 426 | make_key(code=45, names=('MINUS', 'MINS', '-')) 427 | make_key(code=46, names=('EQUAL', 'EQL', '=')) 428 | make_key(code=47, names=('LBRACKET', 'LBRC', '[')) 429 | make_key(code=48, names=('RBRACKET', 'RBRC', ']')) 430 | make_key(code=49, names=('BACKSLASH', 'BSLASH', 'BSLS', '\\')) 431 | make_key(code=51, names=('SEMICOLON', 'SCOLON', 'SCLN', ';')) 432 | make_key(code=52, names=('QUOTE', 'QUOT', "'")) 433 | make_key(code=53, names=('GRAVE', 'GRV', 'ZKHK', '`')) 434 | make_key(code=54, names=('COMMA', 'COMM', ',')) 435 | make_key(code=55, names=('DOT', '.')) 436 | make_key(code=56, names=('SLASH', 'SLSH')) 437 | 438 | # Function Keys 439 | make_key(code=58, names=('F1',)) 440 | make_key(code=59, names=('F2',)) 441 | make_key(code=60, names=('F3',)) 442 | make_key(code=61, names=('F4',)) 443 | make_key(code=62, names=('F5',)) 444 | make_key(code=63, names=('F6',)) 445 | make_key(code=64, names=('F7',)) 446 | make_key(code=65, names=('F8',)) 447 | make_key(code=66, names=('F9',)) 448 | make_key(code=67, names=('F10',)) 449 | make_key(code=68, names=('F11',)) 450 | make_key(code=69, names=('F12',)) 451 | make_key(code=104, names=('F13',)) 452 | make_key(code=105, names=('F14',)) 453 | make_key(code=106, names=('F15',)) 454 | make_key(code=107, names=('F16',)) 455 | make_key(code=108, names=('F17',)) 456 | make_key(code=109, names=('F18',)) 457 | make_key(code=110, names=('F19',)) 458 | make_key(code=111, names=('F20',)) 459 | make_key(code=112, names=('F21',)) 460 | make_key(code=113, names=('F22',)) 461 | make_key(code=114, names=('F23',)) 462 | make_key(code=115, names=('F24',)) 463 | 464 | # Lock Keys, Navigation, etc. 465 | make_key(code=57, names=('CAPS_LOCK', 'CAPSLOCK', 'CLCK', 'CAPS')) 466 | # FIXME: Investigate whether this key actually works, and 467 | # uncomment when/if it does. 468 | # make_key(code=130, names=('LOCKING_CAPS', 'LCAP')) 469 | make_key(code=70, names=('PRINT_SCREEN', 'PSCREEN', 'PSCR')) 470 | make_key(code=71, names=('SCROLL_LOCK', 'SCROLLLOCK', 'SLCK')) 471 | # FIXME: Investigate whether this key actually works, and 472 | # uncomment when/if it does. 473 | # make_key(code=132, names=('LOCKING_SCROLL', 'LSCRL')) 474 | make_key(code=72, names=('PAUSE', 'PAUS', 'BRK')) 475 | make_key(code=73, names=('INSERT', 'INS')) 476 | make_key(code=74, names=('HOME',)) 477 | make_key(code=75, names=('PGUP',)) 478 | make_key(code=76, names=('DELETE', 'DEL')) 479 | make_key(code=77, names=('END',)) 480 | make_key(code=78, names=('PGDOWN', 'PGDN')) 481 | make_key(code=79, names=('RIGHT', 'RGHT')) 482 | make_key(code=80, names=('LEFT',)) 483 | make_key(code=81, names=('DOWN',)) 484 | make_key(code=82, names=('UP',)) 485 | 486 | # Numpad 487 | make_key(code=83, names=('NUM_LOCK', 'NUMLOCK', 'NLCK')) 488 | # FIXME: Investigate whether this key actually works, and 489 | # uncomment when/if it does. 490 | # make_key(code=131, names=('LOCKING_NUM', 'LNUM')) 491 | make_key(code=84, names=('KP_SLASH', 'NUMPAD_SLASH', 'PSLS')) 492 | make_key(code=85, names=('KP_ASTERISK', 'NUMPAD_ASTERISK', 'PAST')) 493 | make_key(code=86, names=('KP_MINUS', 'NUMPAD_MINUS', 'PMNS')) 494 | make_key(code=87, names=('KP_PLUS', 'NUMPAD_PLUS', 'PPLS')) 495 | make_key(code=88, names=('KP_ENTER', 'NUMPAD_ENTER', 'PENT')) 496 | make_key(code=89, names=('KP_1', 'P1', 'NUMPAD_1')) 497 | make_key(code=90, names=('KP_2', 'P2', 'NUMPAD_2')) 498 | make_key(code=91, names=('KP_3', 'P3', 'NUMPAD_3')) 499 | make_key(code=92, names=('KP_4', 'P4', 'NUMPAD_4')) 500 | make_key(code=93, names=('KP_5', 'P5', 'NUMPAD_5')) 501 | make_key(code=94, names=('KP_6', 'P6', 'NUMPAD_6')) 502 | make_key(code=95, names=('KP_7', 'P7', 'NUMPAD_7')) 503 | make_key(code=96, names=('KP_8', 'P8', 'NUMPAD_8')) 504 | make_key(code=97, names=('KP_9', 'P9', 'NUMPAD_9')) 505 | make_key(code=98, names=('KP_0', 'P0', 'NUMPAD_0')) 506 | make_key(code=99, names=('KP_DOT', 'PDOT', 'NUMPAD_DOT')) 507 | make_key(code=103, names=('KP_EQUAL', 'PEQL', 'NUMPAD_EQUAL')) 508 | make_key(code=133, names=('KP_COMMA', 'PCMM', 'NUMPAD_COMMA')) 509 | make_key(code=134, names=('KP_EQUAL_AS400', 'NUMPAD_EQUAL_AS400')) 510 | 511 | # Making life better for folks on tiny keyboards especially: exposes 512 | # the 'shifted' keys as raw keys. Under the hood we're still 513 | # sending Shift+(whatever key is normally pressed) to get these, so 514 | # for example `KC_AT` will hold shift and press 2. 515 | make_shifted_key('GRAVE', names=('TILDE', 'TILD', '~')) 516 | make_shifted_key('1', names=('EXCLAIM', 'EXLM', '!')) 517 | make_shifted_key('2', names=('AT', '@')) 518 | make_shifted_key('3', names=('HASH', 'POUND', '#')) 519 | make_shifted_key('4', names=('DOLLAR', 'DLR', '$')) 520 | make_shifted_key('5', names=('PERCENT', 'PERC', '%')) 521 | make_shifted_key('6', names=('CIRCUMFLEX', 'CIRC', '^')) 522 | make_shifted_key('7', names=('AMPERSAND', 'AMPR', '&')) 523 | make_shifted_key('8', names=('ASTERISK', 'ASTR', '*')) 524 | make_shifted_key('9', names=('LEFT_PAREN', 'LPRN', '(')) 525 | make_shifted_key('0', names=('RIGHT_PAREN', 'RPRN', ')')) 526 | make_shifted_key('MINUS', names=('UNDERSCORE', 'UNDS', '_')) 527 | make_shifted_key('EQUAL', names=('PLUS', '+')) 528 | make_shifted_key('LBRACKET', names=('LEFT_CURLY_BRACE', 'LCBR', '{')) 529 | make_shifted_key('RBRACKET', names=('RIGHT_CURLY_BRACE', 'RCBR', '}')) 530 | make_shifted_key('BACKSLASH', names=('PIPE', '|')) 531 | make_shifted_key('SEMICOLON', names=('COLON', 'COLN', ':')) 532 | make_shifted_key('QUOTE', names=('DOUBLE_QUOTE', 'DQUO', 'DQT', '"')) 533 | make_shifted_key('COMMA', names=('LEFT_ANGLE_BRACKET', 'LABK', '<')) 534 | make_shifted_key('DOT', names=('RIGHT_ANGLE_BRACKET', 'RABK', '>')) 535 | make_shifted_key('SLSH', names=('QUESTION', 'QUES', '?')) 536 | 537 | # International 538 | make_key(code=50, names=('NONUS_HASH', 'NUHS')) 539 | make_key(code=100, names=('NONUS_BSLASH', 'NUBS')) 540 | make_key(code=101, names=('APP', 'APPLICATION', 'SEL', 'WINMENU')) 541 | 542 | make_key(code=135, names=('INT1', 'RO')) 543 | make_key(code=136, names=('INT2', 'KANA')) 544 | make_key(code=137, names=('INT3', 'JYEN')) 545 | make_key(code=138, names=('INT4', 'HENK')) 546 | make_key(code=139, names=('INT5', 'MHEN')) 547 | make_key(code=140, names=('INT6',)) 548 | make_key(code=141, names=('INT7',)) 549 | make_key(code=142, names=('INT8',)) 550 | make_key(code=143, names=('INT9',)) 551 | make_key(code=144, names=('LANG1', 'HAEN')) 552 | make_key(code=145, names=('LANG2', 'HAEJ')) 553 | make_key(code=146, names=('LANG3',)) 554 | make_key(code=147, names=('LANG4',)) 555 | make_key(code=148, names=('LANG5',)) 556 | make_key(code=149, names=('LANG6',)) 557 | make_key(code=150, names=('LANG7',)) 558 | make_key(code=151, names=('LANG8',)) 559 | make_key(code=152, names=('LANG9',)) 560 | 561 | # Consumer ("media") keys. Most known keys aren't supported here. A much 562 | # longer list used to exist in this file, but the codes were almost certainly 563 | # incorrect, conflicting with each other, or otherwise 'weird'. We'll add them 564 | # back in piecemeal as needed. PRs welcome. 565 | # 566 | # A super useful reference for these is http://www.freebsddiary.org/APC/usb_hid_usages.php 567 | # Note that currently we only have the PC codes. Recent MacOS versions seem to 568 | # support PC media keys, so I don't know how much value we would get out of 569 | # adding the old Apple-specific consumer codes, but again, PRs welcome if the 570 | # lack of them impacts you. 571 | make_consumer_key(code=226, names=('AUDIO_MUTE', 'MUTE')) # 0xE2 572 | make_consumer_key(code=233, names=('AUDIO_VOL_UP', 'VOLU')) # 0xE9 573 | make_consumer_key(code=234, names=('AUDIO_VOL_DOWN', 'VOLD')) # 0xEA 574 | make_consumer_key(code=181, names=('MEDIA_NEXT_TRACK', 'MNXT')) # 0xB5 575 | make_consumer_key(code=182, names=('MEDIA_PREV_TRACK', 'MPRV')) # 0xB6 576 | make_consumer_key(code=183, names=('MEDIA_STOP', 'MSTP')) # 0xB7 577 | make_consumer_key( 578 | code=205, names=('MEDIA_PLAY_PAUSE', 'MPLY') 579 | ) # 0xCD (this may not be right) 580 | make_consumer_key(code=184, names=('MEDIA_EJECT', 'EJCT')) # 0xB8 581 | make_consumer_key(code=179, names=('MEDIA_FAST_FORWARD', 'MFFD')) # 0xB3 582 | make_consumer_key(code=180, names=('MEDIA_REWIND', 'MRWD')) # 0xB4 583 | 584 | # Internal, diagnostic, or auxiliary/enhanced keys 585 | 586 | # NO and TRNS are functionally identical in how they (don't) mutate 587 | # the state, but are tracked semantically separately, so create 588 | # two keys with the exact same functionality 589 | for names in (('NO',), ('TRANSPARENT', 'TRNS')): 590 | make_key( 591 | names=names, on_press=handlers.passthrough, on_release=handlers.passthrough 592 | ) 593 | 594 | make_key(names=('RESET',), on_press=handlers.reset) 595 | make_key(names=('BOOTLOADER',), on_press=handlers.bootloader) 596 | make_key( 597 | names=('DEBUG', 'DBG'), 598 | on_press=handlers.debug_pressed, 599 | on_release=handlers.passthrough, 600 | ) 601 | 602 | make_key( 603 | names=('GESC',), on_press=handlers.gesc_pressed, on_release=handlers.gesc_released 604 | ) 605 | make_key( 606 | names=('BKDL',), on_press=handlers.bkdl_pressed, on_release=handlers.bkdl_released 607 | ) 608 | make_key( 609 | names=('GESC', 'GRAVE_ESC'), 610 | on_press=handlers.gesc_pressed, 611 | on_release=handlers.gesc_released, 612 | ) 613 | make_key(names=('RGB_TOG',), on_press=handlers.rgb_tog) 614 | make_key(names=('RGB_HUI',), on_press=handlers.rgb_hui) 615 | make_key(names=('RGB_HUD',), on_press=handlers.rgb_hud) 616 | make_key(names=('RGB_SAI',), on_press=handlers.rgb_sai) 617 | make_key(names=('RGB_SAD',), on_press=handlers.rgb_sad) 618 | make_key(names=('RGB_VAI',), on_press=handlers.rgb_vai) 619 | make_key(names=('RGB_VAD',), on_press=handlers.rgb_vad) 620 | make_key(names=('RGB_ANI',), on_press=handlers.rgb_ani) 621 | make_key(names=('RGB_AND',), on_press=handlers.rgb_and) 622 | make_key(names=('RGB_MODE_PLAIN', 'RGB_M_P'), on_press=handlers.rgb_mode_static) 623 | make_key(names=('RGB_MODE_BREATHE', 'RGB_M_B'), on_press=handlers.rgb_mode_breathe) 624 | make_key(names=('RGB_MODE_RAINBOW', 'RGB_M_R'), on_press=handlers.rgb_mode_rainbow) 625 | make_key( 626 | names=('RGB_MODE_BREATHE_RAINBOW', 'RGB_M_BR'), 627 | on_press=handlers.rgb_mode_breathe_rainbow, 628 | ) 629 | make_key(names=('RGB_MODE_SWIRL', 'RGB_M_S'), on_press=handlers.rgb_mode_swirl) 630 | make_key(names=('RGB_MODE_KNIGHT', 'RGB_M_K'), on_press=handlers.rgb_mode_knight) 631 | 632 | 633 | make_key(names=('LED_TOG',), on_press=handlers.led_tog) 634 | make_key(names=('LED_INC',), on_press=handlers.led_inc) 635 | make_key(names=('LED_DEC',), on_press=handlers.led_dec) 636 | make_key(names=('LED_ANI',), on_press=handlers.led_ani) 637 | make_key(names=('LED_AND',), on_press=handlers.led_and) 638 | make_key(names=('LED_MODE_PLAIN', 'LED_M_P'), on_press=handlers.led_mode_static) 639 | make_key(names=('LED_MODE_BREATHE', 'LED_M_B'), on_press=handlers.led_mode_breathe) 640 | make_key(names=('BT_CLEAR_BONDS', 'BT_CLR'), on_press=handlers.bt_clear_bonds) 641 | make_key(names=('BT_NEXT_CONN', 'BT_NXT'), on_press=handlers.bt_next_conn) 642 | make_key(names=('BT_PREV_CONN', 'BT_PRV'), on_press=handlers.bt_prev_conn) 643 | 644 | 645 | make_key( 646 | names=('LEADER', 'LEAD'), 647 | on_press=handlers.leader_pressed, 648 | on_release=handlers.passthrough, 649 | ) 650 | 651 | # Layers 652 | make_argumented_key( 653 | validator=layer_key_validator, 654 | names=('MO',), 655 | on_press=layers.mo_pressed, 656 | on_release=layers.mo_released, 657 | ) 658 | make_argumented_key( 659 | validator=layer_key_validator, names=('DF',), on_press=layers.df_pressed 660 | ) 661 | make_argumented_key( 662 | validator=layer_key_validator, 663 | names=('LM',), 664 | on_press=layers.lm_pressed, 665 | on_release=layers.lm_released, 666 | ) 667 | make_argumented_key( 668 | validator=layer_key_validator, 669 | names=('LT',), 670 | on_press=layers.lt_pressed, 671 | on_release=layers.lt_released, 672 | ) 673 | make_argumented_key( 674 | validator=layer_key_validator, names=('TG',), on_press=layers.tg_pressed 675 | ) 676 | make_argumented_key( 677 | validator=layer_key_validator, names=('TO',), on_press=layers.to_pressed 678 | ) 679 | make_argumented_key( 680 | validator=layer_key_validator, 681 | names=('TT',), 682 | on_press=layers.tt_pressed, 683 | on_release=layers.tt_released, 684 | ) 685 | 686 | make_argumented_key( 687 | validator=mod_tap_validator, 688 | names=('MT',), 689 | on_press=modtap.mt_pressed, 690 | on_release=modtap.mt_released, 691 | ) 692 | 693 | # A dummy key to trigger a sleep_ms call in a sequence of other keys in a 694 | # simple sequence macro. 695 | make_argumented_key( 696 | validator=key_seq_sleep_validator, 697 | names=('MACRO_SLEEP_MS', 'SLEEP_IN_SEQ'), 698 | on_press=handlers.sleep_pressed, 699 | ) 700 | 701 | make_key( 702 | names=('UC_MODE_NOOP', 'UC_DISABLE'), 703 | meta=UnicodeModeKeyMeta(UnicodeMode.NOOP), 704 | on_press=handlers.uc_mode_pressed, 705 | ) 706 | make_key( 707 | names=('UC_MODE_LINUX', 'UC_MODE_IBUS'), 708 | meta=UnicodeModeKeyMeta(UnicodeMode.IBUS), 709 | on_press=handlers.uc_mode_pressed, 710 | ) 711 | make_key( 712 | names=('UC_MODE_MACOS', 'UC_MODE_OSX', 'US_MODE_RALT'), 713 | meta=UnicodeModeKeyMeta(UnicodeMode.RALT), 714 | on_press=handlers.uc_mode_pressed, 715 | ) 716 | make_key( 717 | names=('UC_MODE_WINC',), 718 | meta=UnicodeModeKeyMeta(UnicodeMode.WINC), 719 | on_press=handlers.uc_mode_pressed, 720 | ) 721 | make_argumented_key( 722 | validator=unicode_mode_key_validator, 723 | names=('UC_MODE',), 724 | on_press=handlers.uc_mode_pressed, 725 | ) 726 | 727 | make_argumented_key( 728 | validator=tap_dance_key_validator, 729 | names=('TAP_DANCE', 'TD'), 730 | on_press=handlers.td_pressed, 731 | on_release=handlers.td_released, 732 | ) 733 | -------------------------------------------------------------------------------- /src/kmk/kmk_keyboard.py: -------------------------------------------------------------------------------- 1 | # There's a chance doing preload RAM hacks this late will cause recursion 2 | # errors, but we'll see. I'd rather do it here than require everyone copy-paste 3 | # a line into their keymaps. 4 | import kmk.preload_imports # isort:skip # NOQA 5 | 6 | import busio 7 | import digitalio 8 | import board 9 | 10 | from kmk import led, rgb 11 | from kmk.consts import LeaderMode, UnicodeMode 12 | from kmk.hid import AbstractHID, HIDModes 13 | from kmk.internal_state import InternalState 14 | from kmk.keys import KC 15 | from kmk.kmktime import sleep_ms 16 | from kmk.matrix import MatrixScanner 17 | from kmk.matrix import intify_coordinate as ic 18 | 19 | class KMKKeyboard: 20 | debug_enabled = False 21 | 22 | keymap = None 23 | coord_mapping = None 24 | 25 | row_pins = None 26 | col_pins = None 27 | diode_orientation = None 28 | matrix_scanner = MatrixScanner 29 | uart_buffer = [] 30 | 31 | unicode_mode = UnicodeMode.NOOP 32 | tap_time = 300 33 | leader_mode = LeaderMode.TIMEOUT 34 | leader_dictionary = {} 35 | leader_timeout = 1000 36 | 37 | # Split config 38 | extra_data_pin = None 39 | split_offsets = () 40 | split_flip = False 41 | target_side = None 42 | split_type = None 43 | split_target_left = True 44 | is_target = None 45 | uart = None 46 | uart_flip = True 47 | uart_pin = None 48 | 49 | # RGB config 50 | rgb_pixel_pin = None 51 | rgb_config = rgb.rgb_config 52 | 53 | # led config (mono color) 54 | led_pin = None 55 | led_config = led.led_config 56 | 57 | encoders = None 58 | ips = None 59 | 60 | def __repr__(self): 61 | return ( 62 | 'KMKKeyboard(' 63 | 'debug_enabled={} ' 64 | 'keymap=truncated ' 65 | 'coord_mapping=truncated ' 66 | 'row_pins=truncated ' 67 | 'col_pins=truncated ' 68 | 'diode_orientation={} ' 69 | 'matrix_scanner={} ' 70 | 'unicode_mode={} ' 71 | 'tap_time={} ' 72 | 'leader_mode={} ' 73 | 'leader_dictionary=truncated ' 74 | 'leader_timeout={} ' 75 | 'hid_helper={} ' 76 | 'extra_data_pin={} ' 77 | 'split_offsets={} ' 78 | 'split_flip={} ' 79 | 'target_side={} ' 80 | 'split_type={} ' 81 | 'split_target_left={} ' 82 | 'is_target={} ' 83 | 'uart={} ' 84 | 'uart_flip={} ' 85 | 'uart_pin={}' 86 | ')' 87 | ).format( 88 | self.debug_enabled, 89 | # self.keymap, 90 | # self.coord_mapping, 91 | # self.row_pins, 92 | # self.col_pins, 93 | self.diode_orientation, 94 | self.matrix_scanner, 95 | self.unicode_mode, 96 | self.tap_time, 97 | self.leader_mode, 98 | # self.leader_dictionary, 99 | self.leader_timeout, 100 | self.hid_helper.__name__, 101 | self.extra_data_pin, 102 | self.split_offsets, 103 | self.split_flip, 104 | self.target_side, 105 | self.split_type, 106 | self.split_target_left, 107 | self.is_target, 108 | self.uart, 109 | self.uart_flip, 110 | self.uart_pin, 111 | ) 112 | 113 | def _send_hid(self): 114 | self._hid_helper_inst.create_report(self._state.keys_pressed).send() 115 | self._state.resolve_hid() 116 | 117 | def _send_key(self, key): 118 | if not getattr(key, 'no_press', None): 119 | self._state.add_key(key) 120 | self._send_hid() 121 | 122 | if not getattr(key, 'no_release', None): 123 | self._state.remove_key(key) 124 | self._send_hid() 125 | 126 | def _handle_matrix_report(self, update=None): 127 | ''' 128 | Bulk processing of update code for each cycle 129 | :param update: 130 | ''' 131 | if update is not None: 132 | 133 | self._state.matrix_changed(update[0], update[1], update[2]) 134 | 135 | def _send_to_target(self, update): 136 | if self.split_target_left: 137 | update[1] += self.split_offsets[update[0]] 138 | else: 139 | update[1] -= self.split_offsets[update[0]] 140 | if self.uart is not None: 141 | self.uart.write(update) 142 | 143 | def _receive_from_initiator(self): 144 | if self.uart is not None and self.uart.in_waiting > 0 or self.uart_buffer: 145 | if self.uart.in_waiting >= 60: 146 | # This is a dirty hack to prevent crashes in unrealistic cases 147 | import microcontroller 148 | 149 | microcontroller.reset() 150 | 151 | while self.uart.in_waiting >= 3: 152 | self.uart_buffer.append(self.uart.read(3)) 153 | if self.uart_buffer: 154 | update = bytearray(self.uart_buffer.pop(0)) 155 | 156 | # Built in debug mode switch 157 | if update == b'DEB': 158 | print(self.uart.readline()) 159 | return None 160 | return update 161 | 162 | return None 163 | 164 | def _send_debug(self, message): 165 | ''' 166 | Prepends DEB and appends a newline to allow debug messages to 167 | be detected and handled differently than typical keypresses. 168 | :param message: Debug message 169 | ''' 170 | if self.uart is not None: 171 | self.uart.write('DEB') 172 | self.uart.write(message, '\n') 173 | 174 | def init_uart(self, pin, timeout=20): 175 | if self.is_target: 176 | return busio.UART(tx=None, rx=pin, timeout=timeout) 177 | else: 178 | return busio.UART(tx=pin, rx=None, timeout=timeout) 179 | 180 | def go(self, hid_type=HIDModes.USB, **kwargs): 181 | assert self.keymap, 'must define a keymap with at least one row' 182 | assert self.row_pins, 'no GPIO pins defined for matrix rows' 183 | assert self.col_pins, 'no GPIO pins defined for matrix columns' 184 | assert self.diode_orientation is not None, 'diode orientation must be defined' 185 | assert ( 186 | hid_type in HIDModes.ALL_MODES 187 | ), 'hid_type must be a value from kmk.consts.HIDModes' 188 | 189 | # Attempt to sanely guess a coord_mapping if one is not provided 190 | 191 | if not self.coord_mapping: 192 | self.coord_mapping = [] 193 | 194 | rows_to_calc = len(self.row_pins) 195 | cols_to_calc = len(self.col_pins) 196 | 197 | if self.split_offsets: 198 | rows_to_calc *= 2 199 | cols_to_calc *= 2 200 | 201 | for ridx in range(rows_to_calc): 202 | for cidx in range(cols_to_calc): 203 | self.coord_mapping.append(ic(ridx, cidx)) 204 | 205 | self._state = InternalState(self) 206 | 207 | if hid_type == HIDModes.NOOP: 208 | self.hid_helper = AbstractHID 209 | elif hid_type == HIDModes.USB: 210 | try: 211 | from kmk.hid import USBHID 212 | 213 | self.hid_helper = USBHID 214 | except ImportError: 215 | self.hid_helper = AbstractHID 216 | print('USB HID is unsupported ') 217 | elif hid_type == HIDModes.BLE: 218 | try: 219 | from kmk.ble import BLEHID 220 | 221 | self.hid_helper = BLEHID 222 | except ImportError: 223 | self.hid_helper = AbstractHID 224 | print('Bluetooth is unsupported ') 225 | 226 | self._hid_helper_inst = self.hid_helper(**kwargs) 227 | 228 | # Split keyboard Init 229 | if self.split_type is not None: 230 | try: 231 | # Working around https://github.com/adafruit/circuitpython/issues/1769 232 | self._hid_helper_inst.create_report([]).send() 233 | self.is_target = True 234 | 235 | # Sleep 2s so target portion doesn't "appear" to boot quicker than 236 | # dependent portions (which will take ~2s to time out on the HID send) 237 | sleep_ms(2000) 238 | except OSError: 239 | self.is_target = False 240 | 241 | if self.split_flip and not self.is_target: 242 | self.col_pins = list(reversed(self.col_pins)) 243 | if self.target_side == 'Left': 244 | self.split_target_left = self.is_target 245 | elif self.target_side == 'Right': 246 | self.split_target_left = not self.is_target 247 | else: 248 | self.is_target = True 249 | 250 | if self.uart_pin is not None: 251 | self.uart = self.init_uart(self.uart_pin) 252 | 253 | if self.rgb_pixel_pin: 254 | self.pixels = rgb.RGB(self.rgb_config, self.rgb_pixel_pin) 255 | self.rgb_config = None # No longer needed 256 | self.pixels.loopcounter = 0 257 | else: 258 | self.pixels = None 259 | 260 | if self.led_pin: 261 | self.led = led.led(self.led_pin, self.led_config) 262 | self.led_config = None # No longer needed 263 | else: 264 | self.led = None 265 | 266 | self.matrix = self.matrix_scanner( 267 | cols=self.col_pins, 268 | rows=self.row_pins, 269 | diode_orientation=self.diode_orientation, 270 | rollover_cols_every_rows=getattr(self, 'rollover_cols_every_rows', None), 271 | ) 272 | 273 | 274 | # Compile string leader sequences 275 | for k, v in self.leader_dictionary.items(): 276 | if not isinstance(k, tuple): 277 | new_key = tuple(KC[c] for c in k) 278 | self.leader_dictionary[new_key] = v 279 | 280 | for k, v in self.leader_dictionary.items(): 281 | if not isinstance(k, tuple): 282 | del self.leader_dictionary[k] 283 | 284 | while True: 285 | 286 | if self.split_type is not None and self.is_target: 287 | update = self._receive_from_initiator() 288 | if update is not None: 289 | self._handle_matrix_report(update) 290 | 291 | update = self.matrix.scan_for_changes() 292 | 293 | if update is not None: 294 | if self.is_target: 295 | self._handle_matrix_report(update) 296 | else: 297 | # This keyboard is a initiator, and needs to send data to target 298 | self._send_to_target(update) 299 | 300 | if(self.encoders is not None): 301 | for encoder in self.encoders: 302 | encoder.read() 303 | 304 | if self._state.hid_pending: 305 | self._send_hid() 306 | 307 | old_timeouts_len = len(self._state.timeouts) 308 | self._state.process_timeouts() 309 | new_timeouts_len = len(self._state.timeouts) 310 | 311 | if old_timeouts_len != new_timeouts_len: 312 | if self._state.hid_pending: 313 | self._send_hid() 314 | 315 | if self.pixels and self.pixels.animation_mode: 316 | self.pixels.loopcounter += 1 317 | if self.pixels.loopcounter >= 30: 318 | self.pixels = self.pixels.animate() 319 | self.pixels.loopcounter = 0 320 | 321 | if self.led and self.led.enabled and self.led.animation_mode: 322 | self.led = self.led.animate() 323 | 324 | 325 | -------------------------------------------------------------------------------- /src/kmk/kmktime.py: -------------------------------------------------------------------------------- 1 | import math 2 | import time 3 | 4 | USE_UTIME = False 5 | 6 | 7 | def sleep_ms(ms): 8 | ''' 9 | Tries to sleep for a number of milliseconds in a cross-implementation 10 | way. Will raise an ImportError if time is not available on the platform. 11 | ''' 12 | if USE_UTIME: 13 | return time.sleep_ms(ms) 14 | else: 15 | return time.sleep(ms / 1000) 16 | 17 | 18 | def ticks_ms(): 19 | if USE_UTIME: 20 | return time.ticks_ms() 21 | else: 22 | return math.floor(time.monotonic() * 1000) 23 | 24 | 25 | def ticks_diff(new, old): 26 | if USE_UTIME: 27 | return time.ticks_diff(new, old) 28 | else: 29 | return new - old 30 | -------------------------------------------------------------------------------- /src/kmk/led.py: -------------------------------------------------------------------------------- 1 | import pulseio 2 | import time 3 | from math import e, exp, pi, sin 4 | from micropython import const 5 | 6 | led_config = { 7 | 'brightness_step': 5, 8 | 'brightness_limit': 100, 9 | 'breathe_center': 1.5, 10 | 'animation_mode': 'static', 11 | 'animation_speed': 1, 12 | } 13 | 14 | 15 | class led: 16 | brightness = 0 17 | time = int(time.monotonic() * 1000) 18 | pos = 0 19 | effect_init = False 20 | 21 | led = None 22 | brightness_step = 5 23 | brightness_limit = 100 24 | breathe_center = 1.5 25 | animation_mode = 'static' 26 | animation_speed = 1 27 | enabled = True 28 | user_animation = None 29 | 30 | def __init__(self, led_pin, config): 31 | self.led = pulseio.PWMOut(led_pin) 32 | self.brightness_step = const(config['brightness_step']) 33 | self.brightness_limit = const(config['brightness_limit']) 34 | self.animation_mode = const(config['animation_mode']) 35 | self.animation_speed = const(config['animation_speed']) 36 | self.breathe_center = const(config['breathe_center']) 37 | if config.get('user_animation'): 38 | self.user_animation = config['user_animation'] 39 | 40 | def __repr__(self): 41 | return 'LED({})'.format(self._to_dict()) 42 | 43 | def _to_dict(self): 44 | return { 45 | 'led': self.led, 46 | 'brightness_step': self.brightness_step, 47 | 'brightness_limit': self.brightness_limit, 48 | 'animation_mode': self.animation_mode, 49 | 'animation_speed': self.animation_speed, 50 | 'breathe_center': self.breathe_center, 51 | } 52 | 53 | def _init_effect(self): 54 | self.pos = 0 55 | self.effect_init = False 56 | return self 57 | 58 | def time_ms(self): 59 | return int(time.monotonic() * 1000) 60 | 61 | def set_brightness(self, percent): 62 | self.led.duty_cycle = int(percent / 100 * 65535) 63 | 64 | def increase_brightness(self, step=None): 65 | if not step: 66 | self.brightness += self.brightness_step 67 | else: 68 | self.brightness += step 69 | 70 | if self.brightness > 100: 71 | self.brightness = 100 72 | 73 | self.set_brightness(self.brightness) 74 | 75 | def decrease_brightness(self, step=None): 76 | if not step: 77 | self.brightness -= self.brightness_step 78 | else: 79 | self.brightness -= step 80 | 81 | if self.brightness < 0: 82 | self.brightness = 0 83 | 84 | self.set_brightness(self.brightness) 85 | 86 | def off(self): 87 | self.set_brightness(0) 88 | 89 | def increase_ani(self): 90 | ''' 91 | Increases animation speed by 1 amount stopping at 10 92 | :param step: 93 | ''' 94 | if (self.animation_speed + 1) >= 10: 95 | self.animation_speed = 10 96 | else: 97 | self.val += 1 98 | 99 | def decrease_ani(self): 100 | ''' 101 | Decreases animation speed by 1 amount stopping at 0 102 | :param step: 103 | ''' 104 | if (self.val - 1) <= 0: 105 | self.val = 0 106 | else: 107 | self.val -= 1 108 | 109 | def effect_breathing(self): 110 | # http://sean.voisen.org/blog/2011/10/breathing-led-with-arduino/ 111 | # https://github.com/qmk/qmk_firmware/blob/9f1d781fcb7129a07e671a46461e501e3f1ae59d/quantum/rgblight.c#L806 112 | sined = sin((self.pos / 255.0) * pi) 113 | multip_1 = exp(sined) - self.breathe_center / e 114 | multip_2 = self.brightness_limit / (e - 1 / e) 115 | 116 | self.brightness = int(multip_1 * multip_2) 117 | self.pos = (self.pos + self.animation_speed) % 256 118 | self.set_brightness(self.brightness) 119 | 120 | return self 121 | 122 | def effect_static(self): 123 | self.set_brightness(self.brightness) 124 | # Set animation mode to none to prevent cycles from being wasted 125 | self.animation_mode = None 126 | return self 127 | 128 | def animate(self): 129 | ''' 130 | Activates a "step" in the animation based on the active mode 131 | :return: Returns the new state in animation 132 | ''' 133 | if self.effect_init: 134 | self._init_effect() 135 | if self.enabled: 136 | if self.animation_mode == 'breathing': 137 | return self.effect_breathing() 138 | elif self.animation_mode == 'static': 139 | return self.effect_static() 140 | elif self.animation_mode == 'user': 141 | return self.user_animation(self) 142 | else: 143 | self.off() 144 | 145 | return self 146 | -------------------------------------------------------------------------------- /src/kmk/matrix.py: -------------------------------------------------------------------------------- 1 | import digitalio 2 | 3 | 4 | def intify_coordinate(row, col): 5 | return row << 8 | col 6 | 7 | 8 | class DiodeOrientation: 9 | ''' 10 | Orientation of diodes on handwired boards. You can think of: 11 | COLUMNS = vertical 12 | ROWS = horizontal 13 | ''' 14 | 15 | COLUMNS = 0 16 | ROWS = 1 17 | 18 | 19 | class MatrixScanner: 20 | def __init__( 21 | self, 22 | cols, 23 | rows, 24 | diode_orientation=DiodeOrientation.COLUMNS, 25 | rollover_cols_every_rows=None, 26 | ): 27 | self.len_cols = len(cols) 28 | self.len_rows = len(rows) 29 | 30 | # A pin cannot be both a row and column, detect this by combining the 31 | # two tuples into a set and validating that the length did not drop 32 | # 33 | # repr() hackery is because CircuitPython Pin objects are not hashable 34 | unique_pins = {repr(c) for c in cols} | {repr(r) for r in rows} 35 | assert ( 36 | len(unique_pins) == self.len_cols + self.len_rows 37 | ), 'Cannot use a pin as both a column and row' 38 | del unique_pins 39 | 40 | self.diode_orientation = diode_orientation 41 | 42 | # __class__.__name__ is used instead of isinstance as the MCP230xx lib 43 | # does not use the digitalio.DigitalInOut, but rather a self defined one: 44 | # https://github.com/adafruit/Adafruit_CircuitPython_MCP230xx/blob/3f04abbd65ba5fa938fcb04b99e92ae48a8c9406/adafruit_mcp230xx/digital_inout.py#L33 45 | 46 | if self.diode_orientation == DiodeOrientation.COLUMNS: 47 | self.outputs = [ 48 | x 49 | if x.__class__.__name__ is 'DigitalInOut' 50 | else digitalio.DigitalInOut(x) 51 | for x in cols 52 | ] 53 | self.inputs = [ 54 | x 55 | if x.__class__.__name__ is 'DigitalInOut' 56 | else digitalio.DigitalInOut(x) 57 | for x in rows 58 | ] 59 | self.translate_coords = True 60 | elif self.diode_orientation == DiodeOrientation.ROWS: 61 | self.outputs = [ 62 | x 63 | if x.__class__.__name__ is 'DigitalInOut' 64 | else digitalio.DigitalInOut(x) 65 | for x in rows 66 | ] 67 | self.inputs = [ 68 | x 69 | if x.__class__.__name__ is 'DigitalInOut' 70 | else digitalio.DigitalInOut(x) 71 | for x in cols 72 | ] 73 | self.translate_coords = False 74 | else: 75 | raise ValueError( 76 | 'Invalid DiodeOrientation: {}'.format(self.diode_orientation) 77 | ) 78 | 79 | for pin in self.outputs: 80 | pin.switch_to_output() 81 | 82 | for pin in self.inputs: 83 | pin.switch_to_input(pull=digitalio.Pull.DOWN) 84 | 85 | self.rollover_cols_every_rows = rollover_cols_every_rows 86 | if self.rollover_cols_every_rows is None: 87 | self.rollover_cols_every_rows = self.len_rows 88 | 89 | self.len_state_arrays = self.len_cols * self.len_rows 90 | self.state = bytearray(self.len_state_arrays) 91 | self.report = bytearray(3) 92 | 93 | def scan_for_changes(self): 94 | ''' 95 | Poll the matrix for changes and return either None (if nothing updated) 96 | or a bytearray (reused in later runs so copy this if you need the raw 97 | array itself for some crazy reason) consisting of (row, col, pressed) 98 | which are (int, int, bool) 99 | ''' 100 | ba_idx = 0 101 | any_changed = False 102 | 103 | for oidx, opin in enumerate(self.outputs): 104 | opin.value = True 105 | 106 | for iidx, ipin in enumerate(self.inputs): 107 | # cast to int to avoid 108 | # 109 | # >>> xyz = bytearray(3) 110 | # >>> xyz[2] = True 111 | # Traceback (most recent call last): 112 | # File "", line 1, in 113 | # OverflowError: value would overflow a 1 byte buffer 114 | # 115 | # I haven't dived too far into what causes this, but it's 116 | # almost certainly because bool types in Python aren't just 117 | # aliases to int values, but are proper pseudo-types 118 | new_val = int(ipin.value) 119 | old_val = self.state[ba_idx] 120 | 121 | if old_val != new_val: 122 | if self.translate_coords: 123 | new_oidx = oidx + self.len_cols * ( 124 | iidx // self.rollover_cols_every_rows 125 | ) 126 | new_iidx = iidx - self.rollover_cols_every_rows * ( 127 | iidx // self.rollover_cols_every_rows 128 | ) 129 | 130 | self.report[0] = new_iidx 131 | self.report[1] = new_oidx 132 | else: 133 | self.report[0] = oidx 134 | self.report[1] = iidx 135 | 136 | self.report[2] = new_val 137 | self.state[ba_idx] = new_val 138 | 139 | any_changed = True 140 | break 141 | 142 | ba_idx += 1 143 | 144 | opin.value = False 145 | if any_changed: 146 | break 147 | 148 | if any_changed: 149 | return self.report 150 | -------------------------------------------------------------------------------- /src/kmk/preload_imports.py: -------------------------------------------------------------------------------- 1 | # Welcome to RAM and stack size hacks central, I'm your host, klardotsh! 2 | # Our import structure is deeply nested enough that stuff 3 | # breaks in some truly bizarre ways, including: 4 | # - explicit RuntimeError exceptions, complaining that our 5 | # stack depth is too deep 6 | # 7 | # - silent hard locks of the device (basically unrecoverable without 8 | # UF2 flash if done in main.py, fixable with a reboot if done 9 | # in REPL) 10 | # 11 | # However, there's a hackaround that works for us! Because sys.modules 12 | # caches everything it sees (and future imports will use that cached 13 | # copy of the module), let's take this opportunity _way_ up the import 14 | # chain to import _every single thing_ KMK eventually uses in a normal 15 | # workflow, in nesting order 16 | # 17 | # GC runs automatically after CircuitPython imports. 18 | 19 | # First, system-provided deps 20 | import busio 21 | import collections 22 | import gc 23 | import supervisor 24 | 25 | # Now "light" KMK stuff with few/no external deps 26 | import kmk.consts # isort:skip 27 | import kmk.kmktime # isort:skip 28 | import kmk.types # isort:skip 29 | 30 | from kmk.consts import LeaderMode, UnicodeMode, KMK_RELEASE # isort:skip 31 | from kmk.hid import USBHID # isort:skip 32 | from kmk.internal_state import InternalState # isort:skip 33 | from kmk.keys import KC # isort:skip 34 | from kmk.matrix import MatrixScanner # isort:skip 35 | 36 | # Now handlers that will be used in keys later 37 | import kmk.handlers.layers # isort:skip 38 | import kmk.handlers.stock # isort:skip 39 | 40 | # Now stuff that depends on the above (and so on) 41 | import kmk.keys # isort:skip 42 | import kmk.matrix # isort:skip 43 | 44 | import kmk.hid # isort:skip 45 | import kmk.internal_state # isort:skip 46 | -------------------------------------------------------------------------------- /src/kmk/rgb.py: -------------------------------------------------------------------------------- 1 | import time 2 | from math import e, exp, pi, sin 3 | from micropython import const 4 | 5 | rgb_config = { 6 | 'pixels': None, 7 | 'num_pixels': 0, 8 | 'pixel_pin': None, 9 | 'val_limit': 255, 10 | 'hue_default': 0, 11 | 'sat_default': 100, 12 | 'rgb_order': (1, 0, 2), # GRB WS2812 13 | 'val_default': 100, 14 | 'hue_step': 1, 15 | 'sat_step': 1, 16 | 'val_step': 1, 17 | 'animation_speed': 1, 18 | 'breathe_center': 1.5, # 1.0-2.7 19 | 'knight_effect_length': 3, 20 | 'animation_mode': 'static', 21 | } 22 | 23 | 24 | class RGB: 25 | hue = 0 26 | sat = 100 27 | val = 80 28 | pos = 0 29 | time = int(time.monotonic() * 10) 30 | intervals = (30, 20, 10, 5) 31 | animation_speed = 1 32 | enabled = True 33 | neopixel = None 34 | rgbw = False 35 | reverse_animation = False 36 | disable_auto_write = False 37 | animation_mode = 'static' 38 | 39 | # Set by config 40 | num_pixels = None 41 | hue_step = None 42 | sat_step = None 43 | val_step = None 44 | breathe_center = None # 1.0-2.7 45 | knight_effect_length = None 46 | val_limit = None 47 | effect_init = False 48 | user_animation = None 49 | 50 | def __init__(self, config, pixel_pin): 51 | try: 52 | import neopixel 53 | 54 | self.neopixel = neopixel.NeoPixel( 55 | pixel_pin, 56 | config['num_pixels'], 57 | pixel_order=config['rgb_order'], 58 | auto_write=False, 59 | ) 60 | if len(config['rgb_order']) == 4: 61 | self.rgbw = True 62 | self.num_pixels = const(config['num_pixels']) 63 | self.hue_step = const(config['hue_step']) 64 | self.sat_step = const(config['sat_step']) 65 | self.val_step = const(config['val_step']) 66 | self.hue = const(config['hue_default']) 67 | self.sat = const(config['sat_default']) 68 | self.val = const(config['val_default']) 69 | self.breathe_center = const(config['breathe_center']) 70 | self.knight_effect_length = const(config['knight_effect_length']) 71 | self.val_limit = const(config['val_limit']) 72 | self.animation_mode = config['animation_mode'] 73 | self.animation_speed = const(config['animation_speed']) 74 | if 'user_animation' in config: 75 | self.user_animation = config['user_animation'] 76 | 77 | except ImportError as e: 78 | print(e) 79 | 80 | def __repr__(self): 81 | return 'RGB({})'.format(self._to_dict()) 82 | 83 | def _to_dict(self): 84 | return { 85 | 'hue': self.hue, 86 | 'sat': self.sat, 87 | 'val': self.val, 88 | 'time': self.time, 89 | 'intervals': self.intervals, 90 | 'animation_mode': self.animation_mode, 91 | 'animation_speed': self.animation_speed, 92 | 'enabled': self.enabled, 93 | 'neopixel': self.neopixel, 94 | 'disable_auto_write': self.disable_auto_write, 95 | } 96 | 97 | def time_ms(self): 98 | return int(time.monotonic() * 1000) 99 | 100 | def hsv_to_rgb(self, hue, sat, val): 101 | ''' 102 | Converts HSV values, and returns a tuple of RGB values 103 | :param hue: 104 | :param sat: 105 | :param val: 106 | :return: (r, g, b) 107 | ''' 108 | r = 0 109 | g = 0 110 | b = 0 111 | 112 | if val > self.val_limit: 113 | val = self.val_limit 114 | 115 | if sat == 0: 116 | r = val 117 | g = val 118 | b = val 119 | 120 | else: 121 | base = ((100 - sat) * val) / 100 122 | color = int((val - base) * ((hue % 60) / 60)) 123 | 124 | x = int(hue / 60) 125 | if x == 0: 126 | r = val 127 | g = base + color 128 | b = base 129 | elif x == 1: 130 | r = val - color 131 | g = val 132 | b = base 133 | elif x == 2: 134 | r = base 135 | g = val 136 | b = base + color 137 | elif x == 3: 138 | r = base 139 | g = val - color 140 | b = val 141 | elif x == 4: 142 | r = base + color 143 | g = base 144 | b = val 145 | elif x == 5: 146 | r = val 147 | g = base 148 | b = val - color 149 | 150 | return int(r), int(g), int(b) 151 | 152 | def hsv_to_rgbw(self, hue, sat, val): 153 | ''' 154 | Converts HSV values, and returns a tuple of RGBW values 155 | :param hue: 156 | :param sat: 157 | :param val: 158 | :return: (r, g, b, w) 159 | ''' 160 | rgb = self.hsv_to_rgb(hue, sat, val) 161 | return rgb[0], rgb[1], rgb[2], min(rgb) 162 | 163 | def set_hsv(self, hue, sat, val, index): 164 | ''' 165 | Takes HSV values and displays it on a single LED/Neopixel 166 | :param hue: 167 | :param sat: 168 | :param val: 169 | :param index: Index of LED/Pixel 170 | ''' 171 | if self.neopixel: 172 | if self.rgbw: 173 | self.set_rgb(self.hsv_to_rgbw(hue, sat, val), index) 174 | else: 175 | self.set_rgb(self.hsv_to_rgb(hue, sat, val), index) 176 | 177 | return self 178 | 179 | def set_hsv_fill(self, hue, sat, val): 180 | ''' 181 | Takes HSV values and displays it on all LEDs/Neopixels 182 | :param hue: 183 | :param sat: 184 | :param val: 185 | ''' 186 | if self.neopixel: 187 | if self.rgbw: 188 | self.set_rgb_fill(self.hsv_to_rgbw(hue, sat, val)) 189 | else: 190 | self.set_rgb_fill(self.hsv_to_rgb(hue, sat, val)) 191 | return self 192 | 193 | def set_rgb(self, rgb, index): 194 | ''' 195 | Takes an RGB or RGBW and displays it on a single LED/Neopixel 196 | :param rgb: RGB or RGBW 197 | :param index: Index of LED/Pixel 198 | ''' 199 | if self.neopixel and 0 <= index <= self.num_pixels - 1: 200 | self.neopixel[index] = rgb 201 | if not self.disable_auto_write: 202 | self.neopixel.show() 203 | 204 | return self 205 | 206 | def set_rgb_fill(self, rgb): 207 | ''' 208 | Takes an RGB or RGBW and displays it on all LEDs/Neopixels 209 | :param rgb: RGB or RGBW 210 | ''' 211 | if self.neopixel: 212 | self.neopixel.fill(rgb) 213 | if not self.disable_auto_write: 214 | self.neopixel.show() 215 | 216 | return self 217 | 218 | def increase_hue(self, step=None): 219 | ''' 220 | Increases hue by step amount rolling at 360 and returning to 0 221 | :param step: 222 | ''' 223 | if not step: 224 | step = self.hue_step 225 | 226 | self.hue = (self.hue + step) % 360 227 | 228 | if self._check_update(): 229 | self._do_update() 230 | 231 | return self 232 | 233 | def decrease_hue(self, step=None): 234 | ''' 235 | Decreases hue by step amount rolling at 0 and returning to 360 236 | :param step: 237 | ''' 238 | if not step: 239 | step = self.hue_step 240 | 241 | if (self.hue - step) <= 0: 242 | self.hue = (self.hue + 360 - step) % 360 243 | else: 244 | self.hue = (self.hue - step) % 360 245 | 246 | if self._check_update(): 247 | self._do_update() 248 | 249 | return self 250 | 251 | def increase_sat(self, step=None): 252 | ''' 253 | Increases saturation by step amount stopping at 100 254 | :param step: 255 | ''' 256 | if not step: 257 | step = self.sat_step 258 | 259 | if self.sat + step >= 100: 260 | self.sat = 100 261 | else: 262 | self.sat += step 263 | 264 | if self._check_update(): 265 | self._do_update() 266 | 267 | return self 268 | 269 | def decrease_sat(self, step=None): 270 | ''' 271 | Decreases saturation by step amount stopping at 0 272 | :param step: 273 | ''' 274 | if not step: 275 | step = self.sat_step 276 | 277 | if (self.sat - step) <= 0: 278 | self.sat = 0 279 | else: 280 | self.sat -= step 281 | 282 | if self._check_update(): 283 | self._do_update() 284 | 285 | return self 286 | 287 | def increase_val(self, step=None): 288 | ''' 289 | Increases value by step amount stopping at 100 290 | :param step: 291 | ''' 292 | if not step: 293 | step = self.val_step 294 | if (self.val + step) >= 100: 295 | self.val = 100 296 | else: 297 | self.val += step 298 | 299 | if self._check_update(): 300 | self._do_update() 301 | 302 | return self 303 | 304 | def decrease_val(self, step=None): 305 | ''' 306 | Decreases value by step amount stopping at 0 307 | :param step: 308 | ''' 309 | if not step: 310 | step = self.val_step 311 | if (self.val - step) <= 0: 312 | self.val = 0 313 | else: 314 | self.val -= step 315 | 316 | if self._check_update(): 317 | self._do_update() 318 | 319 | return self 320 | 321 | def increase_ani(self): 322 | ''' 323 | Increases animation speed by 1 amount stopping at 10 324 | :param step: 325 | ''' 326 | if (self.animation_speed + 1) > 10: 327 | self.animation_speed = 10 328 | else: 329 | self.animation_speed += 1 330 | if self._check_update(): 331 | self._do_update() 332 | 333 | return self 334 | 335 | def decrease_ani(self): 336 | ''' 337 | Decreases animation speed by 1 amount stopping at 0 338 | :param step: 339 | ''' 340 | if (self.animation_speed - 1) <= 0: 341 | self.animation_speed = 0 342 | else: 343 | self.animation_speed -= 1 344 | if self._check_update(): 345 | self._do_update() 346 | 347 | return self 348 | 349 | def off(self): 350 | ''' 351 | Turns off all LEDs/Neopixels without changing stored values 352 | ''' 353 | if self.neopixel: 354 | self.set_hsv_fill(0, 0, 0) 355 | 356 | return self 357 | 358 | def show(self): 359 | ''' 360 | Turns on all LEDs/Neopixels without changing stored values 361 | ''' 362 | if self.neopixel: 363 | self.neopixel.show() 364 | 365 | return self 366 | 367 | def animate(self): 368 | ''' 369 | Activates a "step" in the animation based on the active mode 370 | :return: Returns the new state in animation 371 | ''' 372 | if self.effect_init: 373 | self._init_effect() 374 | 375 | if self.enabled: 376 | if self.animation_mode == 'breathing': 377 | return self.effect_breathing() 378 | elif self.animation_mode == 'rainbow': 379 | return self.effect_rainbow() 380 | elif self.animation_mode == 'breathing_rainbow': 381 | return self.effect_breathing_rainbow() 382 | elif self.animation_mode == 'static': 383 | return self.effect_static() 384 | elif self.animation_mode == 'knight': 385 | return self.effect_knight() 386 | elif self.animation_mode == 'swirl': 387 | return self.effect_swirl() 388 | elif self.animation_mode == 'user': 389 | return self.user_animation(self) 390 | elif self.animation_mode == 'static_standby': 391 | pass 392 | else: 393 | self.off() 394 | 395 | return self 396 | 397 | def _animation_step(self): 398 | interval = self.time_ms() - self.time 399 | if interval >= max(self.intervals): 400 | self.time = self.time_ms() 401 | return max(self.intervals) 402 | if interval in self.intervals: 403 | return interval 404 | else: 405 | return False 406 | 407 | def _init_effect(self): 408 | if ( 409 | self.animation_mode == 'breathing' 410 | or self.animation_mode == 'breathing_rainbow' 411 | ): 412 | self.intervals = (30, 20, 10, 5) 413 | elif self.animation_mode == 'swirl': 414 | self.intervals = (50, 50) 415 | 416 | self.pos = 0 417 | self.reverse_animation = False 418 | self.effect_init = False 419 | return self 420 | 421 | def _check_update(self): 422 | if self.animation_mode == 'static_standby': 423 | return True 424 | 425 | def _do_update(self): 426 | if self.animation_mode == 'static_standby': 427 | self.animation_mode = 'static' 428 | 429 | def effect_static(self): 430 | self.set_hsv_fill(self.hue, self.sat, self.val) 431 | self.animation_mode = 'static_standby' 432 | return self 433 | 434 | def effect_breathing(self): 435 | # http://sean.voisen.org/blog/2011/10/breathing-led-with-arduino/ 436 | # https://github.com/qmk/qmk_firmware/blob/9f1d781fcb7129a07e671a46461e501e3f1ae59d/quantum/rgblight.c#L806 437 | sined = sin((self.pos / 255.0) * pi) 438 | multip_1 = exp(sined) - self.breathe_center / e 439 | multip_2 = self.val_limit / (e - 1 / e) 440 | 441 | self.val = int(multip_1 * multip_2) 442 | self.pos = (self.pos + self.animation_speed) % 256 443 | self.set_hsv_fill(self.hue, self.sat, self.val) 444 | 445 | return self 446 | 447 | def effect_breathing_rainbow(self): 448 | self.increase_hue(self.animation_speed) 449 | self.effect_breathing() 450 | 451 | return self 452 | 453 | def effect_rainbow(self): 454 | self.increase_hue(self.animation_speed) 455 | self.set_hsv_fill(self.hue, self.sat, self.val) 456 | 457 | return self 458 | 459 | def effect_swirl(self): 460 | self.increase_hue(self.animation_speed) 461 | self.disable_auto_write = True # Turn off instantly showing 462 | for i in range(0, self.num_pixels): 463 | self.set_hsv( 464 | (self.hue - (i * self.num_pixels)) % 360, self.sat, self.val, i 465 | ) 466 | 467 | # Show final results 468 | self.disable_auto_write = False # Resume showing changes 469 | self.show() 470 | return self 471 | 472 | def effect_knight(self): 473 | # Determine which LEDs should be lit up 474 | self.disable_auto_write = True # Turn off instantly showing 475 | self.off() # Fill all off 476 | pos = int(self.pos) 477 | 478 | # Set all pixels on in range of animation length offset by position 479 | for i in range(pos, (pos + self.knight_effect_length)): 480 | self.set_hsv(self.hue, self.sat, self.val, i) 481 | 482 | # Reverse animation when a boundary is hit 483 | if pos >= self.num_pixels or pos - 1 < (self.knight_effect_length * -1): 484 | self.reverse_animation = not self.reverse_animation 485 | 486 | if self.reverse_animation: 487 | self.pos -= self.animation_speed / 2 488 | else: 489 | self.pos += self.animation_speed / 2 490 | 491 | # Show final results 492 | self.disable_auto_write = False # Resume showing changes 493 | self.show() 494 | 495 | return self 496 | -------------------------------------------------------------------------------- /src/kmk/rotary_encoder.py: -------------------------------------------------------------------------------- 1 | #Based on code from https://github.com/adafruit/circuitpython/blob/master/ports/atmel-samd/common-hal/rotaryio/IncrementalEncoder.c#L121 2 | 3 | import digitalio 4 | import board 5 | 6 | 7 | class Encoder: 8 | prev_state = 0 9 | q_count = 0 10 | pad_a = None 11 | pad_b = None 12 | onRotate = None 13 | 14 | def __init__(self, pad_a, pad_b, onRotate): 15 | self.pad_a = pad_a 16 | self.pad_b = pad_b 17 | self.onRotate = onRotate 18 | 19 | def read(self): 20 | t = (0, -1, 1, 'BAD', 1, 0, 'BAD', -1, -1, 'BAD', 0, 1, 'BAD', 1, -1, 0) 21 | pada = digitalio.DigitalInOut(self.pad_a) 22 | pada.direction = digitalio.Direction.INPUT 23 | pada.pull = digitalio.Pull.UP 24 | 25 | padb = digitalio.DigitalInOut(self.pad_b) 26 | padb.direction = digitalio.Direction.INPUT 27 | padb.pull = digitalio.Pull.UP 28 | 29 | pav = 1 30 | pbv = 1 31 | if(not pada.value): 32 | pav = 0 33 | if(not padb.value): 34 | pbv = 0 35 | 36 | self.prev_state = (self.prev_state & 0x3) << 2 | pav << 1 | pbv 37 | q = t[self.prev_state] 38 | if(q != 'BAD'): 39 | self.q_count += q 40 | if(self.q_count >= 4): 41 | self.onRotate(1) 42 | self.q_count = 0 43 | if(self.q_count <= -4): 44 | self.onRotate(-1) 45 | self.q_count = 0 46 | 47 | pada.deinit() 48 | padb.deinit() 49 | -------------------------------------------------------------------------------- /src/kmk/types.py: -------------------------------------------------------------------------------- 1 | class AttrDict(dict): 2 | ''' 3 | Primitive support for accessing dictionary entries in dot notation. 4 | Mostly for user-facing stuff (allows for `k.KC_ESC` rather than 5 | `k['KC_ESC']`, which gets a bit obnoxious). 6 | 7 | This is read-only on purpose. 8 | ''' 9 | 10 | def __getattr__(self, key): 11 | return self[key] 12 | 13 | 14 | class LayerKeyMeta: 15 | def __init__(self, layer, kc=None): 16 | self.layer = layer 17 | self.kc = kc 18 | 19 | 20 | class ModTapKeyMeta: 21 | def __init__(self, kc=None, mods=None): 22 | self.mods = mods 23 | self.kc = kc 24 | 25 | 26 | class KeySequenceMeta: 27 | def __init__(self, seq): 28 | self.seq = seq 29 | 30 | 31 | class KeySeqSleepMeta: 32 | def __init__(self, ms): 33 | self.ms = ms 34 | 35 | 36 | class UnicodeModeKeyMeta: 37 | def __init__(self, mode): 38 | self.mode = mode 39 | 40 | 41 | class TapDanceKeyMeta: 42 | def __init__(self, codes): 43 | self.codes = codes 44 | -------------------------------------------------------------------------------- /src/lib/adafruit_ble/__init__.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/src/lib/adafruit_ble/__init__.mpy -------------------------------------------------------------------------------- /src/lib/adafruit_ble/advertising/__init__.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/src/lib/adafruit_ble/advertising/__init__.mpy -------------------------------------------------------------------------------- /src/lib/adafruit_ble/advertising/adafruit.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/src/lib/adafruit_ble/advertising/adafruit.mpy -------------------------------------------------------------------------------- /src/lib/adafruit_ble/advertising/apple.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/src/lib/adafruit_ble/advertising/apple.mpy -------------------------------------------------------------------------------- /src/lib/adafruit_ble/advertising/standard.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/src/lib/adafruit_ble/advertising/standard.mpy -------------------------------------------------------------------------------- /src/lib/adafruit_ble/attributes/__init__.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/src/lib/adafruit_ble/attributes/__init__.mpy -------------------------------------------------------------------------------- /src/lib/adafruit_ble/characteristics/__init__.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/src/lib/adafruit_ble/characteristics/__init__.mpy -------------------------------------------------------------------------------- /src/lib/adafruit_ble/characteristics/float.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/src/lib/adafruit_ble/characteristics/float.mpy -------------------------------------------------------------------------------- /src/lib/adafruit_ble/characteristics/int.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/src/lib/adafruit_ble/characteristics/int.mpy -------------------------------------------------------------------------------- /src/lib/adafruit_ble/characteristics/stream.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/src/lib/adafruit_ble/characteristics/stream.mpy -------------------------------------------------------------------------------- /src/lib/adafruit_ble/characteristics/string.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/src/lib/adafruit_ble/characteristics/string.mpy -------------------------------------------------------------------------------- /src/lib/adafruit_ble/services/__init__.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/src/lib/adafruit_ble/services/__init__.mpy -------------------------------------------------------------------------------- /src/lib/adafruit_ble/services/circuitpython.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/src/lib/adafruit_ble/services/circuitpython.mpy -------------------------------------------------------------------------------- /src/lib/adafruit_ble/services/microbit.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/src/lib/adafruit_ble/services/microbit.py -------------------------------------------------------------------------------- /src/lib/adafruit_ble/services/midi.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/src/lib/adafruit_ble/services/midi.mpy -------------------------------------------------------------------------------- /src/lib/adafruit_ble/services/nordic.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/src/lib/adafruit_ble/services/nordic.mpy -------------------------------------------------------------------------------- /src/lib/adafruit_ble/services/sphero.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/src/lib/adafruit_ble/services/sphero.mpy -------------------------------------------------------------------------------- /src/lib/adafruit_ble/services/standard/__init__.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/src/lib/adafruit_ble/services/standard/__init__.mpy -------------------------------------------------------------------------------- /src/lib/adafruit_ble/services/standard/device_info.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/src/lib/adafruit_ble/services/standard/device_info.mpy -------------------------------------------------------------------------------- /src/lib/adafruit_ble/services/standard/hid.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/src/lib/adafruit_ble/services/standard/hid.mpy -------------------------------------------------------------------------------- /src/lib/adafruit_ble/uuid/__init__.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/src/lib/adafruit_ble/uuid/__init__.mpy -------------------------------------------------------------------------------- /src/lib/adafruit_st7789.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/src/lib/adafruit_st7789.mpy -------------------------------------------------------------------------------- /src/lib/pulseio.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/src/lib/pulseio.py -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | 2 | import board 3 | 4 | from kmk.boards.macropact import KMKKeyboard 5 | from kmk.keys import KC, make_key 6 | from kmk.rotary_encoder import Encoder 7 | from kmk.ips import IPS, ips_config 8 | 9 | keyboard = KMKKeyboard() 10 | #keyboard.debug_enabled = True 11 | 12 | def onRotateA(direction): 13 | if(direction > 0): 14 | keyboard._state.tap_key(KC.LSFT(KC.RBRC)) 15 | elif(direction < 0): 16 | keyboard._state.tap_key(KC.LSFT(KC.LBRC)) 17 | 18 | def rgbv_onRotateA(direction): 19 | if(direction > 0): 20 | keyboard.pixels.increase_val() 21 | elif(direction < 0): 22 | keyboard.pixels.decrease_val() 23 | 24 | def rgbh_onRotateA(direction): 25 | if(direction > 0): 26 | keyboard.pixels.increase_hue() 27 | elif(direction < 0): 28 | keyboard.pixels.decrease_hue() 29 | 30 | def rgbs_onRotateA(direction): 31 | if(direction > 0): 32 | keyboard.pixels.increase_sat() 33 | elif(direction < 0): 34 | keyboard.pixels.decrease_sat() 35 | 36 | def onRotateB(direction): 37 | if(direction > 0): 38 | keyboard._state.tap_key(KC.RBRC) 39 | elif(direction < 0): 40 | keyboard._state.tap_key(KC.LBRC) 41 | 42 | def set_default_handler(*args, **kwargs): 43 | keyboard.encoders[0].onRotate = onRotateA 44 | 45 | def set_handler_rgbv(*args, **kwargs): 46 | keyboard.encoders[0].onRotate = rgbv_onRotateA 47 | 48 | def set_handler_rgbh(*args, **kwargs): 49 | keyboard.encoders[0].onRotate = rgbh_onRotateA 50 | 51 | def set_handler_rgbs(*args, **kwargs): 52 | keyboard.encoders[0].onRotate = rgbs_onRotateA 53 | 54 | keyboard.encoders = [Encoder(board.GP0, board.GP1, onRotateA), Encoder(board.GP2, board.GP3, onRotateB)] 55 | keyboard.ips = IPS() 56 | 57 | LAYER1 = KC.MO(1) 58 | LAYER1.after_press_handler(lambda *args, **kwargs: keyboard.ips.load_bitmap("L1.bmp")) 59 | LAYER1.after_release_handler(lambda *args, **kwargs: keyboard.ips.load_bitmap("L0.bmp")) 60 | 61 | LAYER2 = KC.MO(2) 62 | LAYER2.after_press_handler(lambda *args, **kwargs: keyboard.ips.load_bitmap("L2.bmp")) 63 | LAYER2.after_release_handler(lambda *args, **kwargs: keyboard.ips.load_bitmap("L0.bmp")) 64 | 65 | 66 | 67 | RGBV = make_key(on_press=set_handler_rgbv, on_release=set_default_handler) 68 | RGBH = make_key(on_press=set_handler_rgbh, on_release=set_default_handler) 69 | RGBS = make_key(on_press=set_handler_rgbs, on_release=set_default_handler) 70 | 71 | 72 | keyboard.keymap = [ 73 | [KC.W, KC.E, KC.T, KC.Y, KC.NO, 74 | KC.S, KC.G, KC.J, KC.L, KC.NO, 75 | KC.Z, KC.C, KC.V, KC.B, KC.NO, 76 | KC.G, KC.O, KC.P, LAYER2, LAYER1, 77 | ], 78 | [KC.F1, KC.E, KC.LCMD(KC.J), KC.LCMD(KC.LSFT(KC.EQUAL)), KC.NO, 79 | KC.F6, KC.G, KC.LCMD(KC.LSFT(KC.J)), KC.LCMD(KC.MINUS), KC.NO, 80 | KC.Z, KC.C, KC.LCMD(KC.T), KC.LCMD(KC.N0), KC.NO, 81 | KC.LSFT, KC.LCTL, KC.LALT, KC.LCMD, KC.TRNS, 82 | ], 83 | [RGBV, KC.NO, KC.NO, KC.NO, KC.NO, 84 | RGBH, KC.NO, KC.NO, KC.NO, KC.NO, 85 | RGBS, KC.NO, KC.NO, KC.NO, KC.NO, 86 | KC.RGB_TOG, KC.NO, KC.NO, KC.TRNS, KC.TRNS, 87 | ], 88 | ] 89 | 90 | keyboard.rgb_pixel_pin = board.GP28 91 | keyboard.rgb_config['num_pixels'] = 7 92 | keyboard.rgb_config['sat_default'] = 0 93 | keyboard.rgb_config['val_default'] = 255 94 | keyboard.rgb_config['val_step'] = 5 95 | keyboard.rgb_config['hue_step'] = 5 96 | keyboard.rgb_config['sat_step'] = 5 97 | 98 | 99 | if __name__ == '__main__': 100 | keyboard.ips.load_bitmap("L0.bmp") 101 | keyboard.go() 102 | -------------------------------------------------------------------------------- /src/neopixel.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2016 Damien P. George 4 | # Copyright (c) 2017 Scott Shawcroft for Adafruit Industries 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | # THE SOFTWARE. 23 | 24 | """ 25 | `neopixel` - NeoPixel strip driver 26 | ==================================================== 27 | 28 | * Author(s): Damien P. George & Scott Shawcroft 29 | """ 30 | 31 | import math 32 | 33 | import digitalio 34 | from neopixel_write import neopixel_write 35 | 36 | __version__ = "0.0.0-auto.0" 37 | __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_NeoPixel.git" 38 | 39 | # Pixel color order constants 40 | RGB = (0, 1, 2) 41 | """Red Green Blue""" 42 | GRB = (1, 0, 2) 43 | """Green Red Blue""" 44 | RGBW = (0, 1, 2, 3) 45 | """Red Green Blue White""" 46 | GRBW = (1, 0, 2, 3) 47 | """Green Red Blue White""" 48 | 49 | class NeoPixel: 50 | """ 51 | A sequence of neopixels. 52 | 53 | :param ~microcontroller.Pin pin: The pin to output neopixel data on. 54 | :param int n: The number of neopixels in the chain 55 | :param int bpp: Bytes per pixel. 3 for RGB and 4 for RGBW pixels. 56 | :param float brightness: Brightness of the pixels between 0.0 and 1.0 where 1.0 is full 57 | brightness 58 | :param bool auto_write: True if the neopixels should immediately change when set. If False, 59 | `show` must be called explicitly. 60 | :param tuple pixel_order: Set the pixel color channel order. GRBW is set by default. 61 | 62 | Example for Circuit Playground Express: 63 | 64 | .. code-block:: python 65 | 66 | import neopixel 67 | from board import * 68 | 69 | RED = 0x100000 # (0x10, 0, 0) also works 70 | 71 | pixels = neopixel.NeoPixel(NEOPIXEL, 10) 72 | for i in range(len(pixels)): 73 | pixels[i] = RED 74 | 75 | Example for Circuit Playground Express setting every other pixel red using a slice: 76 | 77 | .. code-block:: python 78 | 79 | import neopixel 80 | from board import * 81 | import time 82 | 83 | RED = 0x100000 # (0x10, 0, 0) also works 84 | 85 | # Using ``with`` ensures pixels are cleared after we're done. 86 | with neopixel.NeoPixel(NEOPIXEL, 10) as pixels: 87 | pixels[::2] = [RED] * (len(pixels) // 2) 88 | time.sleep(2) 89 | """ 90 | def __init__(self, pin, n, *, bpp=3, brightness=1.0, auto_write=True, pixel_order=None): 91 | self.pin = digitalio.DigitalInOut(pin) 92 | self.pin.direction = digitalio.Direction.OUTPUT 93 | self.n = n 94 | if pixel_order is None: 95 | self.order = GRBW 96 | self.bpp = bpp 97 | else: 98 | self.order = pixel_order 99 | self.bpp = len(self.order) 100 | self.buf = bytearray(self.n * self.bpp) 101 | # Set auto_write to False temporarily so brightness setter does _not_ 102 | # call show() while in __init__. 103 | self.auto_write = False 104 | self.brightness = brightness 105 | self.auto_write = auto_write 106 | 107 | def deinit(self): 108 | """Blank out the NeoPixels and release the pin.""" 109 | for i in range(len(self.buf)): 110 | self.buf[i] = 0 111 | neopixel_write(self.pin, self.buf) 112 | self.pin.deinit() 113 | 114 | def __enter__(self): 115 | return self 116 | 117 | def __exit__(self, exception_type, exception_value, traceback): 118 | self.deinit() 119 | 120 | def __repr__(self): 121 | return "[" + ", ".join([str(x) for x in self]) + "]" 122 | 123 | def _set_item(self, index, value): 124 | if index < 0: 125 | index += len(self) 126 | if index >= self.n or index < 0: 127 | raise IndexError 128 | offset = index * self.bpp 129 | r = 0 130 | g = 0 131 | b = 0 132 | w = 0 133 | if isinstance(value, int): 134 | if value>>24: 135 | raise ValueError("only bits 0->23 valid for integer input") 136 | r = value >> 16 137 | g = (value >> 8) & 0xff 138 | b = value & 0xff 139 | w = 0 140 | # If all components are the same and we have a white pixel then use it 141 | # instead of the individual components. 142 | if self.bpp == 4 and r == g and g == b: 143 | w = r 144 | r = 0 145 | g = 0 146 | b = 0 147 | elif (len(value) == self.bpp) or ((len(value) == 3) and (self.bpp == 4)): 148 | r, g, b, w = value if len(value) == 4 else value+(0,) 149 | else: 150 | raise ValueError("Color tuple size does not match pixel_order.") 151 | 152 | self.buf[offset + self.order[0]] = r 153 | self.buf[offset + self.order[1]] = g 154 | self.buf[offset + self.order[2]] = b 155 | if self.bpp == 4: 156 | self.buf[offset + self.order[3]] = w 157 | 158 | def __setitem__(self, index, val): 159 | if isinstance(index, slice): 160 | start, stop, step = index.indices(len(self.buf) // self.bpp) 161 | length = stop - start 162 | if step != 0: 163 | length = math.ceil(length / step) 164 | if len(val) != length: 165 | raise ValueError("Slice and input sequence size do not match.") 166 | for val_i, in_i in enumerate(range(start, stop, step)): 167 | self._set_item(in_i, val[val_i]) 168 | else: 169 | self._set_item(index, val) 170 | 171 | if self.auto_write: 172 | self.show() 173 | 174 | def __getitem__(self, index): 175 | if isinstance(index, slice): 176 | out = [] 177 | for in_i in range(*index.indices(len(self.buf) // self.bpp)): 178 | out.append(tuple(self.buf[in_i * self.bpp + self.order[i]] 179 | for i in range(self.bpp))) 180 | return out 181 | if index < 0: 182 | index += len(self) 183 | if index >= self.n or index < 0: 184 | raise IndexError 185 | offset = index * self.bpp 186 | return tuple(self.buf[offset + self.order[i]] 187 | for i in range(self.bpp)) 188 | 189 | def __len__(self): 190 | return len(self.buf) // self.bpp 191 | 192 | @property 193 | def brightness(self): 194 | """Overall brightness of the pixel""" 195 | return self._brightness 196 | 197 | @brightness.setter 198 | def brightness(self, brightness): 199 | # pylint: disable=attribute-defined-outside-init 200 | self._brightness = min(max(brightness, 0.0), 1.0) 201 | if self.auto_write: 202 | self.show() 203 | 204 | def fill(self, color): 205 | """Colors all pixels the given ***color***.""" 206 | auto_write = self.auto_write 207 | self.auto_write = False 208 | for i, _ in enumerate(self): 209 | self[i] = color 210 | if auto_write: 211 | self.show() 212 | self.auto_write = auto_write 213 | 214 | def write(self): 215 | """.. deprecated: 1.0.0 216 | 217 | Use ``show`` instead. It matches Micro:Bit and Arduino APIs.""" 218 | self.show() 219 | 220 | def show(self): 221 | """Shows the new colors on the pixels themselves if they haven't already 222 | been autowritten. 223 | 224 | The colors may or may not be showing after this function returns because 225 | it may be done asynchronously.""" 226 | if self.brightness > 0.99: 227 | neopixel_write(self.pin, self.buf) 228 | else: 229 | neopixel_write(self.pin, bytearray([int(i * self.brightness) for i in self.buf])) -------------------------------------------------------------------------------- /stl/Bottom.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/stl/Bottom.stl -------------------------------------------------------------------------------- /stl/GlowInsert.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/stl/GlowInsert.stl -------------------------------------------------------------------------------- /stl/TopPlate.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbjunky/MacroPact/2a080a2c0c7c7e499d7a5904099f2a94e0b7d3da/stl/TopPlate.stl --------------------------------------------------------------------------------