Close
0%
0%

RemuterMCS

Remote control for muting and unmuting the microphone, camera, and speakers

Similar projects worth following
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 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:

  1. I don't want the control to be tethered via a USB cable or any other kind of wire.
  2. 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.
  3. While juggling all that stuff, I want it to be resistant to accidental key presses so I don't unmute myself unintentionally.
  4. I want to be able to tell, without exerting too much brain power, whether I am currently muted or unmuted.
  5. 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).

I've thought about this project enough that I'm pretty sure what I'm going to use, but it keeps 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. If you read the project log, I suggest you start with the earliest entry and read them chronologically.

Here is a brief overview video that I made pretty well into the project. I put a link to it here so you can see where things get to after a bunch of time passed. [[Video updated 27 July 2025]]

  • Forget about it, BLE edition

    WJCarpenter08/08/2025 at 00:47 0 comments

    One and a half bugs, one feature. These are in the v20250807 release.

    The bug is that RemuterMCS didn't notice if the BLE connection got disconnected. So, it thought it was sending key presses to the host, but they were just falling on the floor. The half bug was in my fork of BleKeyboard. Even though it noticed disconnects, I had added a flag for authentication (described in an earlier project log). A disconnect should have also invalidated the authenticated flag. That's now remedied in my fork.

    Now, RemuterMCS frequently checks that it's still connected (and authenticated). If it detects a disconnect, it goes back to the "BT connecting..." splash screen. That gives the user an opportunity to re-establish the connection. Even if they can't or don't want to do that, they have obvious visual feedback about the situation they are in.

    On my first start down the trail of getting my code, BleKeyboard, and the BLE stack in shape to initiate a new connection, I ran into all kinds of edge cases and peeling-the-onion considerations. Before too long, I decided that, even if I figured all of that out properly, it was just too fragile and had too much hair on it. I realized I already had proper logic for re-establishing the connection after deep sleep. I changed my strategy to putting the M5StickC into deep sleep for 1 ms. It only took a bit of minor code tweaking to get the splash screen logic to run for that special case. The reason I chose a short deep sleep instead of a full power reboot was that the deep sleep path was already set up to remember little pieces of state (for example, the target with focus and the mute/unmute state of each target). The 1 ms deep sleep is too short for any human perception. I'm really happy with how it turned out.

    I also decided to do something which was a small annoyance for me but which might be a big problem for an unsophisticated user.  Both Bluedroid and NimBLE store some pairing keys in flash. If you want to switch using RemuterMCS with a different computer/tablet/phone (which in BT is the client, with the M5StickC as the server), it's difficult or impossible to get it to work because of that stored data. Up to this point, I had been solving that problem by re-flashing the firmware. That could be a big hurdle for some users. I wanted to create a simple way to deal with that without requiring a re-flash.

    My idea was to detect some specific key clicks when the "BT connecting..." screen is displayed. For a paired M5StickC, that's too brief a time to expect a user to do anything. With the new disconnect detection, that screen will be displayed indefinitely if the connection is not re-established. The procedure became (1) disconnect the connection on the computer side, and (2) do the special thing on the M5StickC to purge the stored key data. To get the BLE stack and friends into the proper state, I used the brief deep sleep trick described above.

    I didn't think about it at first, but a couple of my decisions conspired to prevent using my usual button click counting framework. First, the BLE connection stuff happens as part of the Arduino sketch setup(). Second, my use of EventOS for button clicks depends on something that happens in the Arduino sketch loop(). I decided to instead use a long press as the trigger for purging the stored keys. It's easy to detect (digitalRead()) and time in a loop I was already doing. My final decision was to require long pressing both the Action and Mode buttons for at least 3 seconds. That was pretty easy to implement.

    A couple of interesting user experience things came up when implementing the long press measurements. 

    • It wasn't obvious to the user when they had pressed for a long enough duration, (modulo, "one Mississippi, two Mississippi, three Mississippi"). I already had logic for winking the display, so I had it wink twice when the 3 seconds was up.
    • It takes a while for the user to take their fingers off the buttons. Even for someone trying to react...
    Read more »

  • The second end

    WJCarpenter07/28/2025 at 18:39 0 comments

    Yesterday, 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 to 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...
    Read more »

  • BLE wars. start-up edition

    WJCarpenter07/19/2025 at 04:20 0 comments

    In a previous project log, Bluedroid vs NimBLE, and BleKeyboard, I was comparing the start-up times of NimBLE and Bluedroid BLE stacks. Both produced sub-second start-up times. Those measurements were based on BleKeyboard reporting it was connected. I was fat, dumb, and happy for a while until I realized that being "connected" was not good enough. Until the connections finished authentication, they wouldn't send keystrokes. Depending on unlucky timing, sometimes that meant that keystrokes action clicks in RemuterMCS were lost. I forked BleKeyboard and implemented the necessary callback handlers to detect when authentication was finished.

    I did an initial set of timings of waking up from deep sleep and sending keystrokes. The way to interpret these numbers "789 ms at 1234" is "789 ms from BLE connection initiation to authenticated, at 1234 ms since wake up from sleep".

    DeviceNimBLEBluedroid
    M5StickC693 ms at 2105
    857 ms at 2269
    1036 ms at 2443
    822 ms at 2247
    720 ms at 2575
    1078 ms at 2933
    907 ms at 2781
    1077 ms at 2935
    M5StickC-Plus1692 ms at 2503
    838 ms at 1653
    746 ms at 1553
    1660 ms at 2467
    870 ms at 2100
    798 ms at 2047
    760 ms at 1994
    929 ms at 2193
    M5StickC-Plus21510 ms at 2267
    866 ms at 1624
    1122 ms at 1878
    1011 ms at 1774
    783 ms at 1975
    838 ms at 2022
    780 ms at 1975
    917 ms at 2092

    I thought the above times were too long and felt laggy. I decided to do some work to re-arrange the start-up sequence when waking up from deep sleep. I get the BLE reconnect going as early as possible. Since that mostly happens on the other core of the ESP32, I'm hoping it's overlapped quite a bit with the rest of my start-up code. I discovered that the M5Unified begin() method spends a lot of time initializing the display, so I deferred using the display until after the BLE keystrokes were sent. I was suprised to find that it took nearly a second to initialize the M5StickC's ST7735 display. It's only a few hundred milliseconds for the ST7789 display in the other models.

    Here are the timings after the re-arranging. It's kind of disappointing that they didn't really change much.

    DeviceNimBLEBluedroid
    M5StickC1615 ms at 2336
    1106 ms at 1868
    1209 ms at 1897
    1033 ms at 1732
    779 ms at 1880
    838 ms at 1897
    844 ms at 1939
    891 ms at 1965
    M5StickC-Plus856 ms at 1525
    980 ms at 1685
    709 ms at 1336
    730 ms at 1388
    822 ms at 1971
    877 ms at 2016
    952 ms at 2013
    786 ms at 1906
    M5StickC-Plus21262 ms at 1886
    1511 ms at 2140
    792 ms at 1421
    1081 ms at 1708
    664 ms at 1700
    1052 ms at 2087
    769 ms at 1816
    728 ms at 1779

    There's considerable variation in the numbers, which could be due to whatever happens to be happening on the computer side, or maybe even other radio traffic around the Bluetooth frequencies. If you squint a bit, it looks like it's kind of a wash between Bluedroid and NimBLE.

    The usual motivation for using NimBLE instead of Bluedroid is resource overhead. Here are some measurements of flash space and RAM usage for global variables, as reported by the toolchain in Arduino IDE. I don't have any figures for runtime heap/stack space usage.

    DeviceNimBLEBluedroid
    M5StickC832906 bytes (26%) flash
    38952 bytes (11%) RAM
    1316966 bytes (41%) flash
    43548 bytes (13%) RAM
    M5StickC-Plus853798 bytes (27%)  flash
    39000 bytes (11%) RAM
    1345102 bytes (42%) flash
    43596 bytes (13%) RAM
    M5StickC-Plus2851694 bytes (25%) flash
    39012 bytes (11%) RAM
    1332042 bytes (39%) flash
    43628 bytes (13%) RAM

    There's a modest advantage in global variable RAM usage for NimBLE and a large advantage in flash usage. Neither is close to a danger point for the M5StickC series for either BLE stack.

  • Screenshots

    WJCarpenter07/12/2025 at 23:16 0 comments

    I've started working on user documentation, and that has me wishing for screenshots of the device in action. In a previous unrelated project, I went to quite a bit of trouble to get screenshots. In that case, the display controller didn't provide a way to read the pixels back out. The display controllers for the M5StickC devices (ST7735 or ST7789) do provide that function. Although it's pretty inefficient to read out the pixels for the whole screen, it's one-time admin stuff for me and no impact for users. 

    I started by creating some conditionally compiled code to read out the pixel values. Additional conditionally compiled code asks for a screenshot after something interesting has changed on the screen. Mostly it just walks through synthetic button pushes.

    What's left is getting the pixels values off the device in a usable way. As with the previous project, I write things in a text format to the serial console. I chose to use "plain PPM" because it's very simple to generate. The spec says lines can't be longer than 70 characters, but I found experimentally that the converter I chose would not care about that. To make it easy to filter out any other lines that happened to appear in the serial console output, I gave each line a distinctive prefix. So that I could capture many screenshots in a single session, I separated them with a distinctive cut line. Finally, to make it easy to identify each screenshot, the second line of each contains a piece of text that I would later use as a filename.

    This raw capture file looks something like this:

    ...unrelated serial console log lines...
    ppm_ppm P3
    ppm_ppm # bt_connecting
    ppm_ppm 240 135
    ppm_ppm 255
    ppm_ppm 0 0 0  0 0 0  0 0 0  0 0 0  0 0 0  0 0 0  0 0 0  0 0 0  0 0 0 ...
    ... many lines of RGB data ...
    ... 255 182 0  255 182 0  255 182 0  255 182 0  255 182 0  
    ppm_ppm # screenshot_screenshot_screenshot_screenshot
    ...unrelated serial console log lines...

    Although I have been using the Arduino IDE for compiling and upload, I found it convenient to capture the serial console output with this data using a utility called ttylog:

    sudo ttylog -f -b 115200 -d /dev/ttyUSB0| tee -p /tmp/somefile.txt

    To give myself time to run that command, I put in a delay of several seconds before the first screenshot data was written. (It's unrelated to screenshots, but looking at them made me realize that the battery bar on the very first screen ("BT Connecting....") was sometimes, but not always, weirdly low. I think that probably has something to do with some component coming up to speed after reboot. I don't think that's too important for the user experience, but I didn't want the screenshot to be quirky. I put in a couple seconds delay before that first screen when taking screenshots.)

    For post-processing, I wrote a short bash script that reads that large capture file (just over 2 MB), trims out any unrelated lines, removes my line prefix, splits the file into individual capture files, extracts the second line of each and uses it to rename those files. I then manually converted them to PNG using an online graphics conversion tool.

    Here's what one of those end results looks like. Not bad!

  • Sleeping on pins and ... pins

    WJCarpenter07/07/2025 at 21:06 0 comments

    I got around to configuring the ESP32 to wake up in response to a click of either the action button or the mode button.  The power button doesn't trigger a wake-up because it's wired through the AXP192 on the M5StickC and the M5StickC-Plus, and it's wired completely differently on the M5StickC-Plus2. Maybe I'll come back around to the power button, but for now I don't feel like it's worth messing with it (and it might be impossible).

    On all 3 models, the action button is on GPIO 37 and the mode button is on GPIO 39. Incidentally, those 2 pins are not exposed on most ESP32 dev boards, although they are always present on the ESP32 chip. I'm curious why M5 chose to use those two GPIOs, but it doesn't matter much. Those pins are in the GPIO range that are "input only" on the ESP32. The device circuits wire them both to external pull-up resistors. That is, a button press is a LOW signal level.

    The ESP32 offers two kinds of GPIO triggers for deep sleep wake-ups. esp_sleep_enable_ext0_wakeup lets you specify the signal level, but it can only be applied to a single GPIO. esp_sleep_enable_ext1_wakeup_io takes a bitmask of as many GPIOs are you want, but the triggering choices are "any pin HIGH" or "all pins LOW". Neither of these choices would work for multiple pins with pull-up resistors. My solution was to configure both types of trigger, one for the action button and one for the mode button. This is another reason the power button doesn't trigger a wake-up. There is no GPIO trigger type available for it.

    Once I coded for those two wake-up triggers, in addition to the existing timer trigger, things worked as expected, sort of. They worked completely reliably for actually waking up the device. A problem arose in realizing my scheme of using double and triple clicks of the action button. It was quite a bit of trouble to do that reliably because of tricky timing when the device is waking up from deep sleep. Here is my winding path:

    • Without any consideration of deep sleep, I had originally coded things using AceButton. I liked that particular library because it works on a callback scheme. Like most such libraries, it doesn't use IRQ interrupts, but depends on Arduino `loop()` nudging it once in a while.
    • When I came up with the idea for using action button triple click for switching to different key bindings (which I now realize I haven't described here yet), I found it kind of convoluted to code that up, given that AceButton doesn't natively have any notion of multi-clicks. I switched over to the M5Unified API. It does have multi-clicks (and I also thought, "hey, one fewer 3rd party dependency").
    • When working with GPIO wake-ups from deep sleep, I came up with a 1-deep pushback scheme for recording a synthetic button click for the button that had woken up the device. The problem was that I could not reliably detect the second click of a double-click. I spent tons of time fiddling with debounce intervals, multi-click timeout intervals, and rearrangements of code. Things remained pretty unreliable.
    • I decided to cut out as many middlemen as possible and implemented a scheme with interrupt service routines (ISRs) to record pin signal transitions. I guess many Arduino framework users would consider that an advanced topic, but I was quite familiar with the idea and was comfortable with its documented restrictions. As the beginning of `setup()`, I attached my ISRs to the action and mode pins. I also coded up some bookkeeping structures where the ISRs could record counts and timestamps. (Those are still in the code and as `ButtonBook` structs.) The ISR scheme worked pretty reliably (though not 100% reliably) for the action button. I assumed I could tweak enough things to make it completely reliable, but there was a major problem. The ISR for the mode button was recording many interrupts per second that were not coming from actual button pushes. I tried a lot of things and pored over a lot of library code to try to get rid of those spurious...
    Read more »

  • HID consumer control device

    WJCarpenter06/27/2025 at 21:49 0 comments

    I learned a new (to me) thing about HID today. There is a section of the HID spec that is about defining custom controls for devices. I'm not completely sure how this works or whether I could do something with it without massive surgery in my code, but it's an idea to explore. This github repo has a good starting point write-up about it (and some other HID things), including pointers to several other documents.

    What is my vague idea about this? A big more or less unsolved problem is how to accurately portray the device states on my gadget. It's compounded by the standard speaker mute key and the conferencing hot keys all acting as toggles. If I were to get into the business of providing something on the PC side (which I continue to hope to avoid, other than something off the shelf like AutoHotKey), this might be an easy way in.

  • Battery longevity

    WJCarpenter06/24/2025 at 20:05 0 comments

    In earlier project logs (Turbines to speed!Is this radio on? and friends), I gave my methodology for figuring out how long the battery would last. Those experiments were done with the M5StickC (95 mAh battery) instead of the M5StickC-Plus (120 mAh battery) or M5SticC-Plus2 (200 mAh battery).

    My earlier testing showed my code running about an hour with BT enabled, and about 2 hours with BT not enabled. That's squintingly consistent with the measurements done by this guy: How long does the battery last on the M5StickC? I test the battery life out, and see what gives.

    Now that I've figured out how to do timed deep sleeps, I re-ran testing with all 3 models. In this mode. It costs something to wake up every once in a while and read some measurements. The only thing running most of the time during deep sleep is the RTC. Here are the results of running tests. "Hours flashing" is a test that woke up and recorded the stats every 10 minutes. (I also woke the devices up with button presses 2 or 3 times to see if they were still going. In those cases, the display stayed on for 5-6 seconds before the devices went back to sleep.) The "Hours blinking" times are with waking up for the LED blinks every 4-5 seconds. "Hours displaying" was with the display locked on full-time.

    It's puzzling that the M5StickC-Plus was so much shorter than the other two. If I figure out why later, I'll update this log.

    DeviceHours

    flashing

    Hours
    blinking
    Hours

    displaying

    M5StickC31.724.61.2
    M5StickC-Plus11.89.71.3
    M5StickC-Plus230.920.62.2

    M5Stack makes two battery products that can work with the M5StickC series. I haven't done longevity timing with either of them, and I'm not sure how the external batteries would impact the battery level and voltage readings from the M5Unified API.

    • The M5StickC 18650C is a device that all of the devices can fit into, and they can be mounted securely in place with screws. It holds a replaceable 2200 mAh rechargeable battery cell. I haven't had mine apart (it's 4 small screws), but I'm guessing from the name that it's the standard 18650 cell, and theoretically you could have spares ready to go. I'm not sure if the charging works without an M5StickC in place. Incidentally, the USB-C connection for this battery add-on does not pass data to the M5StickC, which doesn't matter for RemuterMCS.
    • The Atom Tailbat is designed for the M5Stack Atom series, but it does work with the M5StickC. It's not a perfect physical fit but is reasonable. It includes a 190 mAh rechargeable battery. I don't know for sure, but it seems possible that you could "stack" a couple of these together for more battery capacity. If you did that, you'd end up with something as long as the M5StickC 18650C, but less stable and with less battery capacity. The Tailbat does pass data over the USB-C connection. Charging works without the Tailbat being plugged into an M5 controller.

  • If you know, you know

    WJCarpenter06/23/2025 at 23:35 0 comments

    I learned something new about BLE HID devices today. In exploring ESP32 deep sleep, I noticed that there could be some data left in the BLE server transmit buffer when the ESP32 went to sleep. The thing I learned is that there is no simple and reliable way around that. The compromise solution is to delay a bit before going to sleep.

    BLE has two ways to send data from the server to the client. A notification is fire-and-forget. An indication gets an explicit response from the other side. The obvious trade-off is that indications involve more latency. That doesn't really matter for this scenario because you can only push the buttons so fast anyhow. But, alas, the BLE standards seem to specify that HID devices (keyboards, mice, etc) should use notifications.

    I have the source code for everything, so it would be possible to disobey the standard and use indications, but I'd have to be prepared for the other side to "misbehave" by following the standard that we are flauting. That would be acceptable if we also provided some kind of custom HID-level driver on the other side, which I don't want to do. (If I were going down the path of a custom driver, I might as well forego using HID and just implement some simpler protocol. But, again, I don't want to be responsible for that low-level software on the other side.)

    Why can't we know when all the keystrokes have been transmitted? BLE is a layered architecture. The stack we're talking to (NimBLE in this case) is in turn talking to the Bluetooth controller layer, which is responsible for interacting with the radio chip. The Host Controller Interface (HCI) is standardized and doesn't provide the piece of information we want. We can know when the data has been handed off from the NimBLE layer, but we don't have a way to know if it's still partially trapped in the controller layer. Even if we're willing to step aside from the standardized HCI, the ESP native APIs don't provide the information either (at least as far as I can discover).

    So, I'm doing the compromise of providing a configuration for a delay before going into deep sleep (5 seconds is my default). I will eventually reconcile that with the display timeout (6 seconds is my default) since they are sort of cousins anyhow. In other words, if I have already delayed some amount of time for the display to time out, there's not much point in delaying again before sleep. The only reason I haven't reconciled things already is because I haven't yet tied the wake-ups to button pushes. When I do, turning the display off will become the start of deep sleep, and turning the display on will happen at the wake-up.

  • Bluedroid vs NimBLE, and BleKeyboard

    WJCarpenter06/21/2025 at 22:16 0 comments

    After a number of bloody bare-knuckle brawls trying to get things to compile and link correctly in the Arduino IDE 2.x environment, I changed my strategy for working out the timing to start up a BLE connection after deep sleep. I ignored my own code and the BleKeyboard library example. Instead, I used the generic example sketches.

    • File > Examples > BLE > Server, which I think uses the Bluedroid library on ESP32.
    • File > Examples > NimBLE-Arduino > NimBLE_Server, which obviously uses NimBLE.

    With those sample sketches slightly modified to include ESP32 deep sleeps, Bluedroid takes 600-700 ms to reconnect (with a lot of variation for some reason). NimBLE takes just a few milliseconds above 200 ms pretty consistently. That is a large enough difference to matter for human perception for this scenario, which pretty much cinches it for switching to NimBLE. The advertised benefit of NimBLE is that it uses fewer resources (mainly flash and RAM). I don't care much about that for this application, but I do care about the elapsed time on start-up.

    The original BleKeyboard library was written some years ago and has not seen many recent changes. It might be my own pilot errors, but I could not get it to compile and link in NimBLE mode. I suspect that it just hasn't kept up with changes to the Arduino framework, the ESP IDF tooling in their Arduino core, and the NimBLE library itself. Part of the problem is that NimBLE is included in the ESP tooling, which creates some conflicts. There are numerous forks of the BleKeyboard library on github. I'm going to prowl around some of those to see if I can use one as-is. If not, the library is small enough that I might just beat on it until I get it going in a private copy (or yet another fork).

    With a 200 ms BLE initialization time, I'll allow myself a budget of another 100 ms for other overhead. That should be responsive enough to let the M5StickC sleep between button pushes. I have not yet timed that, but I suspect that even with the tiny built-in battery it will give quite a reasonable lifetime between charges.

    UPDATE: I ran some more experiments to get finer grained timing. There are 3 data points I'm looking at.

    • The time for the BLE init() call to complete.
    • The time to wait for the connection to be setup (we get notified by a callback).
    • The total of those two things.

    Interestingly, they are very different for Bluedroid and NimBLE. This table summarizes eyeball observations over many iterations (ignore rounding, etc).

    StepBluedroidNimBLE
    init631 ms209 ms
    connect316 ms414 ms
    total946 ms623 ms

    NimBLE still wins, but not by as much as I previously thought. There is a lot of variation in the timing of waiting for the connection. If I can find a way to speed that up or at least make it consistently lower than the outliers, things might still be OK for user experience.

    The code in BleKeyboard does not wait for connections. If it's not connected, trying to send keys is a no-op, so that waiting will have to be done at a higher level.

  • Time on your side

    WJCarpenter06/20/2025 at 23:32 0 comments

    The immediate problem with using sleep states to extend battery life is that it takes too long to re-establish the BT connection. I decided to explore timing so I can see if I can do something about it. There are probably clever ways to do this efficiently, but I am too lazy to figure that out. Instead, I resorted to reporting times via Serial.print statements in the usual sort of binary search to see where the time is spent. I'm not too concerned about a few milliseconds here and there (unless it eventually turns out that everything is just an aggregation of tiny amounts of milliseconds and I can't get around it).

    NOTE: All of these timing figures are in the ESP32 Arduino framework and using Bluedroid. I wanted to try out NimBLE to see if it was inherently quicker for the re-connection step, but there were too many compilation problems that I am currently side-stepping. I may come back to NimBLE later if I can't get satisfaction from drilling down int Bluedroid.

    The first thing I did was modify my code to detect when I was waking up from deep sleep. For that case, I skipped some screen painting and UX delays. I termed a power-on boot up as a major reboot and a deep sleep wake-up as a minor reboot. For comparison, a major reboot was taking 4-5 seconds to reconnect (including my code's overhead), whereas a minor reboot (without my overhead) takes 600-900 ms. The time seems to vary a bit for reasons I don't know.

    I next drilled into BleKeyboard::begin(). Almost all of the time there was spent in a call to BLEDevice::init(). At the bottom of the init() function is this interesting line:

      vTaskDelay(200 / portTICK_PERIOD_MS);  // Delay for 200 msecs as a workaround to an apparent Arduino environment issue.
    

     I haven't yet uncovered why that inescapable 200 ms delay is needed. Assuming it really can't be avoided, and assuming miscellaneous other overhead is 100 ms or so, then 300 ms might be a tolerable lag for waking up from sleep and reconnecting. I think it would be apparent to humans, but still maybe OK. I'd rather avoid it, but first I'll see how much other overhead I can trim out. The worst case is that I would abandon the Arduino framework and code directly in ESP IDF, but that's a lot of rework for the non-BT code.

    Within BLEDevice::init(), calls to esp_bluedroid_init() are taking 300+ ms, and calls to  esp_bluedroid_enable() are taking about 100 ms. Other than the just-mentioned task delay, that is almost all of the time in BLEDevice::init(). The esp_bluedroid* functions are low-level and included in the ESP IDF libraries. That makes them more work to instrument. I could have rebuilt the libraries from sources, which would be sensible, but instead I copied source files and rejiggered include files until I could get it to build in the Arduino IDE without complaint. That took a lot of rejiggering.

    Within esp_bluedroid_init(), almost all of the time is spent in a call to btc_init().  The source file that includes btc_init() has about a billion include files, making for a lengthy rejiggering dance. Within esp_bluedroid_init(), all of time is spent in osi_thread_create(), which is a generic thread and memory allocation function. I don't think I'll be able to boil that down to something faster.

    Within esp_bluedroid_enable(), all of the time is spent in some kind of asynchronous wait for the completion of "future_p = btc_main_get_future_p(BTC_MAIN_ENABLE_FUTURE)".

    My conclusion, after all this messing around is that I can't readily reduce the BLE init time after deep sleep, except maybe by seeing if I can get rid of the 200 ms task delay. For now, I'm going to spend a few cycles seeing if I can get NimBLE going to see if that can be faster.

View all 32 project logs

Enjoy this project?

Share

Discussions

Does this project spark your interest?

Become a member to follow this project and never miss any updates