├── images ├── mute-button-wiring.png └── macos-keyboard-setup-assistant.png ├── html_color_names.h ├── README.md ├── LICENSE.md └── trinket-m0-physical-mute-button-for-video-chat.ino /images/mute-button-wiring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jimstump/trinket-m0-physical-mute-button-for-video-chat/HEAD/images/mute-button-wiring.png -------------------------------------------------------------------------------- /images/macos-keyboard-setup-assistant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jimstump/trinket-m0-physical-mute-button-for-video-chat/HEAD/images/macos-keyboard-setup-assistant.png -------------------------------------------------------------------------------- /html_color_names.h: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a list of the HTML named colors as #define statements 3 | * 4 | * Color names/values were taken from https://htmlcolorcodes.com/color-names/ 5 | */ 6 | 7 | // RED HTML COLOR NAMES 8 | #define COLOR_INDIANRED 0xCD5C5C 9 | #define COLOR_LIGHTCORAL 0xF08080 10 | #define COLOR_SALMON 0xFA8072 11 | #define COLOR_DARKSALMON 0xE9967A 12 | #define COLOR_LIGHTSALMON 0xFFA07A 13 | #define COLOR_CRIMSON 0xDC143C 14 | #define COLOR_RED 0xFF0000 15 | #define COLOR_FIREBRICK 0xB22222 16 | #define COLOR_DARKRED 0x8B0000 17 | 18 | // PINK HTML COLOR NAMES 19 | #define COLOR_PINK 0xFFC0CB 20 | #define COLOR_LIGHTPINK 0xFFB6C1 21 | #define COLOR_HOTPINK 0xFF69B4 22 | #define COLOR_DEEPPINK 0xFF1493 23 | #define COLOR_MEDIUMVIOLETRED 0xC71585 24 | #define COLOR_PALEVIOLETRED 0xDB7093 25 | 26 | // ORANGE HTML COLOR NAMES 27 | #define COLOR_LIGHTSALMON 0xFFA07A 28 | #define COLOR_CORAL 0xFF7F50 29 | #define COLOR_TOMATO 0xFF6347 30 | #define COLOR_ORANGERED 0xFF4500 31 | #define COLOR_DARKORANGE 0xFF8C00 32 | #define COLOR_ORANGE 0xFFA500 33 | 34 | // YELLOW HTML COLOR NAMES 35 | #define COLOR_GOLD 0xFFD700 36 | #define COLOR_YELLOW 0xFFFF00 37 | #define COLOR_LIGHTYELLOW 0xFFFFE0 38 | #define COLOR_LEMONCHIFFON 0xFFFACD 39 | #define COLOR_LIGHTGOLDENRODYELLOW 0xFAFAD2 40 | #define COLOR_PAPAYAWHIP 0xFFEFD5 41 | #define COLOR_MOCCASIN 0xFFE4B5 42 | #define COLOR_PEACHPUFF 0xFFDAB9 43 | #define COLOR_PALEGOLDENROD 0xEEE8AA 44 | #define COLOR_KHAKI 0xF0E68C 45 | #define COLOR_DARKKHAKI 0xBDB76B 46 | 47 | // PURPLE HTML COLOR NAMES 48 | #define COLOR_LAVENDER 0xE6E6FA 49 | #define COLOR_THISTLE 0xD8BFD8 50 | #define COLOR_PLUM 0xDDA0DD 51 | #define COLOR_VIOLET 0xEE82EE 52 | #define COLOR_ORCHID 0xDA70D6 53 | #define COLOR_FUCHSIA 0xFF00FF 54 | #define COLOR_MAGENTA 0xFF00FF 55 | #define COLOR_MEDIUMORCHID 0xBA55D3 56 | #define COLOR_MEDIUMPURPLE 0x9370DB 57 | #define COLOR_REBECCAPURPLE 0x663399 58 | #define COLOR_BLUEVIOLET 0x8A2BE2 59 | #define COLOR_DARKVIOLET 0x9400D3 60 | #define COLOR_DARKORCHID 0x9932CC 61 | #define COLOR_DARKMAGENTA 0x8B008B 62 | #define COLOR_PURPLE 0x800080 63 | #define COLOR_INDIGO 0x4B0082 64 | #define COLOR_SLATEBLUE 0x6A5ACD 65 | #define COLOR_DARKSLATEBLUE 0x483D8B 66 | #define COLOR_MEDIUMSLATEBLUE 0x7B68EE 67 | 68 | // GREEN HTML COLOR NAMES 69 | #define COLOR_GREENYELLOW 0xADFF2F 70 | #define COLOR_CHARTREUSE 0x7FFF00 71 | #define COLOR_LAWNGREEN 0x7CFC00 72 | #define COLOR_LIME 0x00FF00 73 | #define COLOR_LIMEGREEN 0x32CD32 74 | #define COLOR_PALEGREEN 0x98FB98 75 | #define COLOR_LIGHTGREEN 0x90EE90 76 | #define COLOR_MEDIUMSPRINGGREEN 0x00FA9A 77 | #define COLOR_SPRINGGREEN 0x00FF7F 78 | #define COLOR_MEDIUMSEAGREEN 0x3CB371 79 | #define COLOR_SEAGREEN 0x2E8B57 80 | #define COLOR_FORESTGREEN 0x228B22 81 | #define COLOR_GREEN 0x008000 82 | #define COLOR_DARKGREEN 0x006400 83 | #define COLOR_YELLOWGREEN 0x9ACD32 84 | #define COLOR_OLIVEDRAB 0x6B8E23 85 | #define COLOR_OLIVE 0x808000 86 | #define COLOR_DARKOLIVEGREEN 0x556B2F 87 | #define COLOR_MEDIUMAQUAMARINE 0x66CDAA 88 | #define COLOR_DARKSEAGREEN 0x8FBC8B 89 | #define COLOR_LIGHTSEAGREEN 0x20B2AA 90 | #define COLOR_DARKCYAN 0x008B8B 91 | #define COLOR_TEAL 0x008080 92 | 93 | // BLUE HTML COLOR NAMES 94 | #define COLOR_AQUA 0x00FFFF 95 | #define COLOR_CYAN 0x00FFFF 96 | #define COLOR_LIGHTCYAN 0xE0FFFF 97 | #define COLOR_PALETURQUOISE 0xAFEEEE 98 | #define COLOR_AQUAMARINE 0x7FFFD4 99 | #define COLOR_TURQUOISE 0x40E0D0 100 | #define COLOR_MEDIUMTURQUOISE 0x48D1CC 101 | #define COLOR_DARKTURQUOISE 0x00CED1 102 | #define COLOR_CADETBLUE 0x5F9EA0 103 | #define COLOR_STEELBLUE 0x4682B4 104 | #define COLOR_LIGHTSTEELBLUE 0xB0C4DE 105 | #define COLOR_POWDERBLUE 0xB0E0E6 106 | #define COLOR_LIGHTBLUE 0xADD8E6 107 | #define COLOR_SKYBLUE 0x87CEEB 108 | #define COLOR_LIGHTSKYBLUE 0x87CEFA 109 | #define COLOR_DEEPSKYBLUE 0x00BFFF 110 | #define COLOR_DODGERBLUE 0x1E90FF 111 | #define COLOR_CORNFLOWERBLUE 0x6495ED 112 | #define COLOR_MEDIUMSLATEBLUE 0x7B68EE 113 | #define COLOR_ROYALBLUE 0x4169E1 114 | #define COLOR_BLUE 0x0000FF 115 | #define COLOR_MEDIUMBLUE 0x0000CD 116 | #define COLOR_DARKBLUE 0x00008B 117 | #define COLOR_NAVY 0x000080 118 | #define COLOR_MIDNIGHTBLUE 0x191970 119 | 120 | // BROWN HTML COLOR NAMES 121 | #define COLOR_CORNSILK 0xFFF8DC 122 | #define COLOR_BLANCHEDALMOND 0xFFEBCD 123 | #define COLOR_BISQUE 0xFFE4C4 124 | #define COLOR_NAVAJOWHITE 0xFFDEAD 125 | #define COLOR_WHEAT 0xF5DEB3 126 | #define COLOR_BURLYWOOD 0xDEB887 127 | #define COLOR_TAN 0xD2B48C 128 | #define COLOR_ROSYBROWN 0xBC8F8F 129 | #define COLOR_SANDYBROWN 0xF4A460 130 | #define COLOR_GOLDENROD 0xDAA520 131 | #define COLOR_DARKGOLDENROD 0xB8860B 132 | #define COLOR_PERU 0xCD853F 133 | #define COLOR_CHOCOLATE 0xD2691E 134 | #define COLOR_SADDLEBROWN 0x8B4513 135 | #define COLOR_SIENNA 0xA0522D 136 | #define COLOR_BROWN 0xA52A2A 137 | #define COLOR_MAROON 0x800000 138 | 139 | // WHITE HTML COLOR NAMES 140 | #define COLOR_WHITE 0xFFFFFF 141 | #define COLOR_SNOW 0xFFFAFA 142 | #define COLOR_HONEYDEW 0xF0FFF0 143 | #define COLOR_MINTCREAM 0xF5FFFA 144 | #define COLOR_AZURE 0xF0FFFF 145 | #define COLOR_ALICEBLUE 0xF0F8FF 146 | #define COLOR_GHOSTWHITE 0xF8F8FF 147 | #define COLOR_WHITESMOKE 0xF5F5F5 148 | #define COLOR_SEASHELL 0xFFF5EE 149 | #define COLOR_BEIGE 0xF5F5DC 150 | #define COLOR_OLDLACE 0xFDF5E6 151 | #define COLOR_FLORALWHITE 0xFFFAF0 152 | #define COLOR_IVORY 0xFFFFF0 153 | #define COLOR_ANTIQUEWHITE 0xFAEBD7 154 | #define COLOR_LINEN 0xFAF0E6 155 | #define COLOR_LAVENDERBLUSH 0xFFF0F5 156 | #define COLOR_MISTYROSE 0xFFE4E1 157 | 158 | // GRAY HTML COLOR NAMES 159 | #define COLOR_GAINSBORO 0xDCDCDC 160 | #define COLOR_LIGHTGRAY 0xD3D3D3 161 | #define COLOR_SILVER 0xC0C0C0 162 | #define COLOR_DARKGRAY 0xA9A9A9 163 | #define COLOR_GRAY 0x808080 164 | #define COLOR_DIMGRAY 0x696969 165 | #define COLOR_LIGHTSLATEGRAY 0x778899 166 | #define COLOR_SLATEGRAY 0x708090 167 | #define COLOR_DARKSLATEGRAY 0x2F4F4F 168 | #define COLOR_BLACK 0x000000 169 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Physical Mute Button for Video Chat 2 | 3 | Build a physical mute button (for your microphone and camera) for video chat 4 | applications like Zoom, Google Meet, Microsoft Teams and WebEx using an 5 | [Adafruit Trinket M0](https://www.adafruit.com/product/3500). 6 | 7 | ## Acknowledgements 8 | 9 | Thanks to Elliotmade for the inspiration on this project, 10 | [Physical Mute Button for Zoom Meetings](https://elliotmade.com/2020/04/23/physical-mute-button-for-zoom-meetings/). 11 | 12 | Refactored by [HaxNobody](https://gist.github.com/HaxNobody/7bde369d7a41348b8b91c1a4f358ea4a/1c4d891c09977bd7866d5bf634bce52e46ae9e10) 13 | to extend functionality and add super helpful comments. 14 | 15 | Additionally modified and extended by [Jim Stump](https://github.com/jimstump/trinket-m0-physical-mute-button-for-video-chat) 16 | to add MacOS support as well as additional video chat programs. Also moved to 17 | the [Adafruit Trinket M0](https://www.adafruit.com/product/3500) board as 18 | features outgrew the Digispark ATtiny85's capabilities. 19 | 20 | ## How Does This Work 21 | 22 | This program will send USB HID keyboard presses to activate microphone and 23 | video functions using the application's default keyboard shortcuts. 24 | * A momentary press on button 1 will toggle microphone mute on or off. 25 | * Press and hold button 1 to activate PTT (Push-to-Talk) functionality for 26 | supported applications (Zoom-only). All other applications will perform the 27 | mute toggle action. 28 | * A momentary press on button 2 will toggle video on or off for supported 29 | applications (not WebEx). 30 | 31 | The following video chat programs are supported for selection: 32 | * Zoom 33 | * Google Meet 34 | * Microsoft Teams 35 | * Cisco WebEx 36 | 37 | The video chat program can be chosen using a potentiometer dial attached to 38 | the `PIN_ANALOG_IN` pin. On startup and when the buttons are pressed, the 39 | analog value from the potentiometer is read to determine which video chat 40 | application and platform keyboard shortcuts should be used. Once determined, 41 | the color of the onboard DotStar pixel on the Trinket M0 will be changed to 42 | indicate the current selection (Zoom is blue, Google Meet is Lime, Microsoft 43 | Teams is dark violet, and WebEx is gold). If you don't plan on using a 44 | potentiometer for this, then comment out the `PIN_ANALOG_IN` definition at the 45 | top of the program and the PC keyboard shortcuts for Zoom will be used (this 46 | can be configured at the bottom of the `setApplicationSettings` function - 47 | I didn't get a chance to make that more easily configurable yet). 48 | 49 | ## MacOS Keyboard Setup Assistant 50 | 51 | ![MacOS Keyboard Setup Assistant Window](images/macos-keyboard-setup-assistant.png) 52 | 53 | As mentioned in the "How Does This Work" section, this mute button 54 | device/program identifies itself as a keyboard to the connecting computer so 55 | that it can send the appropriate keyboard shortcuts when the buttons are 56 | pressed. In MacOS, the first time a non-Apple keyboard is connected the 57 | Keyboard Setup Assistant window displays. This application asks you to press 58 | certain keys on the keyboard in an attempt to identify and configure the 59 | correct keyboard layout. 60 | 61 | While a function (`runMacKeyboardSetup`) exists to press the correct keys to 62 | help identify the keyboard as an "ANSI (United States and others)" standard 63 | 101-key or 102-key keyboard, it is not currently called. I originally had it 64 | configured on a double click of button 1 but took it out because I didn't like 65 | the delay that was then required to correctly detect a single click of the 66 | button for mute toggling. 67 | 68 | So, how do you get past the Keyboard Setup Assistant? Click the "Continue" 69 | button on the Introduction screen of the wizard until it displays the 70 | "Identifying Your Keyboard" screen where it asks you to press a specific key on 71 | the keyboard. At that point, press one of the mute buttons a couple times 72 | (in my testing 3 times was required) until the wizard shows a popup saying that 73 | the keyboard couldn't be identified. Click the "OK" button to dismiss the 74 | popup and the wizard will then ask you to select the correct keyboard type. 75 | Choose the "ANSI (United States and others)" option and then click the "Done" 76 | button to exit the wizard. At this point, the device should be correctly 77 | configured and you should not be prompted to go through the setup again on that 78 | computer (at least for the current user). 79 | 80 | ## Current Hardware 81 | 82 | While there are plenty of parts that can be used to build this project, I 83 | thought it might be helpful to list out what I'm currently using in my 84 | prototype. 85 | 86 | * [Adafruit Trinket M0](https://www.adafruit.com/product/3500) 87 | * [24mm Arcade Button with Built-in LED](https://www.amazon.com/dp/B06Y29LBJ4) 88 | (I used 2) 89 | * [10K Linear Potentiometer](https://www.adafruit.com/product/562) 90 | * [Potentiometer Knob](https://www.adafruit.com/product/2047) 91 | * [Arcade Button Quick-Connect Wires](https://www.adafruit.com/product/1152) 92 | (I used 4 pairs) 93 | * Female/Female (F/F) jumper cables (I used 3 cables) 94 | * [Panel Mount Micro B Male to Micro B Female USB Connector](https://www.adafruit.com/product/3258) 95 | * [Polycase TS-2420P 4.00 x 2.13 x 2.00 inch Enclosure](https://www.polycase.com/ts-2420p) 96 | * The appropriate micro USB cable to connect to your computer(s). 97 | * [6x2mm Rubber Feet](https://www.amazon.com/dp/B06XCNM69B) for the bottom of the enclosure. 98 | * [1/2 Inch Diameter Round Labels, Glossy Clear Film](https://www.avery.com/blank/labels/94503) for the button graphics. 99 | * [1 5/8 Inch Diameter Round Label, Matte White](https://www.avery.com/blank/labels/94507) for the Application Selector/Potentiometer label. 100 | 101 | ## Wiring Help 102 | 103 | The pins that are used for different features are configurable using the 104 | `PIN_*` definitions near the top of the program code 105 | (`trinket-m0-physical-mute-button-for-video-chat.ino`). If you don't plan on 106 | using something (like the potentiometer to choose a chat application or the 107 | LEDs or whatever) simply comment out the `#define` statement by adding a 108 | comment (`//`) at the beginning of the line and the associated code won't 109 | be executed. 110 | 111 | However, if you are using the same hardware as above, it may be easiest to just 112 | wire it the same way as I did. Since this was my first Arduino project, I 113 | found it helpful when others were specific about how they wired everything 114 | together, so I'll do the same. First, I soldered the header pins to the board 115 | so that they were on "top". Then I wired everything up as described below. 116 | If you want things to be more permanent you could solder your wires directly 117 | to the board and the components' pins, but be sure to install the components 118 | in your enclosure first (specifically the buttons). 119 | 120 | ![Wiring Diagram](images/mute-button-wiring.png) 121 | 122 | ### Ground (GND) 123 | 124 | Since everything here connects to the ground (GND) pin on the board, I took 125 | one of the F/F jumper cables and cut it in half. I then stripped both cut 126 | ends. One half of this cable will attach to the GND pin and the other half 127 | will connect to the potentiometer later. The wires connected to the GND pins 128 | on the buttons will have the white plastic connector cut off and will get 129 | their ends stripped as well. The bare ends of all of these GND connections 130 | will then be connected together with a wire nut. 131 | 132 | ### Button 1 (Microphone Mute) 133 | 134 | I wired the one side of the button 1 switch (microphone mute) to the GND 135 | terminal and the other side to PIN 0 (configured with `PIN_BUTTON_1`). 136 | With switches, it doesn't matter which side of the switch is used for 137 | which connection. 138 | 139 | Since my buttons have built-in LEDs that I also wanted to control, I also 140 | wired the `+` terminal to PIN 1 (configured with `PIN_BUTTON_1_LED`) and 141 | the `-` terminal to GND. 142 | 143 | To make things easy, I used one of the jumper quick connect pairs to wire one 144 | side of the switch and the `+` terminal of the LED to the GPIO pins using the 145 | 2 wire connector and I used another pair (with the connector cut off) to go to 146 | GND as described above. 147 | 148 | ### Button 2 (Video Toggle) 149 | 150 | Button 2 is wired pretty much the same as button 1. This time around, the 151 | switch is wired to GND and PIN 3 (configured with `PIN_BUTTON_2`) and the LED 152 | is wired to GND and PIN 4 (configured with `PIN_BUTTON_2_LED`). 153 | 154 | ### Potentiometer (Application Selector) 155 | 156 | In my house, we currently use Zoom, Google Meet, Microsoft Teams and WebEx to 157 | various degrees, and of course each of these programs uses a different keyboard 158 | shortcut. I didn't want to continuously reprogram the board every time we 159 | switched applications so I tried to come up with a way to switch programs at 160 | "run time". I thought about having a switch or tap sequence cycle through 161 | available options but thought that would be too complicated, especially for my 162 | children. I settled on a dial using a potentiometer. 163 | 164 | A potentiometer has three pins. The outer 2 pins get connected to the "power" 165 | pin (in this case 3V - if your board has multiple power output pins, make sure 166 | you use the one that matches the logic voltage for your GPIO pins) and GND. 167 | While, in theory, it doesn't matter which side is which, it does affect which 168 | side of the dial reads low (the ground side) and which reads high (the power 169 | side), and thus how your code/label is designed. For me, with the knob 170 | pointing towards me, I have the left pin connected to GND (using that half of 171 | a F/F jumper cable I mentioned in the Ground section) and the right pin 172 | connected to the 3V pin using another F/F jumper cable. This allows me to 173 | create a label with the values "Zoom", "Google Meet", "MS Teams", "WebEx", 174 | "Zoom (MacOS)", "Google Meet (MacOS)", "MS Teams (MacOS)" and "WebEx (MacOS)" 175 | clockwise around the potentiometer's 300 degree range. The center pin of the 176 | potentiometer is the "signal" pin and is wired to pin 2 (configured with 177 | `PIN_ANALOG_IN`). Somewhat confusingly, on the Trinket M0, the pin that is 178 | labeled 2 on the top of the board is digital input 2 and analog input A1. Make 179 | sure you check the documentation for whatever board you are using to ensure you 180 | set the value of `PIN_ANALOG_IN` correctly for use with analog input. 181 | 182 | ### The Enclosure 183 | 184 | So, I started out this project by making a "case" using cardboard and packing 185 | tape while prototyping/beta testing. That won't do for the long haul though. 186 | 187 | I chose a 4 inch by ~2 inch by 2 inch plastic enclosure from Polycase. I 188 | drilled 2 holes evenly spaced in the top of the enclosure to mount the arcade 189 | buttons for the microphone and video toggle. To do this, I centered my 1" 190 | drill bit 1 5/32" from the edge of the enclosure. For the potentiometer, I 191 | drilled a 5/16" hole in the center of the front of the enclosure and also made 192 | a small slit using a 1/16" bit for the alignment peg. Finally, I made a 193 | rectangular hole in the lower center of the back panel for the USB jack using a 194 | series of 1/16" drill holes, smoothed out with a utility knife and some 195 | sandpaper. 196 | 197 | To help drill the holes as precisely as possible, I made some 198 | patterns/templates using paper. I measured exactly where the holes' center 199 | needed to be and drew the shape of the USB jack. Then I taped the paper 200 | templates to the enclosure and drilled 1/16" pilot holes through the paper, 201 | exactly where they needed to be. I took the paper off to complete the larger 202 | holes, so that I could reuse the templates. 203 | 204 | Even though the screws on the enclosure were recessed, I could just barely feel 205 | the screwheads with my finger. Since I was afraid of scratching my desk or 206 | computer, I opted to add some tiny rubber feet to the bottom for some 207 | additional protection. 208 | 209 | ### Labels 210 | 211 | To complete the project, I printed Font Awesome 212 | [microphone](https://fontawesome.com/icons/microphone?style=solid) and 213 | [video](https://fontawesome.com/icons/video?style=solid) icons on 1/2" round 214 | clear labels for the arcade buttons. I used a 1 5/8" round white label for the 215 | application selector. 216 | 217 | ## Programming the Board 218 | 219 | Since this is Arduino code, the first thing you'll need to do is setup and 220 | configure the Arduino IDE on your computer. If you are on Windows, you will 221 | also need to install the correct drivers for your microcontroller board. If 222 | you are using the Trinket M0, you can get the 223 | [Windows drivers here](https://learn.adafruit.com/adafruit-trinket-m0-circuitpython-arduino/windows-driver-installation) 224 | and the [Arduino IDE setup steps here](https://learn.adafruit.com/adafruit-trinket-m0-circuitpython-arduino/arduino-ide-setup). 225 | 226 | Once the Arduino IDE is all setup, you'll need the code in this repository, so 227 | either clone the repository or download it as a zip file and extract it. Then 228 | it _should_ be as simple as opening the .ino file in the Arduino IDE, making 229 | sure the correct board type is selected, and pressing the "Upload" button to 230 | compile and install the program. Once it reboots you should be good to go! 231 | 232 | One thing to note, if you are using the Trinket M0, you might need to double 233 | click the reset button once the board is plugged into your computer to get it 234 | recognized by the Arduino IDE. If the onboard DotStar is red at that point, 235 | try a different USB cable. I ran into an issue where about half of my 236 | stockpile of micro USB cables were charging-only and didn't support data. 237 | Guess which one I grabbed first. I wasted a bit of time on that initially. 238 | 239 | ## TODO 240 | 241 | At some point in the future, I'd like to try to read the current microphone 242 | mute/video state of the chat application. However, this almost certainly 243 | requires switching to a standard serial connection and running custom software 244 | on the machine to figure out the state and communicate that to the 245 | microcontroller, similarly to [toelke/google-hangouts-muter](https://github.com/toelke/google-hangouts-muter). 246 | However that does limit the portability of this, so that future may never come. 247 | 248 | ## Development Resources 249 | 250 | OneButton Documentation: 251 | https://github.com/mathertel/OneButton 252 | 253 | Keyboard Codes: 254 | https://www.arduino.cc/reference/en/language/functions/usb/keyboard/keyboardmodifiers/ 255 | https://www.usb.org/sites/default/files/documents/hut1_12v2.pdf 256 | 257 | Adafruit Trinket M0 Pinout Documentation: 258 | https://learn.adafruit.com/adafruit-trinket-m0-circuitpython-arduino/pinouts 259 | 260 | Adafruit DotStar LED Documentation: 261 | https://learn.adafruit.com/adafruit-dotstar-leds/arduino-library-use 262 | https://github.com/adafruit/Adafruit_DotStar 263 | 264 | ## License 265 | 266 | Since this application traces its roots to [Elliotmade's Physical Mute Button for Zoom Meetings](https://elliotmade.com/2020/04/23/physical-mute-button-for-zoom-meetings/) 267 | project and that project was released under a [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-nc-sa/4.0/) 268 | this is also released under the same license. 269 | 270 | ![Creative Commons License](https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png) 271 | This work is licensed under a [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-nc-sa/4.0/). 272 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International 2 | 3 | Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. 4 | 5 | ### Using Creative Commons Public Licenses 6 | 7 | Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. 8 | 9 | * __Considerations for licensors:__ Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. [More considerations for licensors](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensors). 10 | 11 | * __Considerations for the public:__ By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. [More considerations for the public](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensees). 12 | 13 | ## Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License 14 | 15 | By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. 16 | 17 | ### Section 1 – Definitions. 18 | 19 | a. __Adapted Material__ means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. 20 | 21 | b. __Adapter's License__ means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. 22 | 23 | c. __BY-NC-SA Compatible License__ means a license listed at [creativecommons.org/compatiblelicenses](http://creativecommons.org/compatiblelicenses), approved by Creative Commons as essentially the equivalent of this Public License. 24 | 25 | d. __Copyright and Similar Rights__ means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. 26 | 27 | e. __Effective Technological Measures__ means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. 28 | 29 | f. __Exceptions and Limitations__ means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. 30 | 31 | g. __License Elements__ means the license attributes listed in the name of a Creative Commons Public License. The License Elements of this Public License are Attribution, NonCommercial, and ShareAlike. 32 | 33 | h. __Licensed Material__ means the artistic or literary work, database, or other material to which the Licensor applied this Public License. 34 | 35 | i. __Licensed Rights__ means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. 36 | 37 | j. __Licensor__ means the individual(s) or entity(ies) granting rights under this Public License. 38 | 39 | k. __NonCommercial__ means not primarily intended for or directed towards commercial advantage or monetary compensation. For purposes of this Public License, the exchange of the Licensed Material for other material subject to Copyright and Similar Rights by digital file-sharing or similar means is NonCommercial provided there is no payment of monetary compensation in connection with the exchange. 40 | 41 | l. __Share__ means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. 42 | 43 | m. __Sui Generis Database Rights__ means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. 44 | 45 | n. __You__ means the individual or entity exercising the Licensed Rights under this Public License. __Your__ has a corresponding meaning. 46 | 47 | ### Section 2 – Scope. 48 | 49 | a. ___License grant.___ 50 | 51 | 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: 52 | 53 | A. reproduce and Share the Licensed Material, in whole or in part, for NonCommercial purposes only; and 54 | 55 | B. produce, reproduce, and Share Adapted Material for NonCommercial purposes only. 56 | 57 | 2. __Exceptions and Limitations.__ For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. 58 | 59 | 3. __Term.__ The term of this Public License is specified in Section 6(a). 60 | 61 | 4. __Media and formats; technical modifications allowed.__ The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. 62 | 63 | 5. __Downstream recipients.__ 64 | 65 | A. __Offer from the Licensor – Licensed Material.__ Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. 66 | 67 | B. __Additional offer from the Licensor – Adapted Material.__ Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapter’s License You apply. 68 | 69 | C. __No downstream restrictions.__ You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. 70 | 71 | 6. __No endorsement.__ Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). 72 | 73 | b. ___Other rights.___ 74 | 75 | 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. 76 | 77 | 2. Patent and trademark rights are not licensed under this Public License. 78 | 79 | 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties, including when the Licensed Material is used other than for NonCommercial purposes. 80 | 81 | ### Section 3 – License Conditions. 82 | 83 | Your exercise of the Licensed Rights is expressly made subject to the following conditions. 84 | 85 | a. ___Attribution.___ 86 | 87 | 1. If You Share the Licensed Material (including in modified form), You must: 88 | 89 | A. retain the following if it is supplied by the Licensor with the Licensed Material: 90 | 91 | i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); 92 | 93 | ii. a copyright notice; 94 | 95 | iii. a notice that refers to this Public License; 96 | 97 | iv. a notice that refers to the disclaimer of warranties; 98 | 99 | v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; 100 | 101 | B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and 102 | 103 | C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. 104 | 105 | 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. 106 | 107 | 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. 108 | 109 | b. ___ShareAlike.___ 110 | 111 | In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply. 112 | 113 | 1. The Adapter’s License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-NC-SA Compatible License. 114 | 115 | 2. You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material. 116 | 117 | 3. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply. 118 | 119 | ### Section 4 – Sui Generis Database Rights. 120 | 121 | Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: 122 | 123 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database for NonCommercial purposes only; 124 | 125 | b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, including for purposes of Section 3(b); and 126 | 127 | c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. 128 | 129 | For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. 130 | 131 | ### Section 5 – Disclaimer of Warranties and Limitation of Liability. 132 | 133 | a. __Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.__ 134 | 135 | b. __To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.__ 136 | 137 | c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. 138 | 139 | ### Section 6 – Term and Termination. 140 | 141 | a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. 142 | 143 | b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: 144 | 145 | 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or 146 | 147 | 2. upon express reinstatement by the Licensor. 148 | 149 | For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. 150 | 151 | c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. 152 | 153 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. 154 | 155 | ### Section 7 – Other Terms and Conditions. 156 | 157 | a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. 158 | 159 | b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. 160 | 161 | ### Section 8 – Interpretation. 162 | 163 | a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. 164 | 165 | b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. 166 | 167 | c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. 168 | 169 | d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. 170 | 171 | > Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at [creativecommons.org/policies](http://creativecommons.org/policies), Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. 172 | > 173 | > Creative Commons may be contacted at creativecommons.org 174 | -------------------------------------------------------------------------------- /trinket-m0-physical-mute-button-for-video-chat.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * # Physical Mute Button for Video Chat 3 | * 4 | * Build a physical mute button (for your microphone and camera) for video chat 5 | * applications like Zoom, Google Meet, Microsoft Teams and WebEx using an 6 | * [Adafruit Trinket M0](https://www.adafruit.com/product/3500). 7 | * 8 | * See the [README](README.md) for more information. 9 | * 10 | * ## Acknowledgements 11 | * 12 | * Thanks to Elliotmade for the inspiration on this project, 13 | * [Physical Mute Button for Zoom Meetings](https://elliotmade.com/2020/04/23/physical-mute-button-for-zoom-meetings/). 14 | * 15 | * Refactored by [HaxNobody](https://gist.github.com/HaxNobody/7bde369d7a41348b8b91c1a4f358ea4a) 16 | * to extend functionality and add comments for my own understanding. 17 | * 18 | * Additionally modified and extended by [Jim Stump](https://github.com/jimstump/trinket-m0-physical-mute-button-for-video-chat) 19 | * to add MacOS support as well as additional video chat programs. Also moved to 20 | * the [Adafruit Trinket M0](https://www.adafruit.com/product/3500) board. 21 | * 22 | * ## License 23 | * 24 | * Since this application traces its roots to [Elliotmade's Physical Mute Button for Zoom Meetings](https://elliotmade.com/2020/04/23/physical-mute-button-for-zoom-meetings/) 25 | * project and that project was released under a [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-nc-sa/4.0/) 26 | * this is also released under the same license. 27 | * 28 | * ![Creative Commons License](https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png) 29 | * This work is licensed under a [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-nc-sa/4.0/). 30 | * 31 | **/ 32 | #include // Library to drive DotStar RGB LEDs 33 | #include // Library for sending keystrokes as an HID device over USB. 34 | #include "OneButton.h" // Library for button input functions 35 | #include "html_color_names.h" // Some color definitions using HTML color names/hex values 36 | 37 | // GPIO Pins 38 | #define PIN_LED LED_BUILTIN 39 | #define PIN_BUTTON_1 0 40 | #define PIN_BUTTON_1_LED 1 41 | #define PIN_BUTTON_2 3 42 | #define PIN_BUTTON_2_LED 4 43 | #define PIN_ANALOG_IN A1 // (D2 == A1 on the Trinket M0) 44 | #define PIN_DOTSTAR_DATA 7 45 | #define PIN_DOTSTAR_CLOCK 8 46 | 47 | // Application Constants 48 | #define APP_ZOOM 1 49 | #define APP_GOOGLE_MEET 2 50 | #define APP_MS_TEAMS 3 51 | #define APP_WEBEX 4 52 | 53 | /** 54 | * Should we use MacOS keyboard shortcuts or not. 55 | * 56 | * true - use MacOS shortcuts 57 | * false - use Windows/Chromebook 58 | */ 59 | bool useMacShortcuts; 60 | 61 | /** 62 | * Byte to hold one of the APP_* definitions to indicate which application we 63 | * should send keyboard shortcuts for 64 | */ 65 | byte currentApplication; 66 | 67 | /** 68 | * Output debug information to the Serial console or not 69 | */ 70 | bool debugMode = true; 71 | 72 | /** 73 | * Defines the OneButton instance for the Microphone button (button1) 74 | * 75 | * The button is connected to `PIN_BUTTON_1` and GND, which is active low 76 | * and uses the internal pull-up resistor. 77 | */ 78 | #ifdef PIN_BUTTON_1 79 | OneButton button1 = OneButton( 80 | PIN_BUTTON_1, // Pin Number 81 | true, // Button is active LOW 82 | true // Enable internal pull-up resistor 83 | ); 84 | #endif 85 | 86 | /** 87 | * Defines the OneButton instance for the Video button (button2) 88 | * 89 | * The button is connected to `PIN_BUTTON_2` and GND, which is active low 90 | * and uses the internal pull-up resistor. 91 | */ 92 | #ifdef PIN_BUTTON_2 93 | OneButton button2 = OneButton( 94 | PIN_BUTTON_2, // Pin Number 95 | true, // Input is active LOW 96 | true // Enable internal pull-up resistor 97 | ); 98 | #endif 99 | 100 | /** 101 | * Adafruit_DotStar instance for the onboard LED strip 102 | */ 103 | #define DOTSTAR_NUMPIXELS 1 104 | #if DOTSTAR_NUMPIXELS > 0 && defined(PIN_DOTSTAR_DATA) && defined(PIN_DOTSTAR_CLOCK) 105 | Adafruit_DotStar strip(DOTSTAR_NUMPIXELS, PIN_DOTSTAR_DATA, PIN_DOTSTAR_CLOCK, DOTSTAR_BGR); 106 | #endif 107 | 108 | 109 | /** 110 | * The setup routine. 111 | * 112 | * This runs once when you press reset 113 | */ 114 | void setup() { 115 | if (debugMode) { 116 | Serial.begin(9600); 117 | } 118 | 119 | #if DOTSTAR_NUMPIXELS > 0 && defined(PIN_DOTSTAR_DATA) && defined(PIN_DOTSTAR_CLOCK) 120 | // configure the strip before getting the application so we set the correct color 121 | strip.begin(); // Initialize pins for output 122 | #endif 123 | 124 | setApplicationSettings(); 125 | 126 | #ifdef PIN_BUTTON_1 127 | button1.attachClick(handleButton1Click); // Set up button 1 for microphone mute toggle function 128 | button1.attachLongPressStart(handleButton1LongPressStart); // Set up button 1 for microphone temporary unmute function 129 | button1.attachLongPressStop(handleButton1LongPressStop); // Set up button 1 for microphone temporary unmute release function 130 | button1.setPressTicks(300); // Reduce long-press delay for button 1 to make temporary unmute more responsive (default 1000) 131 | //button1.attachDoubleClick(handleButton1DoubleClick); // Set up button 1 double-click to complete MacOS Keyboard Setup routine 132 | #endif 133 | 134 | #ifdef PIN_BUTTON_2 135 | button2.attachClick(handleButton2Click); // Set up button 2 for video toggle function 136 | button2.attachLongPressStart(handleButton2Click); // bind long-press start to the click handler so it still does _something_ 137 | button2.setPressTicks(300); // Reduce long-press delay for button 2 to make it more responsive (default 1000) 138 | #endif 139 | 140 | Keyboard.begin(); 141 | Keyboard.releaseAll(); // Clear any current key presses that may be happening for some reason 142 | delay(50); // Delay before entering loop 143 | 144 | // initialize the digital pins as an output for the LEDs. 145 | // and turn it on to indicate that we are ready 146 | #ifdef PIN_LED 147 | pinMode(PIN_LED, OUTPUT); 148 | turnLedOn(); 149 | #endif 150 | #ifdef PIN_BUTTON_1_LED 151 | pinMode(PIN_BUTTON_1_LED, OUTPUT); 152 | turnButton1LedOn(); 153 | #endif 154 | #ifdef PIN_BUTTON_2_LED 155 | pinMode(PIN_BUTTON_2_LED, OUTPUT); 156 | turnButton2LedOn(); 157 | #endif 158 | } 159 | 160 | /** 161 | * The main loop routine 162 | * 163 | * Check status of buttons in a continuous loop 164 | */ 165 | void loop() { 166 | #ifdef PIN_BUTTON_1 167 | button1.tick(); 168 | #endif 169 | #ifdef PIN_BUTTON_2 170 | button2.tick(); 171 | #endif 172 | } 173 | 174 | /** 175 | * Output debug information, if enabled by setting debugMode equal to true 176 | */ 177 | void debug(const String &s) { 178 | if (debugMode) { 179 | //Keyboard.println(s); 180 | Serial.println(s); 181 | } 182 | } 183 | 184 | /** 185 | * Figure out what application we should send keyboard shortcuts for. 186 | * 187 | * Read the potentiometer value from PIN_ANALOG_IN and use that to determine 188 | * the current application setting 189 | */ 190 | void setApplicationSettings() { 191 | #ifdef PIN_ANALOG_IN 192 | // values range from 0 to 1023 193 | // we have 8 application options 194 | // switch is labeled so 195 | // min (0) is the "center" for application 1 selection 196 | // 1/7 (146) app 2 197 | // 2/7 (292) qpp 3 198 | // 3/7 (438) app 4 199 | // 4/7 (585) app 5 200 | // 5/7 (731) app 6 201 | // 6/7 (877) app 7 202 | // max (1023) is the "center" for application 8 selection 203 | // 204 | // indicators are labeled at 1/3 (341) and 2/3 (683) of the range 205 | // 0 (0) - 1/14 (73) choose application 1 206 | // 1/14 (73) - 3/14 (219) choose application 2 207 | // 3/14 (219) - 5/14 (365) choose application 3 208 | // 5/14 (365) - 7/14 (512) choose application 4 209 | // 7/14 (512) - 9/14 (658) choose application 5 210 | // 9/14 (658) - 11/14 (804) choose application 6 211 | // 11/14 (804) - 13/14 (950) choose application 7 212 | // 13/14 (950) - max (1023) choose application 8 213 | 214 | // read the value 215 | int sensorValue = analogRead(PIN_ANALOG_IN); 216 | char debugString[100]; 217 | sprintf(debugString, "Sensor Value: %d", sensorValue); 218 | debug(debugString); 219 | 220 | if (sensorValue < 73) { 221 | debug("Application: Zoom (PC)"); 222 | useMacShortcuts = false; 223 | currentApplication = APP_ZOOM; 224 | updateDotStarColor(COLOR_BLUE); 225 | } else if (sensorValue >= 73 && sensorValue < 219) { 226 | debug("Application: Google Meet (PC)"); 227 | useMacShortcuts = false; 228 | currentApplication = APP_GOOGLE_MEET; 229 | updateDotStarColor(COLOR_LIME); 230 | } else if (sensorValue >= 219 && sensorValue < 365) { 231 | debug("Application: MS Teams (PC)"); 232 | useMacShortcuts = false; 233 | currentApplication = APP_MS_TEAMS; 234 | updateDotStarColor(COLOR_DARKVIOLET); 235 | } else if (sensorValue >= 365 && sensorValue < 512) { 236 | debug("Application: WebEx (PC)"); 237 | useMacShortcuts = false; 238 | currentApplication = APP_WEBEX; 239 | updateDotStarColor(COLOR_GOLD); 240 | } else if (sensorValue >= 512 && sensorValue < 658) { 241 | debug("Application: Zoom (MAC)"); 242 | useMacShortcuts = true; 243 | currentApplication = APP_ZOOM; 244 | updateDotStarColor(COLOR_BLUE); 245 | } else if (sensorValue >= 658 && sensorValue < 804) { 246 | debug("Application: Google Meet (MAC)"); 247 | useMacShortcuts = true; 248 | currentApplication = APP_GOOGLE_MEET; 249 | updateDotStarColor(COLOR_LIME); 250 | } else if (sensorValue >= 804 && sensorValue < 950) { 251 | debug("Application: MS Teams (MAC)"); 252 | useMacShortcuts = true; 253 | currentApplication = APP_MS_TEAMS; 254 | updateDotStarColor(COLOR_DARKVIOLET); 255 | } else if (sensorValue >= 950) { 256 | debug("Application: WebEx (MAC)"); 257 | useMacShortcuts = true; 258 | currentApplication = APP_WEBEX; 259 | updateDotStarColor(COLOR_GOLD); 260 | } 261 | #else 262 | debug("No Analog Input - Using Default Application: Zoom (PC)"); 263 | useMacShortcuts = false; 264 | currentApplication = APP_ZOOM; 265 | updateDotStarColor(HUE_BLUE); 266 | #endif 267 | } 268 | 269 | /** 270 | * Button One Click Handler 271 | * 272 | * This function will be called when button 1 is pressed for more than 50ms and 273 | * less than 300ms (configured in `setup` using `button1.setPressTicks`). 274 | */ 275 | void handleButton1Click() { 276 | // first we turn off the LED to indicate that we received the click 277 | turnLedOff(); 278 | turnButton1LedOff(); 279 | 280 | // update the current application in case the nob was changed 281 | setApplicationSettings(); 282 | 283 | // clear any current key presses 284 | Keyboard.releaseAll(); 285 | 286 | // toggle the microphone mute state 287 | toggleMicrophoneMute(); 288 | 289 | // turn the LED back on to indicate that we are ready for another command 290 | delay(50); 291 | turnLedOn(); 292 | turnButton1LedOn(); 293 | } 294 | 295 | /** 296 | * Button One Double-click Handler 297 | * 298 | * This function is called when button 1 is pressed twice within 600ms 299 | */ 300 | void handleButton1DoubleClick() { 301 | // first we turn off the LED to indicate that we received the click 302 | turnLedOff(); 303 | turnButton1LedOff(); 304 | 305 | // update the current application in case the nob was changed 306 | setApplicationSettings(); 307 | 308 | runMacKeyboardSetup(); 309 | 310 | // turn the LED back on to indicate that we are ready for another command 311 | delay(50); 312 | turnLedOn(); 313 | turnButton1LedOn(); 314 | } 315 | 316 | /** 317 | * Button One Long Press Start Handler 318 | * 319 | * This function will be called when button 1 is pressed and held down for more 320 | * than 300ms (configured in `setup` using `button1.setPressTicks`). 321 | */ 322 | void handleButton1LongPressStart() { 323 | // first we turn off the LED to indicate that we received the long press 324 | turnLedOff(); 325 | turnButton1LedOff(); 326 | 327 | // update the current application in case the nob was changed 328 | setApplicationSettings(); 329 | 330 | // clear any current key presses 331 | Keyboard.releaseAll(); 332 | 333 | startPushToTalk(); 334 | } 335 | 336 | /** 337 | * Button One Long Press Stop Handler 338 | * 339 | * This function will be called when button 1 is released after being held down. 340 | */ 341 | void handleButton1LongPressStop() { 342 | stopPushToTalk(); 343 | 344 | // don't update the application here - whatever we started we want to stop 345 | 346 | // turn the LED back on to indicate that we are ready for another command 347 | delay(50); 348 | turnLedOn(); 349 | turnButton1LedOn(); 350 | } 351 | 352 | /** 353 | * Button Two Click Handler 354 | * 355 | * This function will be called when button 2 is pressed for more than 50ms and 356 | * less than 1000ms 357 | */ 358 | void handleButton2Click() { 359 | // first we turn off the LED to indicate that we received the click 360 | turnLedOff(); 361 | turnButton2LedOff(); 362 | 363 | // update the current application in case the nob was changed 364 | setApplicationSettings(); 365 | 366 | // clear any current key presses 367 | Keyboard.releaseAll(); 368 | 369 | // toggle the video state 370 | toggleVideoState(); 371 | 372 | // turn the LED back on to indicate that we are ready for another command 373 | delay(50); 374 | turnLedOn(); 375 | turnButton2LedOn(); 376 | } 377 | 378 | /** 379 | * Toggle the microphone mute state for the selected application 380 | */ 381 | void toggleMicrophoneMute() { 382 | debug("Toggle Microphone Mute"); 383 | switch (currentApplication) { 384 | case APP_ZOOM: 385 | toggleMicrophoneMuteZoom(); 386 | break; 387 | 388 | case APP_GOOGLE_MEET: 389 | toggleMicrophoneMuteGoogleMeet(); 390 | break; 391 | 392 | case APP_MS_TEAMS: 393 | toggleMicrophoneMuteTeams(); 394 | break; 395 | 396 | case APP_WEBEX: 397 | toggleMicrophoneMuteWebEx(); 398 | break; 399 | } 400 | } 401 | 402 | /** 403 | * Toggle the microphone mute state in Zoom 404 | */ 405 | void toggleMicrophoneMuteZoom() { 406 | if (useMacShortcuts) { 407 | // Mac doesn't seem to have a keyboard shortcut for 408 | // bringing Zoom into the foreground 409 | 410 | // send cmd-shift-A for Mac 411 | Keyboard.press(KEY_LEFT_GUI); 412 | Keyboard.press(KEY_LEFT_SHIFT); 413 | Keyboard.press('a'); 414 | Keyboard.releaseAll(); 415 | } else { 416 | // send ctl-alt-shift to bring Zoom into the foreground 417 | Keyboard.press(KEY_LEFT_CTRL); 418 | Keyboard.press(KEY_LEFT_ALT); 419 | Keyboard.press(KEY_LEFT_SHIFT); 420 | Keyboard.releaseAll(); 421 | delay(50); // Give Zoom a chance to get into the foreground 422 | 423 | // send alt-A for PC/Chromebook/etc 424 | Keyboard.press(KEY_LEFT_ALT); 425 | Keyboard.press('a'); 426 | Keyboard.releaseAll(); 427 | } 428 | } 429 | 430 | /** 431 | * Toggle the microphone mute state in Google Meet 432 | */ 433 | void toggleMicrophoneMuteGoogleMeet() { 434 | if (useMacShortcuts) { 435 | // send cmd-D for Mac 436 | Keyboard.press(KEY_LEFT_GUI); 437 | Keyboard.press('d'); 438 | Keyboard.releaseAll(); 439 | } else { 440 | // send ctrl-D for PC/Chromebook/etc 441 | Keyboard.press(KEY_LEFT_CTRL); 442 | Keyboard.press('d'); 443 | Keyboard.releaseAll(); 444 | } 445 | } 446 | 447 | /** 448 | * Toggle the microphone mute state in Microsoft Teams 449 | */ 450 | void toggleMicrophoneMuteTeams() { 451 | if (useMacShortcuts) { 452 | // send cmd-shift-M for Mac 453 | Keyboard.press(KEY_LEFT_GUI); 454 | Keyboard.press(KEY_LEFT_SHIFT); 455 | Keyboard.press('m'); 456 | Keyboard.releaseAll(); 457 | } else { 458 | // send ctrl-shift-M for PC/Chromebook/etc 459 | Keyboard.press(KEY_LEFT_CTRL); 460 | Keyboard.press(KEY_LEFT_SHIFT); 461 | Keyboard.press('m'); 462 | Keyboard.releaseAll(); 463 | } 464 | } 465 | 466 | /** 467 | * Toggle the microphone mute state in Cisco WebEx 468 | */ 469 | void toggleMicrophoneMuteWebEx() { 470 | if (useMacShortcuts) { 471 | // send cmd-shift-M for Mac 472 | Keyboard.press(KEY_LEFT_GUI); 473 | Keyboard.press(KEY_LEFT_SHIFT); 474 | Keyboard.press('m'); 475 | Keyboard.releaseAll(); 476 | } else { 477 | // send ctrl-M for PC/Chromebook/etc 478 | Keyboard.press(KEY_LEFT_CTRL); 479 | Keyboard.press('m'); 480 | Keyboard.releaseAll(); 481 | } 482 | } 483 | 484 | /** 485 | * Start Push-to-talk (PTT) for the selected application 486 | * 487 | * Currently only implemented for Zoom 488 | */ 489 | void startPushToTalk() { 490 | debug("Start Push To Talk"); 491 | switch (currentApplication) { 492 | case APP_ZOOM: 493 | startPushToTalkZoom(); 494 | break; 495 | 496 | case APP_GOOGLE_MEET: 497 | case APP_MS_TEAMS: 498 | case APP_WEBEX: 499 | // Not implemented in Google Meet, Microsoft Teams, or WebEx so we are 500 | // just going to toggle the microphone mute for a better UX, otherwise 501 | // the button doesn't do _anything_ on a long press detection 502 | toggleMicrophoneMute(); 503 | break; 504 | } 505 | } 506 | 507 | /** 508 | * Start Push-to-talk (PTT) for Zoom 509 | */ 510 | void startPushToTalkZoom() { 511 | if (useMacShortcuts) { 512 | // Mac doesn't seem to have a keyboard shortcut for 513 | // bringing Zoom into the foreground 514 | 515 | // Press and hold the space key for Mac 516 | Keyboard.press(' '); 517 | } else { 518 | // send ctl-alt-shift to bring Zoom into the foreground 519 | Keyboard.press(KEY_LEFT_CTRL); 520 | Keyboard.press(KEY_LEFT_ALT); 521 | Keyboard.press(KEY_LEFT_SHIFT); 522 | Keyboard.releaseAll(); 523 | delay(50); // Give Zoom a chance to get into the foreground 524 | 525 | // Press and hold the space key for PC/Chromebook/etc 526 | Keyboard.press(' '); 527 | } 528 | } 529 | 530 | /** 531 | * Stop Push-to-talk (PTT) for the selected application 532 | * 533 | * Currently only implemented for Zoom 534 | */ 535 | void stopPushToTalk() { 536 | debug("Stop Push To Talk"); 537 | // Delay to prevent cutting off the speaker's last word 538 | delay(300); 539 | 540 | switch (currentApplication) { 541 | case APP_ZOOM: 542 | stopPushToTalkZoom(); 543 | break; 544 | 545 | case APP_GOOGLE_MEET: 546 | case APP_MS_TEAMS: 547 | case APP_WEBEX: 548 | // Not implemented in Google Meet, Microsoft Teams, or WebEx 549 | break; 550 | } 551 | } 552 | 553 | /** 554 | * Stop Push-to-talk (PTT) for Zoom 555 | */ 556 | void stopPushToTalkZoom() { 557 | // Release the PTT key to re-mute Zoom after temporary unmute 558 | Keyboard.releaseAll(); 559 | } 560 | 561 | /** 562 | * Toggle the video state for the selected application 563 | */ 564 | void toggleVideoState() { 565 | debug("Toggle Video State"); 566 | switch (currentApplication) { 567 | case APP_ZOOM: 568 | toggleVideoStateZoom(); 569 | break; 570 | 571 | case APP_GOOGLE_MEET: 572 | toggleVideoStateGoogleMeet(); 573 | break; 574 | 575 | case APP_MS_TEAMS: 576 | toggleVideoStateTeams(); 577 | break; 578 | 579 | case APP_WEBEX: 580 | // Not implemented in WebEx 581 | //toggleVideoStateWebEx(); 582 | break; 583 | } 584 | } 585 | 586 | /** 587 | * Toggle the video state for Zoom 588 | */ 589 | void toggleVideoStateZoom() { 590 | if (useMacShortcuts) { 591 | // Mac doesn't seem to have a keyboard shortcut for 592 | // bringing Zoom into the foreground 593 | 594 | // send cmd-shift-V for Mac 595 | Keyboard.press(KEY_LEFT_GUI); 596 | Keyboard.press(KEY_LEFT_SHIFT); 597 | Keyboard.press('v'); 598 | Keyboard.releaseAll(); 599 | } else { 600 | // send ctl-alt-shift to bring Zoom into the foreground 601 | Keyboard.press(KEY_LEFT_CTRL); 602 | Keyboard.press(KEY_LEFT_ALT); 603 | Keyboard.press(KEY_LEFT_SHIFT); 604 | Keyboard.releaseAll(); 605 | delay(50); // Give Zoom a chance to get into the foreground 606 | 607 | // send alt-V for PC/Chromebook/etc 608 | Keyboard.press(KEY_LEFT_ALT); 609 | Keyboard.press('v'); 610 | Keyboard.releaseAll(); 611 | } 612 | } 613 | 614 | /** 615 | * Toggle the video state in Google Meet 616 | */ 617 | void toggleVideoStateGoogleMeet() { 618 | if (useMacShortcuts) { 619 | // send cmd-E for Mac 620 | Keyboard.press(KEY_LEFT_GUI); 621 | Keyboard.press('e'); 622 | Keyboard.releaseAll(); 623 | } else { 624 | // send ctrl-E for PC/Chromebook/etc 625 | Keyboard.press(KEY_LEFT_CTRL); 626 | Keyboard.press('e'); 627 | Keyboard.releaseAll(); 628 | } 629 | } 630 | 631 | /** 632 | * Toggle the video state in Microsoft Teams 633 | */ 634 | void toggleVideoStateTeams() { 635 | if (useMacShortcuts) { 636 | // send cmd-shift-O for Mac 637 | Keyboard.press(KEY_LEFT_GUI); 638 | Keyboard.press(KEY_LEFT_SHIFT); 639 | Keyboard.press('o'); 640 | Keyboard.releaseAll(); 641 | } else { 642 | // send ctrl-shift-O for PC/Chromebook/etc 643 | Keyboard.press(KEY_LEFT_CTRL); 644 | Keyboard.press(KEY_LEFT_SHIFT); 645 | Keyboard.press('o'); 646 | Keyboard.releaseAll(); 647 | } 648 | } 649 | 650 | /** 651 | * Hits the correct keys to get through the MacOS Keyboard Setup wizard to 652 | * identify the keyboard as a "ANSI (United States and others)" standard 653 | * 101-key or 102-key keyboard so that it is useable. 654 | * 655 | * Presses the "Z" key (the key immediately to the right of the Shift key on the 656 | * left side of the keyboard) followed by the "/" (and ?) key (the key 657 | * immediately to the left of the Shift key on the right side of the keyboard). 658 | */ 659 | void runMacKeyboardSetup() { 660 | debug("Run Mac Keyboard Setup Keys"); 661 | delay(1000); 662 | Keyboard.releaseAll(); // Clear any current key presses 663 | Keyboard.press('z'); // Press the key immediately to the right of the Shift key on the left side of the keyboard 664 | Keyboard.releaseAll(); // Clear any current key presses 665 | delay(2000); 666 | Keyboard.press('/'); // Press the key immediately to the left of the Shift key on the right side of the keyboard (/ and ?) 667 | Keyboard.releaseAll(); // Clear any current key presses 668 | delay(1000); 669 | } 670 | 671 | /** 672 | * Turn the LED (connected to `PIN_LED` and GND) on 673 | */ 674 | void turnLedOn() { 675 | #ifdef PIN_LED 676 | digitalWrite(PIN_LED, HIGH); 677 | #endif 678 | } 679 | 680 | /** 681 | * Turn the LED (connected to `PIN_LED` and GND) off 682 | */ 683 | void turnLedOff() { 684 | #ifdef PIN_LED 685 | digitalWrite(PIN_LED, LOW); 686 | #endif 687 | } 688 | 689 | /** 690 | * Turn on the LED (connected to `PIN_BUTTON_1_LED` and GND) inside Button 1 691 | */ 692 | void turnButton1LedOn() { 693 | #ifdef PIN_BUTTON_1_LED 694 | digitalWrite(PIN_BUTTON_1_LED, HIGH); 695 | #endif 696 | } 697 | 698 | /** 699 | * Turn off the LED (connected to `PIN_BUTTON_1_LED` and GND) inside Button 1 700 | */ 701 | void turnButton1LedOff() { 702 | #ifdef PIN_BUTTON_1_LED 703 | digitalWrite(PIN_BUTTON_1_LED, LOW); 704 | #endif 705 | } 706 | 707 | /** 708 | * Turn on the LED (connected to `PIN_BUTTON_2_LED` and GND) inside Button 2 709 | */ 710 | void turnButton2LedOn() { 711 | #ifdef PIN_BUTTON_2_LED 712 | digitalWrite(PIN_BUTTON_2_LED, HIGH); 713 | #endif 714 | } 715 | 716 | /** 717 | * Turn off the LED (connected to `PIN_BUTTON_2_LED` and GND) inside Button 2 718 | */ 719 | void turnButton2LedOff() { 720 | #ifdef PIN_BUTTON_2_LED 721 | digitalWrite(PIN_BUTTON_2_LED, LOW); 722 | #endif 723 | } 724 | 725 | /** 726 | * Update the color of the onboard DotStar 727 | * 728 | * color is a single "packed" 32-bit RGB color. 729 | * 730 | * Use the COLOR_* definitions from "html_color_names.h" for ease of use 731 | */ 732 | void updateDotStarColor(uint32_t color) { 733 | #if DOTSTAR_NUMPIXELS > 0 && defined(PIN_DOTSTAR_DATA) && defined(PIN_DOTSTAR_CLOCK) 734 | // color is passed through strip.gamma32() to provide 'truer' colors 735 | // before filling the strip 736 | strip.fill(strip.gamma32(color)); 737 | strip.show(); 738 | #endif 739 | } 740 | --------------------------------------------------------------------------------