├── 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 | Lights On 10 | 11 | End Result 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 | --------------------------------------------------------------------------------