├── lights-on.png
├── end-result.jpg
├── 3d-files
├── macropad-top.stl
├── macropad-bottom.stl
├── macropad-top.blend
├── macropad-bottom.blend
└── warning.txt
├── Linux-Tools
├── key-macros.sh
└── send-macropad-command.py
├── README.md
└── MacroKeyPad
└── MacroKeyPad.ino
/lights-on.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atkaper/macro-keypad/HEAD/lights-on.png
--------------------------------------------------------------------------------
/end-result.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atkaper/macro-keypad/HEAD/end-result.jpg
--------------------------------------------------------------------------------
/3d-files/macropad-top.stl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atkaper/macro-keypad/HEAD/3d-files/macropad-top.stl
--------------------------------------------------------------------------------
/3d-files/macropad-bottom.stl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atkaper/macro-keypad/HEAD/3d-files/macropad-bottom.stl
--------------------------------------------------------------------------------
/3d-files/macropad-top.blend:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atkaper/macro-keypad/HEAD/3d-files/macropad-top.blend
--------------------------------------------------------------------------------
/3d-files/macropad-bottom.blend:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atkaper/macro-keypad/HEAD/3d-files/macropad-bottom.blend
--------------------------------------------------------------------------------
/3d-files/warning.txt:
--------------------------------------------------------------------------------
1 | The bottom part had too small of a fit, and you need to scale that one up a bit when slicing (a couple percent).
2 | Or just redesign the case ;-)
3 |
4 |
--------------------------------------------------------------------------------
/Linux-Tools/key-macros.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | if [ "$1" == "" ]
4 | then
5 | echo "Usage: $0 macroname"
6 | echo "Macros:"
7 | echo " teams-toggle-mic = toggle microphone input in ms-teams window"
8 | echo " teams-toggle-video = toggle video/camera in ms-teams window"
9 | echo " teams-new-message = start writing new message in extended edit mode"
10 | echo " teams-exit = end call, or cancel composing message"
11 | echo " mpx-toggle-draw = toggle drawing on screen (gromit-mpx tool)"
12 | echo " mpx-clear = clear drawings from screen (gromit-mpx tool)"
13 | echo " screenshot-region = make screenshot of region of screen"
14 | exit 1
15 | fi
16 |
17 | DONE=0
18 |
19 | if [ "$1" == "teams-toggle-mic" ]
20 | then
21 | DONE=1
22 | # remember current active window, focus teams, send keys "ctrl+shift+m", change focus back
23 | CURWIN=$(printf "0x%0x" $(xdotool getactivewindow))
24 | wmctrl -x -R "microsoft teams - preview"
25 | xdotool key ctrl+shift+m
26 | wmctrl -a $CURWIN -i
27 | fi
28 |
29 | if [ "$1" == "teams-toggle-video" ]
30 | then
31 | DONE=1
32 | # remember current active window, focus teams, send keys "ctrl+shift+o", change focus back
33 | CURWIN=$(printf "0x%0x" $(xdotool getactivewindow))
34 | wmctrl -x -R "microsoft teams - preview"
35 | xdotool key ctrl+shift+o
36 | wmctrl -a $CURWIN -i
37 | fi
38 |
39 | if [ "$1" == "teams-new-message" ]
40 | then
41 | DONE=1
42 | # focus teams, send keys "alt+shift+c" "ctrl+shift+x"
43 | wmctrl -x -R "microsoft teams - preview"
44 | xdotool key alt+shift+c
45 | sleep 0.2
46 | xdotool key ctrl+shift+x
47 | fi
48 |
49 | if [ "$1" == "teams-exit" ]
50 | then
51 | DONE=1
52 | # remember current active window, focus teams, send keys "ctrl+shift+h" escape, change focus back
53 | # the ctrl+shift+h should en a call, and the escape should cancel starting a new message (attempt at double use of the macro-key)
54 | CURWIN=$(printf "0x%0x" $(xdotool getactivewindow))
55 | wmctrl -x -R "microsoft teams - preview"
56 | xdotool key ctrl+shift+h
57 | sleep 0.1
58 | xdotool key 0xff1b
59 | wmctrl -a $CURWIN -i
60 | fi
61 |
62 | if [ "$1" == "mpx-toggle-draw" ]
63 | then
64 | DONE=1
65 | # tell gromit-mpx tool to toggle drawing (note: key 180 is a web-homepage key which I attached to gromit-mpx)
66 | # I start the tool on startup using: /usr/bin/gromit-mpx -K 180
67 | xdotool key 180
68 | fi
69 |
70 | if [ "$1" == "mpx-clear" ]
71 | then
72 | DONE=1
73 | # tell gromit-mpx to clear screen (note: using a custom assigned key)
74 | # I start the tool on startup using: /usr/bin/gromit-mpx -K 180
75 | xdotool key shift+180
76 | fi
77 |
78 | if [ "$1" == "screenshot-region" ]
79 | then
80 | DONE=1
81 | # make screenshot of region, and write as file with date/time in name
82 | /usr/bin/xfce4-screenshooter -r -s $HOME/Pictures/screenshots/screenshot-`/bin/date +"%Y%m%d-%H%M%S"`.png
83 | # turn off the led when done
84 | send-macropad-command.py d 7
85 | fi
86 |
87 | if [ "$DONE" != "1" ]
88 | then
89 | echo "Unknown macro name '$1'"
90 | echo
91 | $0
92 | fi
93 |
94 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Macro Keypad
2 |
3 | The Macro Keypad is an 8 key USB keyboard, with programmable keys, and a led in each key.
4 | See blog post: https://www.kaper.com/electronics/macro-keypad/ for schematics, more information, and on
5 | how to build this.
6 |
7 | ## Some images of the end result
8 |
9 |
10 |
11 |
12 |
13 | ## Used components
14 |
15 | The next (2) titles are taken from the Aliexpress site, where I ordered the parts:
16 |
17 | - Honyone TS26 Series Square With LED Momentary SPST PCB Mini Push Button Tact Switch --> [12x 0.81 euro, when I bought them].
18 | - PRO MICRO/MINI/TYPE-C USB 5V 16MHz Board Module For Arduino/Leonardo ATMEGA32U4-AU/MU Controller Pro-Micro Replace Pro Mini 1PCS --> [1x 5.62 euro, when I bought it - I took the USB-C one].
19 | - 8x 100 Ohm resistor (taken from a previously ordered set of many resistor values).
20 | - Some spare through-hole protoboard (experiment printed circuit board).
21 |
22 | ## Files/Folders in this Git Repository
23 |
24 | Folders:
25 |
26 | - ```3d-files``` ; contains the blender and STL files to print the case.
27 | - ```MacroKeyPad``` ; contains the Arduino code to run on the microcontroller.
28 | - ```Linux-Tools``` ; contains Linux scripts I use for executing macro's and talking to the keypad lights.
29 |
30 | Linux-Tools Files:
31 |
32 | - ```key-macros.sh``` ; a shell script which can execute some functions I needed. These include talking to MS-Teams
33 | chat/video, drawing on screen, region-screenshot, and starting a terminal/shell.
34 | - ```send-macropad-command.py``` ; python script to change led states, and change key functions/mappings.
35 |
36 | ## Compile code / program the controller
37 |
38 | The ```MacroKeyPad.ino``` in the ```MacroKeyPad``` folder can be opened in the Arduino IDE (see https://www.arduino.cc/).
39 | To compile the code ("sketch"), you need to add support for the Arduino Leonardo ATMEGA 32U4 Pro micro.
40 | Use this URL as board manager setting (Files / Preferences / Additional Boards Manager URLs):
41 | https://raw.githubusercontent.com/sparkfun/Arduino_Boards/master/IDE_Board_Manager/package_sparkfun_index.json
42 | Via: Tools / Board / Boards Manager you can add missing boards.
43 |
44 | In Tools menu: Choose board "SparkFun Pro Micro", CPU 16 MHZ, 5V. And the proper device to send it to.
45 | For me that was: /dev/ttyACM0. And then hit the "upload" button.
46 | If it does not understand some of the used includes, you might need to add some libraries using the:
47 | Sketch / Include Library / Manage Libraries menu item.
48 |
49 | Unfortunately, my Arduino IDE already has all stuff I need, so I do not know what you will need to add ;-)
50 | Perhaps one day I'll try a fresh installation to write down the exact steps. For now, just use an internet
51 | search if you get stuck.
52 |
53 | After you did program the controller, and completed building it, you are ready to test it.
54 | The ```send-macropad-command.py``` tool can be used to change behaviour, and change led states.
55 | And for the rest you will need to either put the proper key you need in the keypad definition, or
56 | use your operating systems keyboard mapping possibilities or external tools to add special actions on each
57 | key-press.
58 |
59 | For me this was simple. I have mapped the F13..F22 keys to the keypad, and in Linux Mint, I can simply
60 | open up the Keyboard GUI program to assign Application Shortcuts to those keys. As shortcuts I used the
61 | shell script ```key-macros.sh``` for the different functions. But some of these can also be done directly in the
62 | Keyboard GUI settings.
63 |
64 | Note: the two ```Linux-Tools``` script files should be places in a ```bin``` folder in your path. I used my
65 | bin folder in my user's home directory for that, and added it to the PATH setting.
66 |
67 | For more information, see the blog-post as mentioned at the top of this page.
68 |
69 | Thijs Kaper, January 29, 2022.
70 |
--------------------------------------------------------------------------------
/Linux-Tools/send-macropad-command.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 |
3 | """
4 | send-macropad-command.py
5 |
6 | This tool can be used to interact with the macro key pad from within batch scripts,
7 | or from the shell/bash command line.
8 |
9 | Type:
10 | - "send-macropad-command.py -h" for help about this tool.
11 | - "send-macropad-command.py help" for help about the macro pad commands.
12 |
13 | This last help, needs a functioning macro pad connected, and proper detection of
14 | the com port / device to use. If auto detection does not work, execute:
15 |
16 | "send-macropad-command.py -v -l" to debug it.
17 |
18 | Fixing auto detect can be done either by using the -d option, or by finding
19 | some unique words in the "-l" data-set, and passing these in via "-a".
20 |
21 | Thijs Kaper, January 29, 2022.
22 | """
23 |
24 | # Silence some pylint warnings (non-standard names or globals, too general exception catching)
25 | #pylint:disable=C0103,W0703,W0603
26 |
27 | import argparse
28 | import re
29 | import textwrap
30 | import sys
31 | import threading
32 | import serial
33 | import serial.tools.list_ports
34 |
35 | # Define command line argument parser
36 | parser = argparse.ArgumentParser(
37 | formatter_class=argparse.RawDescriptionHelpFormatter,
38 | description='Send command to macro keypad',
39 | epilog=textwrap.dedent('''\
40 | Note: if you use the 'f' or 'flash' command, you need to increase the timeout if you also
41 | need a response to be read. Count each flash time double (one for on, one for off time).
42 |
43 | Examples:
44 | %(prog)s -v -l # debug port autodetection
45 | %(prog)s -a "9206 hidpc" -l -v # change auto detect keywords to find proper macro pad device
46 | %(prog)s help # get command help
47 | %(prog)s -t 0.3 help # get command help, use increased timeout on slow computer
48 | %(prog)s -t 0.5 t 2 f 1 200 e 2 g 1 # toggle 2, flash 1 for 200ms, and turn on 2 after that, read 1
49 | %(prog)s -d /dev/ttyACM0 t 1 # toggle led 1, use device /dev/ttyACM0 instead of autodetecting the device
50 | _
51 | ''')
52 | )
53 | parser.add_argument(dest='commands', metavar='command', nargs='*')
54 | parser.add_argument('-i', '--interactive', dest='interactive', action='store_true',
55 | help='interactive mode (end with "exit" or ctrl-d)')
56 | parser.add_argument('-v', '--verbose', dest='verbose', action='store_true',
57 | help='verbose / debug mode')
58 | parser.add_argument('-q', '--quiet', dest='quiet', action='store_true', help='quiet / silent mode')
59 | parser.add_argument('-l', '--list', dest='list', action='store_true', help='list serial ports')
60 | parser.add_argument('-t', '--timeout', metavar='timeout', type=float, dest='timeout', default=0.1,
61 | help='time to wait for macro keypad to send a response to the command'
62 | ' (default %(default)s, if partial or no response, change to 0.2 or higher)')
63 | parser.add_argument('-b', '--baud-rate', metavar='bps', type=int, dest='baud_rate', default=115200,
64 | help='baud rate, not interesting for atmega-32u4, but might be for others'
65 | ' (default %(default)s)')
66 | group = parser.add_mutually_exclusive_group()
67 | group.add_argument('-d', '--device', metavar='device', dest='device',
68 | help='serial device to use (example /dev/ttyACM0)')
69 | group.add_argument('-a', '--autodetect', metavar='matchwords', dest='autodetect',
70 | default='1B4F:9206 SparkFun',
71 | help='auto detect serial port hwid/description words (default: %(default)s)')
72 | args = parser.parse_args()
73 |
74 | def debug(data):
75 | """Debug / verbose conditional print function (enable with -v)"""
76 | if args.verbose:
77 | print("# " + data)
78 |
79 | # Autodetect the port to use. Will be overwritten later, if someone specified the -d flag.
80 | autodetectregex = ".*" + (args.autodetect.lower()).replace(" ", ".*") + ".*"
81 | comportlist = serial.tools.list_ports.comports()
82 | device = None
83 | debug("Autodetect search regex: " + autodetectregex)
84 | for element in comportlist:
85 | finder = (str(element.hwid) + " " + str(element.description)).lower()
86 | debug("For device " + element.device + "; Autodetect string to match against: " + finder)
87 | if re.match(autodetectregex, finder):
88 | debug("Match: autodetected device: " + element.device)
89 | if device and not args.device:
90 | print("WARNING: multiple matches on autodetect. Last match will be used."
91 | "Use -v and -l options to investigate, and change -a or -d value to fix this!")
92 | print("Previous match: " + device + ", current match: " + element.device)
93 | device = element.device
94 |
95 | # If the -l option was used, show available serial ports,
96 | # and show which one would be chosen from the autodetect keywords.
97 | if args.list:
98 | print("The following serial ports are found:\n")
99 | for element in comportlist:
100 | print("device:" + element.device + "\n description:" + str(element.description) +
101 | "\n hwid:" + str(element.hwid) + "\n")
102 | print("Based on the autodetect setting keywords, we would choose: " + str(device))
103 | sys.exit(0)
104 |
105 | # Override device when -d defined.
106 | if args.device:
107 | debug("Overriding autodetected device " + str(device) + " with " + args.device)
108 | device = args.device
109 |
110 | debug("Device to use: " + device + ", " + str(args.baud_rate) + " baud")
111 |
112 | # Check if the user specified either any commands to send or to use interactive mode.
113 | if (not args.commands) != args.interactive:
114 | print("Either specify a command as argument, or use -i for interactve mode.\n")
115 | parser.print_help()
116 | sys.exit(1)
117 |
118 | # Open the serial port.
119 | try:
120 | ser = serial.Serial(device, args.baud_rate, timeout=args.timeout)
121 | except Exception as msg:
122 | print("Error opening serial port " + device + ", error: " + str(msg))
123 | sys.exit(1)
124 |
125 | # Flag for use in interactive mode to indicate (not) end of processing.
126 | keep_reading = True
127 |
128 | # response read function
129 | def read_response():
130 | """Try to read response, catch any excpetions."""
131 | global ser, keep_reading
132 | try:
133 | response = ser.read(size=2048).decode('utf-8').strip()
134 | except Exception as msg:
135 | print("Error reading response: " + str(msg))
136 | keep_reading = False
137 | return None
138 | return response
139 |
140 | # reader thread function
141 | def response_reader_thread():
142 | """Routine for background thread to keep reading and printing data as read from serial port."""
143 | global keep_reading
144 | debug("Start background thread for reading responses")
145 | while keep_reading:
146 | response = read_response()
147 | if response:
148 | print(response)
149 | debug("End background thread for reading responses")
150 |
151 | # If user passed in "-i" option, we are using interactive mode.
152 | # Keep asking for user input, and start a response reader thread in the background.
153 | if args.interactive:
154 | t1 = threading.Thread(target=response_reader_thread)
155 | t1.start()
156 | debug("Start interactive mode, reading stdin until end-of-file (ctrl-d, or type exit)")
157 | for line in sys.stdin:
158 | line = line.strip()
159 | if line == "exit" or not keep_reading:
160 | debug("Stopping")
161 | break
162 | debug("Send: " + line)
163 | ser.write((line + "\n").encode('utf-8'))
164 | debug("End of interactive mode")
165 | keep_reading = False
166 | t1.join()
167 | sys.exit(0)
168 |
169 | # Non-interactive mode, send command from commmand line arguments
170 | commandstring = ' '.join(args.commands) + "\n"
171 | debug("Send command: " + commandstring.strip())
172 |
173 | # Send commands
174 | ser.write(commandstring.encode('utf-8'))
175 |
176 | debug("Read response (reading for " + str(args.timeout) + " seconds):")
177 | # Read response
178 | answer = read_response()
179 |
180 | # And print any responses (if not -q / -quiet)
181 | if not args.quiet:
182 | if answer:
183 | print(answer)
184 | else:
185 | debug("No response")
186 | else:
187 | debug("Quiet mode, do not print answer")
188 |
189 | ser.close()
190 |
--------------------------------------------------------------------------------
/MacroKeyPad/MacroKeyPad.ino:
--------------------------------------------------------------------------------
1 | #include "Keyboard.h"
2 | #include
3 | #include
4 |
5 | // KeyPad, 8 keys, with (single color) leds in them.
6 | //
7 | // Arduino Leonardo Compatible - ATMEGA 32U4-AU/MU - Pro micro.
8 | // USB Device HID/Mouse/Keyboard/etc...
9 | // Board info: https://learn.sparkfun.com/tutorials/pro-micro--fio-v3-hookup-guide/all
10 | //
11 | // Arduino IDE board manager url: https://raw.githubusercontent.com/sparkfun/Arduino_Boards/master/IDE_Board_Manager/package_sparkfun_index.json
12 | // Choose board "SparkFun Pro Micro", CPU 16 MHZ, 5V.
13 | //
14 | // Linux/Ubuntu, show which key has been pressed: "sudo showkey -s" or "sudo showkey -k"
15 | // Note: The keypad will also work on windows, it is just simulating an USB keyboard.
16 | //
17 | // Thijs Kaper, January 29, 2022.
18 |
19 | // Enable next line if you want double click on key 8 to toggle between keymap 1 and 2, and key 7 to show startup animation (was only for testing).
20 | //#define DOUBLE_CLICK_TEST
21 |
22 | // Just some random version number (can be queried via serial command interface).
23 | const char VERSION[] PROGMEM = "MacroPad-1.0, " __DATE__ " " __TIME__;
24 |
25 | // Key matrix size setup
26 | const byte ROWS = 2;
27 | const byte COLS = 4;
28 | const byte KEYCOUNT = (ROWS * COLS);
29 |
30 | // Key matrix pin connections:
31 | const byte rowPins[ROWS] = { 10, 16 }; //connect to the row pinouts of the keypad.
32 | const byte colPins[COLS] = { A1, A0, 15, 14 }; //connect to the column pinouts of the keypad.
33 |
34 | // Led to pin mapping
35 | const byte ledPin[KEYCOUNT] = { 2, 3, 4, 5, 6, 7, 8, 9 }; // Led nr is index (0 based), value is controller pin number.
36 |
37 | // Assign indexes to keymap, we use the indexes as pointer into keyMap1 or keyMap2 (after subtracting 1).
38 | // The index starts at 1, because reading the keypad will return 0 if there was none, so we can not use 0 as index.
39 | // Normally you would just put the "real" keys in this array, but I wanted to be able to choose between 2 keymaps,
40 | // so I did just put in index in here, to point into my two keymaps.
41 | const char keys[ROWS][COLS] = {
42 | { 1, 2, 3, 4 },
43 | { 5, 6, 7, 8 },
44 | };
45 |
46 | // The data in this configuration struct can be modified at runtime via serial port, and persisted in EEPROM for use on fresh boot.
47 | struct ConfigData {
48 | // keyMap1 defintion, the one used by default after power on. You can fill in special keynames
49 | // as seen in arduino-*/libraries/Keyboard/src/Keyboard.h, or just use ascii code chars, e.g. '0' or 'a'.
50 | // My defintion, F13-F22. Skipping two f-keys, as these do not seem to work on linux mint? (F19/F20 moved to F21/F22).
51 | char keyMap1[KEYCOUNT] = { KEY_F13, KEY_F14, KEY_F15, KEY_F16, KEY_F17, KEY_F18, KEY_F21, KEY_F22 };
52 |
53 | // Alternate keymap, just for testing the keys, sending normal digits. Same comment as for keyMap1.
54 | char keyMap2[KEYCOUNT] = { '1', '2', '3', '4', '5', '6', '7', '8' };
55 |
56 | // Configuration of led action when key is pressed. Initial start, set to auto toggle (t).
57 | // Options: t = toggle, e = enable(on), d = disable(off), f = flash 150 ms.
58 | char ledConfig1[KEYCOUNT] = { 't', 't', 't', 't', 't', 't', 't', 't' };
59 | char ledConfig2[KEYCOUNT] = { 't', 't', 't', 't', 't', 't', 't', 't' };
60 | } config;
61 |
62 | // Startup animation, led numbers to light, 1 based to easily match up keys (not zero based).
63 | // Animation lights up 2 leds at a time.
64 | const byte ledAnimation[][2] = {
65 | { 1, 8 },
66 | { 2, 7 },
67 | { 3, 6 },
68 | { 4, 5 },
69 | { 8, 1 },
70 | { 7, 2 },
71 | { 6, 3 },
72 | { 5, 4 },
73 | { 1, 8 },
74 | };
75 |
76 | // Toggle flag to change keymap. Default starts using keyMap1. You can toggle to keyMap2 using double click on key 8 (if DOUBLE_CLICK_TEST defined), or by sending a command from PC via serial.
77 | // Note: the second keymap was not meant for any serious use (just testing), as double clicking key 8 will also send the configured key function on its first press.
78 | boolean useAlternateKeyMap=false;
79 |
80 | // Initialze keypad functions.
81 | Keypad keypad = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS );
82 |
83 | // Led state memory.
84 | byte ledState[KEYCOUNT]; // on = 1 / off = 0
85 |
86 | // Helper function to determine length of array.
87 | #define membersof(x) (sizeof(x) / sizeof(x[0]))
88 |
89 | // memorize when last keypress occurred.
90 | unsigned long startTime = millis();
91 |
92 | // memorize last key press.
93 | char lastKey = '-';
94 |
95 | // Serial data buffer.
96 | char readBuffer[80];
97 | char *readBufferPtr;
98 |
99 | // Reset serial data buffer.
100 | void clearReadBuffer() {
101 | readBuffer[0] = 0;
102 | readBufferPtr = readBuffer;
103 | }
104 |
105 | // Initialize all.
106 | void setup() {
107 | Serial.begin(115200);
108 |
109 | // Load config data from EEPROM.
110 | handleLoad();
111 |
112 | // Initialize readBuffer to blank/empty-string.
113 | clearReadBuffer();
114 |
115 | // Set LED pins as output, and turn off.
116 | for (int i=0;i0) {
142 | // Turn previous leds off
143 | setLed(ledAnimation[i-1][0]-1, 0);
144 | setLed(ledAnimation[i-1][1]-1, 0);
145 | }
146 | delay(250);
147 | }
148 | // Turn last ones off.
149 | setLed(ledAnimation[membersof(ledAnimation)-1][0]-1, 0);
150 | setLed(ledAnimation[membersof(ledAnimation)-1][1]-1, 0);
151 |
152 | // Restore led state, in case we ran animation during use.
153 | for(int i=0;i=KEYCOUNT || onOff<0 || onOff>1) return;
161 | ledState[nr] = onOff;
162 | digitalWrite(ledPin[nr], (onOff==1)?LOW:HIGH);
163 | }
164 |
165 | // Toggle led (zero based index).
166 | void toggleLed(int nr) {
167 | if (nr<0 || nr>=KEYCOUNT) return;
168 | if (ledState[nr] == 0) {
169 | setLed(nr, 1);
170 | } else {
171 | setLed(nr, 0);
172 | }
173 | }
174 |
175 | // Change led state, on key-press, according to what has been confgiured for it.
176 | void executeLedConfig(int nr) {
177 | if (nr<0 || nr>=KEYCOUNT) return;
178 | char setting;
179 | if (useAlternateKeyMap) {
180 | setting = config.ledConfig2[nr];
181 | } else {
182 | setting = config.ledConfig1[nr];
183 | }
184 | // Options: t = toggle, e = enable(on), d = disable(off), f = flash 150 ms.
185 | if (setting == 't') {
186 | toggleLed(nr);
187 | }
188 | if (setting == 'e') {
189 | setLed(nr, 1);
190 | }
191 | if (setting == 'd') {
192 | setLed(nr, 0);
193 | }
194 | if (setting == 'f') {
195 | setLed(nr, 1);
196 | delay(150);
197 | setLed(nr, 0);
198 | delay(150);
199 | }
200 | }
201 |
202 | // Read number from the serial data buffer.
203 | int parseNumber(int minValue, int maxValue) {
204 | // Parse next word into a (zero or positive) number.
205 | char *nextword = strtok(NULL, " ");
206 | if (nextword == NULL) {
207 | Serial.print("Error, missing number\n");
208 | return -1;
209 | }
210 | int nr = atoi(nextword);
211 | if (nrmaxValue) {
212 | Serial.print("Error, expected number from ");
213 | Serial.print(minValue);
214 | Serial.print(" to ");
215 | Serial.println(maxValue);
216 | return -1;
217 | }
218 | return nr;
219 | }
220 |
221 | // Create function "template".
222 | typedef int (*HandlerFunction)();
223 |
224 | // Create struct with function, name, description.
225 | struct HandlerEntry {
226 | char *shortcode;
227 | char *command;
228 | char *help;
229 | HandlerFunction handler;
230 | };
231 |
232 | // Stored all long text messages in PROGMEM, this does require them to be copied back using strncpy_P to use them.
233 | const char MSG_HELP_START[] PROGMEM = "Valid commands:\n";
234 | const char MSG_HELP[] PROGMEM = "help : [or ?] show this help";
235 | const char MSG_VERSION[] PROGMEM = "version : [or v] show the macropad version";
236 | const char MSG_GETLED[] PROGMEM = "getled : [or g] get led state (return 0 for off or 1 for on)";
237 | const char MSG_TOGGLE[] PROGMEM = "toggle : [or t] toggle led state ( is 1..8)";
238 | const char MSG_ON[] PROGMEM = "on : [or e] turn led on/enable ( is 1..8)";
239 | const char MSG_OFF[] PROGMEM = "off : [or d] turn led off/disable ( is 1..8)";
240 | const char MSG_FLASH[] PROGMEM = "flash : [or f] turn led on, sleep milliseconds, turn off, and sleep same time again ( is 1..8, is 10..500)";
241 | const char MSG_ANIMATION[] PROGMEM = "animation : [or a] run startup led animation";
242 | const char MSG_USEMAP[] PROGMEM = "usemap : [or u] enable keymap ( is 1 or 2)";
243 | const char MSG_CONFIGTOGGLE[] PROGMEM = "configtoggle : [or c] configure key led toggle for key (1..8), is one of [t,e,d,f] where flash will be 150ms";
244 | const char MSG_SHOWSETTINGS[] PROGMEM = "showsettings : [or s] show settings";
245 | const char MSG_SETKEYCODE[] PROGMEM = "setkeycode : [or k] set key code of key (1..8) to 0..255";
246 | const char MSG_SETKEYCHAR[] PROGMEM = "setkeychar : [or h] set key character of key (1..8) to (a single char)";
247 | const char MSG_PERSIST[] PROGMEM = "persist : [or p] write config to EEPROM (use if you change keys or configtoggles)";
248 | const char MSG_LOAD[] PROGMEM = "load : [or l] load config from EEPROM (automatically done on each startup)";
249 | const char MSG_HELP_END[] PROGMEM = "\nYou can put multiple commands in one line - up to 79 characters (space separated), example: 'on 1 toggle 7 off 3'\n";
250 |
251 | // List of all possible commands.
252 | const HandlerEntry handlers[] = {
253 | { "?", "help", MSG_HELP, &handleHelp },
254 | { "v", "version", MSG_VERSION, &handleVersion },
255 | { "g", "getled", MSG_GETLED, &handleGetLed },
256 | { "t", "toggle", MSG_TOGGLE, &handleToggle },
257 | { "e", "on", MSG_ON, &handleOn },
258 | { "d", "off", MSG_OFF, &handleOff },
259 | { "f", "flash", MSG_FLASH, &handleFlash },
260 | { "a", "animation", MSG_ANIMATION, &handleAnimation },
261 | { "u", "usemap", MSG_USEMAP, &handleActivateKeyMap },
262 | { "c", "configtoggle", MSG_CONFIGTOGGLE, &handleConfigToggle },
263 | { "s", "showsettings", MSG_SHOWSETTINGS, &handleShowSettings },
264 | { "k", "setkeycode", MSG_SETKEYCODE, &handleSetKeyCode },
265 | { "h", "setkeychar", MSG_SETKEYCHAR, &handleSetKeyChar },
266 | { "p", "persist", MSG_PERSIST, &handlePersist },
267 | { "l", "load", MSG_LOAD, &handleLoad },
268 | };
269 |
270 | // Print char* from PROGMEM (copy to normal memory, and then print)
271 | void println_P(char *data_P) {
272 | // Would be nice if we could determine length of string in progmem, and define buffer of that size.
273 | // Shortcut for now, just set 200 as max, and fix when needed.
274 | char tmp[200];
275 | strncpy_P(tmp, data_P, 199);
276 | Serial.println(tmp);
277 | }
278 |
279 | // Help command, show list.
280 | int handleHelp() {
281 | println_P(MSG_HELP_START);
282 | for(int i=0; i0) {
471 | char ser = Serial.read();
472 | // Collect received data in readBuffer, until someone sends "enter" (cr and or lf).
473 | if (strlen(readBuffer) < (sizeof(readBuffer)-1) && ser != 13 && ser !=10) {
474 | // Add new char to buffer.
475 | *(readBufferPtr++) = ser;
476 | *readBufferPtr = 0;
477 | }
478 | // "enter" (cr and or lf) pressed? handle data (if any).
479 | if ((ser == 13 || ser == 10) && strlen(readBuffer) > 0) {
480 | executeSerialCommands();
481 | clearReadBuffer();
482 | }
483 | }
484 | }
485 |
486 | // main loop
487 | void loop() {
488 | handleSerialReceive();
489 |
490 | // key returns our 1 based index 1..8, or 0 if no key was pressed.
491 | char key = keypad.getKey();
492 |
493 | if (key){
494 | char keyIndex = key - 1;
495 |
496 | #ifdef DOUBLE_CLICK_TEST
497 | // Check if key'8' was pressed twice quickly, if so, toggle keymap.
498 | if (key == 8 && lastKey == 8 && ((millis()-startTime) < 600)) {
499 | // toggle keymap between keyMap1 and keyMap2.
500 | useAlternateKeyMap = !useAlternateKeyMap;
501 | toggleLed(useAlternateKeyMap?0:4); delay(250); toggleLed(useAlternateKeyMap?0:4); // flash led 1 or 5 to confirm toggle (1 = numeric keys, 5 = function keys)
502 | delay(250);
503 | toggleLed(useAlternateKeyMap?0:4); delay(250); toggleLed(useAlternateKeyMap?0:4); // flash led 1 or 5 to confirm toggle (1 = numeric keys, 5 = function keys)
504 | startTime = millis();
505 | delay(20);
506 | return;
507 | }
508 |
509 | // Check if key'7' was pressed twice quickly, if so show startup led animation.
510 | if (key == 7 && lastKey == 7 && ((millis()-startTime) < 600)) {
511 | startupLedAnimation();
512 | startTime = millis();
513 | delay(20);
514 | return;
515 | }
516 | #endif
517 |
518 | // Send key press to PC via USB
519 | Keyboard.write(useAlternateKeyMap ? config.keyMap2[keyIndex] : config.keyMap1[keyIndex]);
520 | lastKey = key;
521 | executeLedConfig(keyIndex);
522 | startTime = millis();
523 | }
524 | delay(20);
525 | }
526 |
--------------------------------------------------------------------------------