Oh, sorry. I was on mute. Like everybody else, I spend a lot of time on computer conference calls these days. I wanted to have a way to quickly mute or unmute my microphone without having to think too much, without having to find the video conference application behind whatever window I was doing "research" in, and so on. That's not a very unique need. It's not just because I'm a tinkerer that I haven't found something that suits me. This project describes how I made something that I like. Maybe after I describe it, you'll think it's what you need, too. In case you are wondering, "MCS" stands for Martian Chronographic Spectrometer. Or something.
I started the project in 2021, let it go dormant for a few years, and picked it up again in mid-2025. I've just recently made my first public release (v20250723). The source code repository is at https://gitlab.com/wjcarpenter/remutermcs
Interesting mediaCode repository: https://gitlab.com/wjcarpenter/remutermcs
Detailed project log: https://hackaday.io/project/177896-remutermcs
User Guide: https://gitlab.com/wjcarpenter/remutermcs/-/blob/master/USER_GUIDE.md
Install Guide: https://gitlab.com/wjcarpenter/remutermcs/-/blob/master/INSTALL.md
FAQ: https://gitlab.com/wjcarpenter/remutermcs/-/blob/master/FAQ.md
Video demo:
Self-imposed requirementsI usually wear a bluetooth headset when I'm on a conference call. Like all courteous people around the world, I mute my microphone when I'm not talking. If I have to do anything outrageous, I also turn off my camera. There are times when I get out of my chair and move around, to get a cup of coffee, to deal with the family cat, or whatever. But if I have to say something on the call, I don't want to have to dash back to my keyboard to unmute myself.
So, that leads to these requirements:
- I don't want the control to be tethered via a USB cable or any other kind of wire.
- I want the control to be compact enough that I can carry it in my hand while also holding a coffee cup and maybe an unhealthy snack item and maybe some kind of cat shenanigans.
- While juggling all that stuff, I want it to be resistant to accidental key presses so I don't unmute myself unintentionally.
- I want to be able to tell, without exerting too much brain power, whether I am currently muted or unmuted.
- My primary concern is muting and unmuting my microphone input, but if I can get it, I would like to be able to turn my camera off (and back on, I guess, but that's mostly for symmetry), and I wouldn't mind being able to mute the speakers (even though, as I said, I'm usually on a headset).
As I thought about this project, it kept oscillating back and forth between pretty hard and pretty easy. I'll describe my research of alternatives and the actual development in the project log over here. If you read that project log, I suggest you start with the earliest entry and read them chronologically.
Spoiler alert! Here's a brief overview video of the implementation on the M5StickC-Plus:
The key to mutingIt's pretty typical for mainstream keyboards to have keys that do media controls. With those keys, you can pause or resume a video or audio track, you can turn the volume up or down. You can also do a few other things, like launch a web browser or your email application.
One of things you cannot do is mute your microphone. Sure, there is typically a mute button, but on a computer it has a different meaning than it has on a telephone handset. On a telephone, muting means muting the microphone. On a PC, muting means muting the audio output from the speakers.
At first, this lack of keyboard code for muting the microphone seemed like a weird oversight to me. I thought maybe I just hadn't looked in the right place to find that keyboard code. Well, by now I have looked in the right places, and there simply isn't one. I don't know where they were originally defined, but those special keyboard codes are listed in HID Usage Tables FOR Universal Serial Bus (USB). Likewise, the Bluetooth HID profile builds mostly on the USB HID profile, so it has the same keys defined. In fact, the Bluetooth Human Interface Device Profile 1.1.1 simply references the USB usage tables.
All of that goes to show that it's not a mistake that keyboards don't have such keys, except maybe in special cases where the manufacturer has gone to some extra trouble (maybe on some laptops). Likewise, it's not a mistake that operating systems don't provide a driver to listen for such a code and mute the microphone. It's the same situation for turning the camera off and on. Instead, it's up to applications to control those devices while leaving management of the speakers to the operating system. OK, I still think it's an mistake, but it's a standards-based mistake.
M5StickCEarly in my thinking, I was pretty solid on wanting a wireless solution, and using something that acted like a keyboard seemed promising. The natural DIY solution was an ESP32 because of its in-built wifi and Bluetooth. One of those in a nice little case with some buttons outside and battery inside sounded about right. But, hey, wait... I've already got something like that. M5Stack's M5StickC. I started in 2021 with the original M5StickC, but by the time I came back to this project in 2025, M5Stack had EOLed that and came out with the M5StickC-Plus and the M5StickC-Plus2. RemuterMCS supports all 3 models.
It's got a 95 mAh LiPo battery, a USB C charge/serial port, a 160x80 TFT display, a couple of buttons, and some other goodies that might or might not be interesting for this use case. You can get these for US$10-15, and I already have a couple right here. I've done some experimenting with them and found them pretty easy to work with. The M5Stack company's documentation and examples are pretty good.
They are all physically the same overall size: about the size of 2 LEGO bricks stacked on top of each other. If that's too small for your hand, or if you want more battery life, M5Stack also sells the M5StickC 18650C, which is a sort of wand with a LiPo battery into which the M5StickC can be fitted.
Sometimes the simplest-seeming things hide considerable complexity. If you use the USB C port or run the Bluetooth samples from M5Stack, they both act as simple serial communicators. I can remember back in the days before USB and PS/2 connectors when some keyboards just sent characters over an RS232-like connection. You typed a character, and it sent that character. If you held down the Control key and typed a character, it sent a control character (or nothing if the key you pressed was in the wrong place in the ASCII table). If you held down the Alt key and typed a character, it sent the character with the high bit set. Times were simple, indeed.
With the advent of USB standards, or maybe a little before, keyboards and mice were specifically recognized as those kinds of Human Interface Devices (HIDs), and they spoke a more elaborate protocol that allowed for more flexible interactions over a standardized protocol. A keyboard sends what is commonly called a "scan code", which generally represents a particular physical key. Along with that scan code, it also sends the current state of modifier keys like Control, Alt, Shift, charm, spin, damage, health, and... (wait, that can't be right).
What all this means is that you can't just feed characters over a serial connection and expect things to work on a modern PC. Luckily, there is an Arduino USB keyboard library, and someone called T-vK has taken the trouble to adapt that into an ESP32 BLE Keyboard library. A quick test showed that the BLE keyboard library gets recognized as a keyboard and can send characters over Bluetooth to the PC it's paired with. I later discovered I had to make an enhancement to that library for my scenario.
The user experienceIt makes it sound like I anticipated everything and worked toward my design intent, but coming up with what I think is a satisfactory user experience took me a lot of iterating and experimenting. There are only 2 usable buttons on the M5StickC (the power button can't reasonably be used for the sorts of things I wanted to do).
I've written an entire User Guide explaining how things work, but the workings in a nutshell (which you can also see in the above video) are:
- The large "M5" button is the Action button and is used to mute or unmute a target device on the computer.
- The smaller button on the side is the Mode button and is used to cycle focus among the target devices (microphone, camera, speakers).
If you hold the M5StickC in your hand with the Action button away from you, you can work it with your thumb. That's pretty comfortable, at least for me.
Here are some screenshots from the M5StickC showing various target device states.
The orange and blue segmented line on the right-hand size is the battery charge indicator. The small blob in the lower right-hand corner indicates that the MtStickC is plugged in; it's orange if the device is charging and blue if the device is fully charged. Battery level is also reported over the Bluetooth connection.
Speaking of colorsIt's not an accident or a whim that I used blue and orange colors instead of the more traditional green and red. It's a better user experience for people who have color vision deficiency and can't easily distinguish red and green shades. Colors for everything can be customized by easy edits in the source code.
These keys are so hotI originally thought that the computer side of this was going to be the easy part. And, I guess it is or it isn't, depending on how you look at it.
If you are only interested in using a single video conferencing application, and if you can live with the ambiguity of keyboard shortcuts that act as toggles, then you could customize the keyboard codes on the remote device to do your bidding. For example, here are the keyboard shortcuts I found for some popular video conferencing applications:
; Google Meet default Windows shortcuts
; https://support.google.com/a/users/answer/9896256
; microphone toggle: Control-d
; camera toggle: Control-e
; Webex Meetings default Windows shortcuts
; https://help.webex.com/en-us/84har3/Cisco-Webex-Meetings-and-Cisco-Webex-Events-Accessibility-Features
; microphone toggle: Control-m
; camera toggle: Control-Shift-v
; Zoom default Windows shortcuts
; https://support.zoom.us/hc/en-us/articles/205683899-Hot-Keys-and-Keyboard-for-Zoom
; microphone toggle: Alt-a
; camera toggle: Alt-v
; Microsoft Teams meetings and calls
; https://support.microsoft.com/en-us/office/keyboard-shortcuts-for-microsoft-teams-2e8e2a70-e8d8-4a19-949b-4c36dd5292d2
; microphone toggle: Ctrl+Shift+M
; camera toggle: Ctrl+Shift+O
I really don't care for the simple toggles that the standard hotkeys use, and I myself use multiple conferencing applications. For a while I was planning to have one set of unique hot keys and let a computer-side utility "do the right thing". Along the way, I decided I didn't want to be in the business of providing that utility for various computer, tablet, or phone platforms.
The current scheme has RemuterMCS knowing about the specific hot keys for several conferencing applications (Zoom, Microsoft Teams, Google Meet, GoToMeeting, Cisco Webex, Slack huddles, Jitsi, my original AutoHotKey stuff) for both Windows/Linux/Android and Mac/iOS. There's a framework in place so that an ambitious person could add even more key bindings without too much trouble.
If you look at the screenshots earlier, you can see the word "Zoom" in the lower left-hand corner. That indicates that the hot keys for Zoom are currently active on the M5StickC. You can triple-click the Action button to cycle through other active bindings. In the default configuration, that includes the Microsoft Teams and Google Meet hot keys. If you use multiple conferencing systems, you can use the same M5StickC with RemuterMCS without needing to change the firmware.
Gained by counting sheepBatteries in all of the M5StickC models are on the small side. Even the M5StickC-Plus2 is only 200 mAh. I did some experiments to measure battery longevity.
The best power-savings is had by putting the ESP32 into deep sleep mode. On wake-up, the ESP32 goes through the entire boot-up sequence. My early code was a typical Arduino-esque sketch with setup() and loop() functions, and the assumption that setup() would only run once. After I decided to go with a deep sleep, I was running that setup() function over and over, so I had to rejigger things so that that makes sense. I wanted to turn off the BT radio during sleep and make sure it's turned back on at wake-up, but I also didn't want to go through the whole connection process each time.
The ESP32 has a built-in RTC with some kilobits of static RAM that can be used for state storage during sleep. That doesn't survive a real reboot, so I still have to use "preferences" memory for a few things. GPIO pins associated with the ESP32 RTC can be used to cause a wake-up.
Deep sleep and Bluetooth re-connection present their own problems. I did some additional experiments to see how long it took to get the BT connection in a state where it could reliably send keystrokes. It was while I was figuring this out that I found out I had to modify the BleKeyboard library. It was reporting "connected", but if I sent keystrokes immediately after that they were sometimes lost. As nearly as I can determine (without going to the trouble of actually knowing what I am talking about), I have to wait for the connection to be authenticated. My modification to the library provides an indication for that. Bluedroid and NimBLE stacks provide different mechanisms for that, so I had to implement both.
Bluedroid versus NimBLEThere are 2 readily available Bluetooth stacks for ESP32 in the Arduino framework. Which one to use? The short answer is that it probably doesn't matter for RemuterMCS.
NimBLE offers only Bluetooth Low Energy (BLE), whereas Bluedroid offers BLE as well as Bluetooth classic. RemuterMCS does not use Bluetooth classic, so that doesn't matter. NimBLE appears to be more frugal in resources (flash size and RAM usage) than Bluedroid, but there is plenty of room for both on the M5StickC running RemuterMCS. What you really care about for the RemuterMCS use case is responsiveness, and that seems to be pretty much a wash.
Because I had already gone to the trouble of supporting them both, it's a simple compile-time configuration option to choose which one you want. In the pre-built firmware bundles, I provide an image with each. You get to choose. :-)
Retrospective and futureYesterday, I cut the first public release (v20250723) of RemuterMCS. I'm pretty much done working on this for a while. I don't know if I'll ever come back around to any major work on this project, but there were a few ideas I had for futures....
- Explore whether there is any advantage to implementing directly with the ESP-IDF instead of the Arduino framework. I kind of doubt that there are any big wins there, but you never know.
- Refactor the code to make it easier to port to other devices beyond M5StickC. One of the major user experience limitations of the M5StickC series is having only two usable buttons. That's OK for the predominant job of controlling mute on the microphone, but it's slightly clumsy when dealing with multiple targets. A device with dedicated buttons (or a touchscreen) for each of the targets would be a better UX. (I don't regret using the M5StickC. I chose it because I already had one. It was fun and interesting.)
- Maybe even build my own device with a custom PCB. Having as many buttons or whatever is then no problem. The custom PCB houses make it simple these days for someone else to replicate the work, which is nice.
- Make a way to make the current compile-time configuration options be configurable at runtime. Almost all of them are already simple values rather than C++ "#define" things, so it's mostly a matter of creating a user experience for changing them. My initial thought is a simple web server running on the device with a form for changing the configuration items. Connection would be over wifi. Changes would be stored in flash.
- Maybe add some stuff for OTA firmware updates. That's really handy and I've use it a lot in other projects, but I've never looked into how to do it with Arduino framework and ESP32.
- Display updates are done via complete redraws of the screen. With more bookkeeping, I could probably redraw just parts that change. I'm not sure how much of a benefit that would be. It might make a bigger difference on a device with a larger screen.
- There's significant lag when waking up from sleep and reconnecting Bluetooth. I thought of some things that might help with that:
- With a custom device with a bigger battery, maybe I could avoid deep sleep when the display is off and still have reasonable battery life.
- Maybe if I study BLE more I can figure out a way to make the connections faster. I mostly know "just enough" BLE stuff.
- In particular, maybe there is a way to have the ESP32 mostly sleeping but still maintain the connection.
- I might be able to use FreeRTOS tasks to overlap the BLE re-connect with other things going on so that elapsed time is reduced. I'm not clear on which parts of the BLE stack happen on Core 1 and which happen on Core 0.
- There is no client-side component for RemuterMCS. That's a whole can of worms because of all the possible client environments. I was originally thinking of AutoHotKey, but that's a Windows-only partial solution.
- There is no feedback from the client to RemuterMCS, so it doesn't really know if targets are muted or unmuted. The RemuterMCS display can get out of sync with reality. An idea I had was to co-opt the USB HID mechanism for feeding back state to keyboards (caps lock, num lock, etc) to tell RemuterMCS about the state of target devices. My idea was to send multiple feedback packets in some pattern that would tell RemuterMCS what was going on, but would leave other devices in the correct states for the original use of that mechanism.
- Control of speakers is global to the operating system, but control of microphone and camera is application specific. If the intended application does not currently have focus, the sent hot keys would at best be ignored and at worst do something entirely different in whatever did have focus. That's a universal problem, but it's made worse by being remote from the computer. A client-side component might be able to force focus to the appropriate window (or funnel the hot keys there without actual focus, which would be just as good). (Actually detecting focused applications and changing focus are problems to be solved.)
- With a client-side component, there could be just a single set of keys transmitted by RemuterMCS. The client side could easily translate them into whatever is appropriate for any particular application. That was my original AutoHotKey scheme.
- RemuterMCS can have separate hot keys for mute vs unmute. Every application I've come across so far uses simple toggles, so the same hot key is used for both mute and unmute. A client side component could detect the current state of the target device and only act on the hot key if it was appropriate. That also partially solves the problem of lack of client-side feedback. Again, that was my original idea with AutoHotKey. (Actually detecting the target device states is a problem to be solved.
Oh, you wonder about my retrospective thoughts on this project?
- First, it was obviously a bit weird to shelve the project for 3-4 years before coming back and doing a large amount of work on it.
- I used to do a lot of C/C++ development in my day job literally decades ago. For the last few years, I'd only done the kind of simple-minded C++ stuff that you see in Arduino and similar projects. At some point I decided to get serious and read up on "modern C++". Over the years, they've done a lot of good things, but C++ can't escape its legacy problem of having all kinds of weird stuff in it that makes it difficult to match it up with any of the other mainstream languages of the last 20 years or so. Those other languages are mostly on the same page about things, but C++ is not, in various and quirky ways.
- I first tried the Arduino IDE 2.x when it was still in beta. I didn't really see the point of it, except that someone felt like doing it. When I came back to it, it was much more polished (as you would expect compared to beta), but it's a slow dog and has various usability problems. After a bit, I switched to doing all my editing in an external editor (emacs) and only using the Arduino IDE for building and uploading firmware. I think I could have switched to the Arduino CLI tools and probably even integrated them into the emacs build workflow, but I was too lazy to get around to that.
- My first pass a couple years ago used various M5Stack and 3rd party APIs for interfacing to hardware. When I resumed work, I saw that M5Stack had published the M5Unified API. I switched to that new API, hoping it would solve all my problems. Instead, it just kind of swapped me to different problems. The conversion itself was mechanically easy, consisting mostly of a few find-and-replace operations after changing the "#include" headers. The biggest problem -- that I spent a lot of time on -- was that the M5.begin() method has to be called first, and it can take a lot of time (about a second on the original M5StickC). A secondary problem is that the documentation for the M5Unified API is not the best. Luckily, it's open source, so I spent quite a bit of time up to my elbows in that source code when I needed to work out details.
- The M5StickC-Plus2 was a disappointment for RemuterMCS. It doesn't have a rich enough API for power management (hardware limitation), and I was limited pretty much to reading the battery voltage. For other use cases, that might not matter much, but power management is a pretty big deal in RemuterMCS. I wanted to support all 3 variants of the M5StickC, so I had to be creative with workarounds for the M5StickC-Plus2 power management shortcomings.
- Timing is everything. Getting things right for double and triple clicks occupied a lot of my time. The complicating factor was clicks that wake the device up from deep sleep. It took a lot (a lot!) of trial and error to get to the point of correctly recognizing multi-clicks when the device was awake and when the device woke up from deep sleep and that also worked with all 3 M5StickC models (which have different timing characteristics).
Comments