Controlling KVM via CircuitPython ESP32-S3 HID emulation

Hello! I absolutely adore my KVM, but I’d love to place it under my desk to keep things tidy. Unfortunately, I use a Logitech Bolt receiver which very much does not like being plugged into the HID port, so I’ve just been switching by pressing the button manually. I’ve recently learned how to use CircuitPython on the ESP32-S3 to create programmable HID devices, and figured I could essentially make a WiFi-connected keyboard to switch the KVM via my Elgato Stream Deck.

Unfortunately, while my “keyboard” works (since my OS reports ScrollLock, ScrollLock, 1 key press events), the KVM isn’t switching to the first input.

I know at least one other person managed to make a HID device, but that was with a WiFi-less board. Has anyone managed to get it working with a CircuitPython ESP32-S3 device?

Here’s what I have in case anyone is curious!

# boot.py

import storage
import usb_cdc
import usb_midi
from usb_hid import Device

storage.disable_usb_drive()
usb_cdc.disable()
usb_midi.disable()

# Inform the host that support for a a boot keyboard is available
usb_hid.enable((Device.KEYBOARD), boot_device=1)
# code.py

import socketpool
import wifi
import usb_hid
from time import sleep

from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keycode import Keycode

from adafruit_httpserver import Server, Request, Response, POST


pool = socketpool.SocketPool(wifi.radio)
server = Server(pool, "/static", debug=True)

keyboard = Keyboard(usb_hid.devices)


@server.route("/switch", [POST])
def switch(request: Request):
    port = request.form_data.get("port")
    try:
        if port == "1":
            keyboard.send(Keycode.SCROLL_LOCK)
            sleep(0.1)
            keyboard.send(Keycode.SCROLL_LOCK)
            sleep(0.1)
            keyboard.send(Keycode.ONE)
            return Response(request, "1")
        else:
            return Response(request, "invalid port")
    except Exception as e:
        return Response(request, f"{e}")



server.serve_forever(str(wifi.radio.ipv4_address), port=80)
1 Like

I had forgotten to reply to this because it’s wayyy out of my wheelhouse, and I have no clue how to help here.

Did you figure this out? I’d like to learn more about this sort of thing.

Thanks for the inspiration. It’s actually workign for me on PicoW. Remember to connect to the USB HID port. Though i’m not sure if the HID port has much less power or something but my wifi signal is really part when connected there.

# boot.py

import usb_hid
import storage
import microcontroller

if microcontroller.nvm[0] == 1:
  storage.enable_usb_drive() # probably redundant
else:
  storage.disable_usb_drive()

usb_hid.enable((usb_hid.Device.KEYBOARD,))

# code.py

import os
import socketpool
import wifi
import usb_hid
import microcontroller
import time

from time import sleep

from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keycode import Keycode

from adafruit_httpserver import Server, Request, Response, POST


def seconds_to_iso_time(s, tz):
    st = time.localtime(s)
    return f"{st[0]:04d}-{st[1]:02d}-{st[2]:02d}T{st[3]:02d}:{st[4]:02d}:{st[5]:02d}{tz:+03}:00"


def wifi_connect():
    last = 0
    while True:
        if (time.monotonic() - last) >= 1:
            try:
                wifi.radio.connect(
                    os.getenv("CIRCUITPY_WIFI_SSID"),
                    os.getenv("CIRCUITPY_WIFI_PASSWORD"),
                )

                if wifi.radio.connected:
                    print(
                        f"{seconds_to_iso_time(time.time(), -5)} [wifi connected={wifi.radio.connected} ipv4={wifi.radio.ipv4_address}]"
                    )

                    break
            except:
                print(f".", end="")
            last = time.monotonic()
            if not wifi.radio.connected:
                print("Retry connecting to wifi")


time.sleep(3)  # wait for serial
print(f"{'-'*25}")
wifi_connect()

pool = socketpool.SocketPool(wifi.radio)
server = Server(pool, "/static", debug=True)

keyboard = Keyboard(usb_hid.devices)


@server.route("/storage", [POST])
def toggle_storage(request: Request):
    microcontroller.nvm[0] = int(not microcontroller.nvm[0])
    # microcontroller.reset()
    return Response(request, f"{microcontroller.nvm[0]}")


@server.route("/switch", [POST])
def switch(request: Request):
    try:
        port = int(request.form_data.get("port"))
        if port >= 1 and port <= 4:
            keyboard.send(Keycode.SCROLL_LOCK)
            sleep(0.1)
            keyboard.send(Keycode.SCROLL_LOCK)
            sleep(0.1)
            keyboard.send(Keycode.ONE + port - 1)
            return Response(request, f"{port}")
        else:
            return Response(request, "invalid port")
    except Exception as e:
        return Response(request, f"{e}")


server.serve_forever(str(wifi.radio.ipv4_address), port=8080)


I have a logitech bolt receiver as well and the KVM I received actually reads it correctly, to the point that I can reprogram it with solaar on linux. I wasn’t able to do this on the cheaper KVM from amazon.

None the less, this is an interesting project!