├── LICENSE
├── README.md
├── rsc
├── final1.jpg
├── final2.jpg
├── final3.jpg
├── key_layout.png
└── layout_diagram.png
├── src
└── macro_keyboard
│ └── macro_keyboard.ino
└── stl
├── bottom.stl
└── lid.stl
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Alin Baciu
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 | # Macro Keyboard
2 | A 15-key Arduino-based, bluetooth wireless, mechanical macro keyboard.
3 | I made this to improve my workflow at work and to avoid the finger gymnastics characteristic to iOS development. It charges and can be programed on-the-fly via USB-C.
4 | # Glamour shots
5 | 
6 | 
7 | 
8 | # How it works
9 | The keyboard is designed to work in conjunction with a tool like BetterTouchTool, which maps key presses to other shortcuts, based on the app in focus. Although I haven't tested it (lacking a PC with bluetooth), this should work just fine on Windows using a tool like AutoHotkey
10 |
11 | The keyboard transmits CMD + control + shift + F1 -> F12 and 1 -> 3, to cover all the keys.
12 |
13 | Using BetterTouchTool, these inputs can be mapped to various actions for each app.
14 |
15 | Besides this, the top left key "master", when long pressed, transmits CMD + control + shift + option + F1 and puts the keyboard in sleep mode. This is useful for setting up a shortcut to lock the Mac.
16 |
17 | After 5 minutes where no keys are pressed, the keyboard turns off the bluetooth module and the arduino goes to deep sleep. The "master" key acts as a wake-up key and will turn the arduino and the bluetooth back on.
18 | In my use, the battery lasts about 2-3 weeks.
19 |
20 | At startup, holding the *master* key and the first key on its right and then turning on the power, will force *Pairing mode*
21 | Holding *master* and the second key on its right will clear all paired devices.
22 |
23 | The keys are set up like this:
24 |
25 | CMD + control + shift +
26 | 
27 |
28 | # Folder structure
29 |
30 | .
31 | ├── src # source code for the arduino
32 | └── stl # STL files for the case and the lid
33 |
34 | # Bill of materials
35 | | Item | Description |
36 | | ------ | ------ |
37 | | 15 Cherry MX Mechanical switches with diode | I used Ebay, but the closest/cheapest the better |
38 | | Arduino Pro Micro 32u4 (I used a clone) | Ebay |
39 | | HC-05 bluetooth module | Take care to not get the "fake" one. You need to flash it with the RN-42 Firmware and the "fake" one won't work. Google is your friend here |
40 | | Adafruit USB-C breakout | https://www.adafruit.com/product/4090 |
41 | | 3mm LED | Used to indicate various actions. I used blue to match my case |
42 | | 1400 MaH Lipo Battery JA-803450P | You can pick another one, but the case is designed with these dimensions in mind |
43 | | TP4056 LiPo charger | Ebay or Aliexpress |
44 | | SPDT Switch | https://www.adafruit.com/product/805 |
45 | | Diodes | |
46 | | 2N2222 transistor | Used to turn the bluetooth module on/off to when entering/exiting sleep mode |
47 | | P-channel mosfet | Used for LiPo bypass when charging. We don't want to drain the battery while it's charging, as this can break the LiPo |
48 | | Resistors | Used as pull-down resistors for the keyboard matrix and as a voltage divider to check the LiPo battery level |
49 | | Keycaps | I 3D printed some from https://www.thingiverse.com/thing:468651, but you can pick whatever you fancy |
50 | | Spare micro-USB cable | Canibalized to link the Arduino to the USB-C breakout board. This allows programming without taking the keyboard apart. |
51 | | 4 x anti-slip pads | Need to fit into the circular cut-outs on the bottom of the case. Help keep it steady on the desk and compensate for slight warping from 3D printing. I used transparent self-adhesive ones, but you can stick them on with anything |
52 |
53 | # The layout
54 | The box is designed for the components I used. It's a bit of a squeeze, but everything fits in the end. You can see its layout in the diagram below. The empty spaces are used to store wires, resistors and the NPN transistor used to turn the bluetooth board on/off.
55 | 
56 |
57 | # The circuit
58 | The keys are set up using a keyboard matrix. The top-left key is not included in the matrix, as it has to be able to wake the Arduino from sleep. It is set up as an interrupt.
59 |
60 | Since the LiPo charger doesn't have a bypass for when it's plugged in, and it's dangerous to charge a LiPo while draining it, a bypass circuit is needed. I used the one described here and it works perfectly: https://arduino.stackexchange.com/questions/39805/can-you-charge-and-use-a-lipo-battery-at-the-same-time.
61 |
62 | # Missing stuff
63 | - Circuit diagram (coming soon)
64 | - Ghosting mitigation
65 | - Sending keys through the USB connection. Technically possible and the reason why I chose a 32u4 Arduino, but still to be implemented
66 | - LiPo voltage reading isn't the best
67 | - This could easily be implemented with an Adafruit Feather 32u4 Bluefruit (https://learn.adafruit.com/adafruit-feather-32u4-bluefruit-le/overview). It has on-board bluetooth, LiPo voltage reading and reporting along with a battery charger. It should simplify the circuit, fit fine in the case and could allow for a larger battery, too.
--------------------------------------------------------------------------------
/rsc/final1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Logomorph/MacroKeyboard/de2faad629cfe0f2a03fc887a0c347e788dfa723/rsc/final1.jpg
--------------------------------------------------------------------------------
/rsc/final2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Logomorph/MacroKeyboard/de2faad629cfe0f2a03fc887a0c347e788dfa723/rsc/final2.jpg
--------------------------------------------------------------------------------
/rsc/final3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Logomorph/MacroKeyboard/de2faad629cfe0f2a03fc887a0c347e788dfa723/rsc/final3.jpg
--------------------------------------------------------------------------------
/rsc/key_layout.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Logomorph/MacroKeyboard/de2faad629cfe0f2a03fc887a0c347e788dfa723/rsc/key_layout.png
--------------------------------------------------------------------------------
/rsc/layout_diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Logomorph/MacroKeyboard/de2faad629cfe0f2a03fc887a0c347e788dfa723/rsc/layout_diagram.png
--------------------------------------------------------------------------------
/src/macro_keyboard/macro_keyboard.ino:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include
5 | #include
6 |
7 | int r[3] = { 9, 8, 10 };
8 | int c[5] = { 6, 5, 4, 7, 2};
9 | int masterKey = 3;
10 | int statusLed = A2;
11 | int internalLed = 17;
12 | int btPower = A3;
13 | int lipoVoltage = A0;
14 | int sleepTriggerThreshold = 2000; // 2 seconds
15 | unsigned long sleepThreshold = 300000; // 5 minutes
16 | bool sentEmptyKey;
17 | unsigned long lastKeyPress;
18 |
19 | #define RX_PIN 16
20 | #define TX_PIN 14
21 |
22 | //F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12 1 2 3
23 | byte btKeys[15] = { 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x1E, 0x1F, 0x20 };
24 |
25 | SoftwareSerial btSerial(RX_PIN, TX_PIN);
26 |
27 | struct KeyInfo {
28 | unsigned long val;
29 | unsigned long last;
30 | };
31 | KeyInfo matrix[15];
32 |
33 | // Battery management
34 | float lipoValue = 0.0;
35 | unsigned long lastBatteryCheck = 0;
36 | unsigned long lastBatteryWarning = 0;
37 | bool isLowBattery = false;
38 | bool isEmptyBattery = false;
39 |
40 | void setup() {
41 | lastKeyPress = millis();
42 | pinMode(lipoVoltage, INPUT);
43 | pinMode(statusLed, OUTPUT);
44 | digitalWrite(statusLed, LOW);
45 | pinMode(btPower, OUTPUT);
46 | digitalWrite(btPower, HIGH);
47 | enableStatusLed(true);
48 | resetKeyboardMatrix();
49 | for (int i = 0; i < 3; i++) {
50 | pinMode(r[i], OUTPUT);
51 | digitalWrite(r[i], LOW);
52 | }
53 | for (int i = 0; i < 5; i++) {
54 | pinMode(c[i], INPUT);
55 | digitalWrite(c[i], LOW);
56 | }
57 | pinMode(masterKey, INPUT);
58 | digitalWrite(masterKey, LOW);
59 |
60 | Serial.begin(115200);
61 | enableBluetooth(true);
62 | readKeyboardData();
63 | processStartupHotkeys();
64 | }
65 |
66 | void loop() {
67 | readKeyboardData();
68 | processKeyPresses();
69 | monitorBattery();
70 |
71 | while (btSerial.available() > 0) {
72 | Serial.write(btSerial.read());
73 | }
74 | while (Serial.available() > 0) {
75 | btSerial.write(Serial.read());
76 | }
77 | if (millis() - lastKeyPress > sleepThreshold) {
78 | enterSleep();
79 | }
80 | }
81 |
82 | void monitorBattery() {
83 | unsigned long currentTime = millis();
84 | // Only check the voltage every 10 seconds
85 | if (currentTime - lastBatteryCheck > 10000 && !isLowBattery && !isEmptyBattery) {
86 | return;
87 | }
88 | lastBatteryCheck = currentTime;
89 | lipoValue = analogRead(lipoVoltage);
90 | lipoValue = (lipoValue * 2.0f * 3.3f) / 1024.0f;
91 | if (lipoValue < 3.2) {
92 | isLowBattery = true;
93 | }
94 | if (lipoValue < 3.1) {
95 | isEmptyBattery = true;
96 | }
97 | if (isEmptyBattery && currentTime - lastBatteryWarning > 1000) {
98 | enableStatusLed(true);
99 | lastBatteryWarning = currentTime;
100 | } else if (isLowBattery && currentTime - lastBatteryWarning > 3000) {
101 | enableStatusLed(true);
102 | lastBatteryWarning = currentTime;
103 | }
104 | }
105 |
106 | void resetKeyboardMatrix() {
107 | for (int i = 0; i < 15; i++) {
108 | matrix[i].val = 0;
109 | matrix[i].last = 0;
110 | }
111 | }
112 |
113 | void readKeyboardData() {
114 | int val = digitalRead(masterKey);
115 | processKey(0, 0, val);
116 | for (int i = 0; i < 3; i++) {
117 | digitalWrite(r[i], HIGH);
118 | for (int j = 0; j < 5; j++) {
119 | if (i == 0 && j == 0) {
120 | continue;
121 | }
122 | int val = digitalRead(c[j]);
123 | processKey(i, j, val);
124 | }
125 | digitalWrite(r[i], LOW);
126 | }
127 | enableStatusLed(false);
128 | }
129 |
130 | void processKey(int i, int j, int val) {
131 | int index = i*5 + j;
132 | if (val == 1) {
133 | if (matrix[index].val == 0 ) {
134 | Serial.print(i);
135 | Serial.print(" ");
136 | Serial.println(j);
137 | matrix[index].val = millis();
138 | enableStatusLed(true);
139 | }
140 | } else {
141 | matrix[index].val = 0;
142 | }
143 | }
144 |
145 | void processStartupHotkeys() {
146 | enableStatusLed(true);
147 | if (matrix[0].val != 0 && matrix[2].val != 0) {
148 | btClearPairedDevices();
149 | }
150 | if (matrix[0].val != 0 && matrix[1].val != 0) {
151 | btEnterPairMode();
152 | }
153 | enableStatusLed(false);
154 | }
155 |
156 | void processKeyPresses() {
157 | unsigned long currentTime = millis();
158 | if (currentTime - matrix[0].val > sleepTriggerThreshold && matrix[0].val != 0) {
159 | if (matrix[1].val != 0) {
160 | btSerial.print("Logokeys voltage: ");
161 | btSerial.println(lipoValue);
162 | delay(1000);
163 | resetKeyboardMatrix();
164 | return;
165 | }
166 | resetKeyboardMatrix();
167 |
168 | byte data[] = {0xFD, 0x9, 0x1, (byte)0b00001111, (byte)0x0,(byte)0x3A,(byte)0x0,(byte)0x0,(byte)0x0,(byte)0x0,(byte)0x0,(byte)0x0,(byte)0x0};
169 | btSerial.write(data, sizeof(data));
170 |
171 | byte data2[] = {0xFD, 0x9, 0x1, (byte)0x0, (byte)0x0,(byte)0x0,(byte)0x0,(byte)0x0,(byte)0x0,(byte)0x0,(byte)0x0,(byte)0x0,(byte)0x0};
172 | btSerial.write(data2, sizeof(data2));
173 |
174 | enableStatusLed(true);
175 | delay(1000);
176 | enableStatusLed(false);
177 | enterSleep();
178 | } else {
179 | byte data[] = {0xFE, 0x5, (byte)0b0000111, (byte)0x0, (byte)0x0, (byte)0x0, (byte)0x0, (byte)0x0};
180 | byte keysDown = 0;
181 | int index = 3;
182 | for (int i=0; i<15; i++) {
183 | if (matrix[i].val != 0 && matrix[i].last == 0 && keysDown < 5) {
184 | data[index] = btKeys[i];
185 | keysDown++;
186 | index++;
187 | matrix[i].last = matrix[i].val;
188 | lastKeyPress = currentTime;
189 | }
190 | if (matrix[i].val == 0 && matrix[i].last != 0) {
191 | matrix[i].last = 0;
192 | }
193 | }
194 | if (keysDown != 0) {
195 | sentEmptyKey = false;
196 | }
197 | if (keysDown == 0 && !sentEmptyKey) {
198 | sentEmptyKey = true;
199 | byte empty[] = {0xFE, 0x0};
200 | btSerial.write(empty, sizeof(empty));
201 | }
202 | if (keysDown == 0 && sentEmptyKey) {
203 | return;
204 | }
205 | byte* finalArray = (byte*) malloc((keysDown+3) * sizeof(byte));
206 |
207 | finalArray[1] = keysDown;
208 | for (byte i=0;i