Zero to ESP32 remote control

Published on

A learn in public post on building a multi-button remote control with ESP32, esphome and Home Assistant.

Background #

I had a pair of shelf speakers some time ago. They were plugged in and turned on all the time and a brown-out fried them. I tried my hand at repairing them, but having next to zero skills when it comes to working with stuff in the meatspace, I probably just broke them even further, or at least beyond economic repair.

Since then I was looking for opportunities to improve my hardware skills, so my next screw up will be less spectacular.

NB: I don’t consider myself even remotely experienced to give the advice on the “this is how this should be done” level. I wrote this post to remember the lessons learned and because I wish something like this existed in one place when I was starting out.

Project goals #

The main goal here is to practice and to build something actually useful. The “useful” part is:

All three buttons will report their state to home assistant. The KVM I have uses a 3.5mm button which works as a momentary switch. The “switch inputs” bit will work as a momentary switch. One of the pins of ESP32 will connect to an optocoupler which will in turn pretend to be the original KVM button.

Hardware #

Tools #

These are tools that I (now) have in the toolbox and some comments on them.

If provided, none of the links are affiliate.

Materials for the circuit #

The circuit #

Rough idea is to use a single ESP32 pin (GPIO34) as ADC input. It’s connected to 3v3 via a pull-up resistor so that when no buttons are pressed, reading will be ~3 volts.

Each button has a resistor in series with values chosen so that so that ADC can see any combination of buttons pressed. E.g.:

For toggling the KVM switch, I used an optocoupler with a 220 Ohm resistor connected to GPIO32. I chose this pin because it can function as an output (unlike, say, GPIO35).

The connection is:

  1. GPIO32 to 220 Ohm to anode
  2. Cathode to ground
  3. Collector to tip
  4. Emitter to sleeve

Schematically:

And here it is, in its janky glory:

Having always been fascinated with the blinkenlights, I dub thee “das Schaltenknopken 1.0”.

The optocoupler is hiding below the 3.5mm breakout board, the wire going to the top connects to the KVM switch. Pin and ground connections for the optocoupler are below the perfboard.

This is by no means a final version, but I wanted something less bulky than a breadboard on my table.

Software #

The main logic (“if button 1 is pressed, do the thing”) is implemented in Home Assistant and is trivial. The relevant bits of esphome configuration:

sensor:
  # This sensor will report voltage on the pin and the binary_sensor's will calculate which button is actually pressed
  - platform: adc
    pin: GPIO34
    id: voltage_ladder
    update_interval: 0.1s 
    internal: true
    attenuation: 11db # range is  up to 3.3 => needs 11db attenuation
    filters:
      # Apply a debouncing filter. The value will only update
      # after the voltage has been stable for 50ms.
      - debounce: 50ms

binary_sensor:
  # The binary sensors will report the state of the buttons
  # Empirical voltage values:
  # # 1 solo: 2.73
  # 1+2: 1.51
  # 1+2+3: 1.25
  # 1+3: 1.99
  # 2 solo: 1.67
  # 2+3: 1.36
  # 3 solo: 2.28
  # The lambdas will check if the value is within a ~0.2 neighborhood of the value above

  # Button 1 (leftmost)
  - platform: template
    name: "Button 1"
    id: button_1
    # not specifying on_press, that code is managed through nix
    lambda: |-
      float voltage = id(voltage_ladder).state;
      // Solo
      // 1+2
      // 1+3
      // 1+2+3
      return (voltage >= 2.72 && voltage <= 2.745) ||
             (voltage >= 1.50 && voltage <= 1.52) ||
             (voltage >= 1.98 && voltage <= 2.00) || 
             (voltage >= 1.24 && voltage <= 1.26);

  # Button 2 (middle)
  - platform: template
    name: "Button 2"
    id: button_2
    # not specifying on_press, that code is managed through nix
    lambda: |-
      float voltage = id(voltage_ladder).state;
      // Solo
      // 1+2
      // 2+3
      // 1+2+3
      return (voltage >= 1.655 && voltage <= 1.685) ||
             (voltage >= 1.50 && voltage <= 1.52) ||
             (voltage >= 1.35 && voltage <= 1.37) ||
             (voltage >= 1.24 && voltage <= 1.26);

  # Button 3 (right)
  - platform: template
    name: "Button 3"
    id: button_3
    # not specifying on_press, that code is managed through nix
    lambda: |-
      float voltage = id(voltage_ladder).state;
      // Solo
      // 1+3
      // 2+3
      // 1+2+3
      return (voltage >= 2.265 && voltage <= 2.295) ||
             (voltage >= 1.98 && voltage <= 2.00) || 
             (voltage >= 1.35 && voltage <= 1.37) || 
             (voltage >= 1.24 && voltage <= 1.26);

output:
  - platform: gpio
    pin: 32
    id: kvm_press_output

button:
  - platform: template
    name: "KVM Switch Button"
    on_press:
      - output.turn_on: kvm_press_output
      - delay: 200ms
      - output.turn_off: kvm_press_output

Miscellaneous notes #

Future #

Longer-term I am planning to install this project under my main monitor that has USB ports on the bottom side. The display will provide power and the cable going to the KVM will be tucked away behind the monitor. Since I am using only a couple of pins, I can switch to a small version of ESP32 to save on space. The whole board can be in a tiny 3d-printed enclosure.

Links #

Tags