No internet connection
  1. Home
  2. General

Controlling USB HID Relay with TinyPilot for KVM Switching, CPU Restart, etc.

By @barrym
    2023-08-01 19:44:26.084Z2023-08-02 18:51:45.494Zassigned to
    • @david

    Just sharing some tweaking I've been doing with my TinyPilot stuff, with the hope it might help others.
    For context, I'm primarily a Windows user and not a Linux/Python/Ansible/Bash person, but I own a couple of Pis to learn on. I can usually read source code/scripts/traces, sort of understand how GitHub works, and can generally piece how stuff should fit together.

    Preface

    I use my TinyPilot with a multi-monitor CPU and an additional HDMI KVM switch to toggle back and forth between outputs into the TinyPilot. Since I'm connected with both HDMI paths to the same machine and use the built-in "Ctrl-Ctrl-1/2" hotkey support to switch inputs, in order for this setup to work I have to run 2 USB lines from the KVM to the same PC. This does work, but it's has some drawbacks.

    The external KVM's "keyboard input" port only supports USB 1.1 connections and doesn't work with Mass Storage Drive support, which is a major use case for how I'd like to use these IPKVM boxes. I wanted a way to switch inputs without doing multiple USB connections to the same machine, but was stuck.

    The external KVM does have a 3.5mm headphone jack for external switching I could leverage to do this. Problem there is I don't really enjoy hardware hacking on finished products. Trying to lash something onto the PI's GPIO ports and cut into the case of my Voyager 2 was not appealing. If I have to I could, but there has to be a better option.

    Instead, I learned recently about usbrelay and the cheap EBay/AliExpress/Amazon (example) circuit boards with relays mounted to them you can find that it supports. No physical mods and a relay to play with? This I can get behind.

    All we have to do is bring it into TinyPilot. As usual, doing any of this work is at your own risk, you accept all consequences of your actions and indemnify me from them and their results, no warranty of any form, implied or otherwise, for fitness for purpose, design, usage, etc. , observe and respect and abide all safety precautions, this is not legal advice and consult proper conuncil, don't do this on something you need for critical use, don't blame me if it doesn't work, not an endorsement of any products/services/people/businesses mentioned, etc. :-)

    Step 0 - Install a fresh copy of TinyPilot on a new SDCard

    Always make sure you have a fallback position.

    Step 1 - Add usbrelay to TinyPilot

    After getting some of the USB Relay boards from Amazon, I started playing around with them on my other Pi to learn how to talk to them from the command line.

    I ended up using darrylb123's repository of code since it seemed to work better than what I started with.

    Most of these boards now come preloaded with their identifier string always set to "WARNM", so using the code above I was able to switch my test board identifier to "RELAY".

    After giving my TinyPilot temporary access to the Internet and logging in with the pilot user, I just followed the instructions on GitHub how to load the code onto the system. No issues.

    Step 2 - Write a script to trigger the relay

    The instructions for usbrelay note that because this is using a low-level hardware access in Linux, you have to either run the command with sudo or grant permission to the user you're logged in with to make it work. I'm lazy and not too interested in mucking around, so sudo it is.

    I eventually figured out that TinyPilot also does this with the Privileged Scripts for some functions like "Hostname Change", so let's follow that model.

    From the pilot user I got into root via sudo, switched to the directory containing the Privileged Scripts, copied one of the existing scripts to a new file, and then rewrote it.

    sudo -i
    cd /opt/tinpilot-privledged/scripts
    cp read-update-log usbrelay-push-1
    nano usbrelay-push-1
    

    And here's the code for usbrelay-push-1. Remember that I renamed my relay to RELAY, so adjust your name to whatever works for your board.

    #!/bin/bash
    
    # Use the USB Relay 1 trigger to switch the HDMI video
    
    # Exit on unset variable.
    set -u
    
    # Exit on first error.
    set -e
    
    readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd)"
    
    print_help() {
      cat << EOF
    Usage: ${0##*/} [-h]
    Use the USB Relay 1 trigger to switch the HDMI video
      -h Display this help and exit.
    EOF
    }
    
    # Parse command-line arguments.
    while getopts 'h' opt; do
      case "${opt}" in
        h)
          print_help
          exit
          ;;
        *)
          print_help >&2
          exit 1
      esac
    done
    
    # Push the button
    usbrelay RELAY_1=1
    
    # Wait for 2 second
    sleep 2
    
    # Release the button
    usbrelay RELAY_1=0
    

    Run the script from the command shell and make sure it works. You should see the green light on the board solid, the red LED will turn on and off with the relay, you'll hear the clicks, etc.

    3 - Allow TinyPilot to run usbrelay-push-1 under sudo without a password

    This was the hardest thing to figure out, since I don't have much experience with running webservers and whatnot.
    We now have to give the appropriate permissions to make the new script run.

    cd /etc/sudoers.d
    nano tinypilot
    

    Add the following line to to the top of this file:

    tinypilot ALL=(ALL) NOPASSWD: /opt/tinypilot-privileged/scripts/usbrelay-push-1
    

    4 - Modify the TinyPilot application backend to run the script

    Now we have to modify the main Python code to run the new script when we want it. Since I don't really know anything about Python, I'm going to steal as much design and code from existing functions as I can.

    cd /opt/tinypilot/app
    nano api.py
    

    At the top of the file, add this line to the bottom of the section with the Import lines:

    import switchvideo
    

    At the bottom of the file, add this code in:

    @api_blueprint.route('/switchvideo', methods=['POST'])
    def switchvideo_post():
        """Triggers switching the video input signal via USBHID Relay
    
        Returns:
            Empty response on success, error object otherwise.
        """
        try:
            switchvideo.push_the_button()
            return json_response.success()
        except local_system.Error as e:
            return json_response.error(e), 500
    
    

    Create a new Python script to trigger the script we previously created

    nano switchvideo.py
    

    Add this code:

    # Import the external modules
    import subprocess
    
    def push_the_button():
        """Using the USB HID relay known as RELAY_, press the KVM button with Relay 1, waith 2 seconds, then release
    
        Returns:
            Nothing.
        """
        try:
            return subprocess.check_output(
                ['sudo', '/opt/tinypilot-privileged/scripts/usbrelay-push-1'],
                stderr=subprocess.STDOUT,
                universal_newlines=True)
        except:
            return json_response.error(e), 404
    

    All of the files in this directory have to be owned by the tinypilot, so we'll have to fix that for the file we just made.

    chown tinypilot:tinypilot switchvideo.py
    

    5 - Modify the TinyPilot application frontend

    First we modify the menu files.

    cd /opt/tinypilot/app/templates/custom-elements
    nano menu-bar.html
    

    Search the file for this section.

                  <a data-onclick-event="ctrl-alt-del-requested"
                    >Ctrl + Alt + Del</a>
                </li>
              </ul>
            </li>
          </ul>
        </li>
    

    Overwrite it with this blob. You get some additional keyboard combinations as well with this in the menus, but you can edit them out if you want.

                  <a data-onclick-event="ctrl-alt-del-requested"
                    >Ctrl + Alt + Del</a>
                  <a data-onclick-event="alt-tab-requested"
                    >Alt + Tab</a>
                  <a data-onclick-event="shift-printscreen-requested"
                    >Shift + PrintScreen</a>
                </li>
              </ul>
            </li>
            <li class="item">
              <a data-onclick-event="switchvideo-requested">Switch Video</a>
            </li>
          </ul>
        </li>
    

    With the menu items now added, we need something to listen for the user clicking on them.

    cd /opt/tinypilot/app/static/js
    nano app.js
    

    At the top of the file, put this at the bottom of the section with the import statements

    import { sendSwitchVideo } from "./controllers.js";
    

    Search the file for this line

    document.onload = document.getElementById("app").focus();
    

    Keep that code and put this text above that line

    // Send a Switch Video message to the backend
    function sendSwitch() {
      const result = sendSwitchVideo();
    }
    

    Search the file for this line

    menuBar.addEventListener("ctrl-alt-del-requested", () => {
    

    Add this blob after the last leftmost set of }); for that addEventListener function.

    menuBar.addEventListener("alt-tab-requested", () => {
      processKeystroke({
        altLeft: true,
        key: "Alt",
        code: "AltLeft",
      });
      processKeystroke({
        altLeft: true,
        key: "Tab",
        code: "Tab",
      });
    });
    menuBar.addEventListener("shift-printscreen-requested", () => {
      processKeystroke({
        shiftLeft: true,
        key: "Shift",
        code: "ShiftLeft",
      });
      processKeystroke({
        shiftLeft: true,
        key: "Print",
        code: "PrintScreen",
      });
    });
    menuBar.addEventListener("switchvideo-requested", () => {
      // Send a Switch Video request to the USBHID relay device
      sendSwitch();
    });
    

    Now we have to integrate the command to send the events from the frontend to the backend

    nano controllers.js
    

    Add the following to the bottom:

    export async function sendSwitchVideo() {
    
      return fetch("/api/switchvideo", {
        method: "POST",
        headers: { "X-CSRFToken": getCsrfToken(),
        },
        credentials: "same-origin",
        mode: "same-origin",
        cache: "no-cache",
        redirect: "error",
      }).then(processJsonResponse);
    
    }
    

    6 - Reboot and try it out

    If it doesn't work, try setting the TinyPilot DEBUG flag and reading the Python output logs

    7 - Tidy up

    Connect the relay outputs to what you want. Not sure I would trust random hardware from the Internet for anything beyond very low amperage 5V DC.

    A non-conductive housing of some sort for the circuit board is a good idea to protect it and yourself. One of these small project boxes could work, but you have to do a bunch of cutting with a Dremel to make it fit and usable.

    • 2 replies
    1. David @david2023-08-03 11:18:40.572Z2023-08-03 11:35:55.249Z

      Hi @barrym, thanks so much for sharing your detailed write-up!

      I love seeing users tinkering and expanding the functionality of their devices. Adding a relay to the 3.5mm jack is a clever idea - I haven't seen that before, so it's cool to think about the other uses for relays.

      And thanks for sharing your info in those other threads too - I'm sure others will find this super useful too!

      1. In reply tobarrym:

        Thanks so much, @barrym! This is fantastic! Thanks for taking the time to research this and share all these details.