Macro keyboard, Using a micro computer for second monitor - part 3

09 Oct 2023

USB Switch and Barrier works great. But some times you just need to do some quick change, like volume or play/pause or next song etc. To not interrupt my current workflow or gaming session on my main computer I bought a 12 key with 2 rotary encoders macro keyboard.

The Macro keyboard

Macro Keyboard
Macro Keyboard
The macro keyboard features 12 programmable buttons and two rotary encoders with buttons. The rotary encoders will emulate a button press on each “click” when you turn the knob. You can bind one key press for each directon and a third key press for pushing down the knob.

There is a lot of different sellers of the same type of macro keyboard on Amazon. It was delivered in a no-name box and small folder with a QR code to download software, which did not work..

When connected to a Linux machine it’s identified as

Bus 001 Device 002: ID 1189:8890 Acer Communications & Multimedia CH57x

It will not work out of the box because the buttons are not programmed from factory.

Programming the Macro keyboard, (windows)

Unfortunately you have to use Windows to program the macro keyboard. But the programming will be stored in the device and will not require any software to use onced it is programmed. So you can program the device once to use some wierd hotkey combination you would not normally use. This way you can bind that wierd hotkey combination in your Desktop Environment. You can also program the buttons to use the standard Media hotkeys to control play/paus/next,volume etc.

Some screenshots from the software I used

When the device is connected you will se the current keybind in the top right fields above the “Download” and “Cancel” buttons.

To program the keyboard you select a button from the top left buttons and then you use the lower part to select what keybind will be sent to the computer when you press that button. The middle part is for the rotary encoders, there are 3 in the software but my keyboard only have two so the third is not used and will be ignored.

I don’t know what the layer feature is that is in the top right of the software. I first thought it was so you could use one key on the macro keyboard to switch between layers, but I have not found any keybind for that.

Software
Keyboard software
First view of the normal keyboard keys

Software
Keyboard software
To add ctrl/alt/shift/win key modifier to your keybind

Software
Keyboard software
Multimedia keys

Software
Keyboard software
Buttons for LED lights, I have not tested this but I assume there is some built in RGB Led that will illuminate the transparent acrylic glass on the keyboard.

Software
Keyboard software
Control mouse movement.

Software
Keyboard software
Delay, I have not tested this and I assume this will just add a global delay for the buttons.

My macro keyboard scripts

To be able to control the stuff I want on my secondary machine I have made some scripts. What I want to control is

Start/Stop Applications:

  • Chrome
  • Spotify
  • Discord

Media control (play/pause/next) Volume control, mic mute button.

Media keys are easy, because I just bound the default media keys to some of the buttons and the same for main Volume. For the mic mute I just bound one of my hotkeys to do that in KDE.

My macro keyboard has two rotary encoders and I use one for Volume. The second one I will use for Application volume so I can control the volume for Spotify without affecting the other volumes.

Start/Stop applications

I did not just want a simple Start Spotify button, I want to be able to close it as well. So I made a run script for those applications I needed.

This is my script for starting or closing Google Chrome

#!/bin/bash

RUNNING=$(pidof chrome)

function notify() {
  echo "$@"
  notify-send -a "run_chrome.sh" "$@"
}


if [ "$RUNNING" != "" ]; then
  notify "Chrome is running, closing Chrome"
  PID=$(echo "$RUNNING" | cut -f 1 -d ' ')

  while true; do
    #echo "Trying to kill $PID"
    #kill -TERM $PID
    killall chrome
    sleep 2

    PIDLIST=$(pidof chrome)
    PID=$(echo "$PIDLIST" | cut -f 1 -d ' ')
    if [ "$PID" == "" ]; then
      break;
    fi
  done
else
  notify "Starting Google Chrome"
  /usr/bin/google-chrome-stable
fi

This script will check if chrome is running using the pidof command, and if it is running it will run killall chrome and wait 2 seconds and check if it’s still running or not. If you run the script and it can’t find any chrome process it will start chrome.

I’m using notify-send command to send notifications to my desktop environment so I know that the button press actually worked. So I know if the application doesn’t work I have to verify my script or something.

Application Volume controller

I don’t want to bind my application specific volume controller to one application, like Spotify for example. I want a bit more control and the rotary encoder has a built in push button so I want to use that one to switch application between Chrome and Spotify (and maybe more in the future).

So I just had to make another bash script for this, I’m using pactl to control the volume and once again I’m using notify-send so I know what’s happening in my script.

My script is also using KDE specific notification for application volume controller.

#!/bin/bash

# Application settings file
APP_CONF=$HOME/.config/app_volume.conf

# Default config just in case ~/.config/app_volume.conf doesn't have anything
APPS=( "spotify" )

# Cache for selected application name
APP_VOL_CONF=$HOME/.cache/app_volume.tmp.conf

# Source config file
if [ -f $APP_CONF ]; then
  source $APP_CONF
fi

# Source cache for selected application
if [ -f $APP_VOL_CONF ]; then
  source $APP_VOL_CONF
fi

# if no app_name is selected (in APP_VOL_CONF) select a default one
if [ "$app_name" == "" ]; then
  echo "No default application"
  app_name=${APPS[0]}
  echo "Set default: $app_name"
fi

##############################
#
# notifyError
# Send error message to both console and using notify-send
#
function notifyError() {
  echo "$@"
  notify-send -a "app_volume.sh" "$@"
}

##############################
#
# parsePactl
# extract the current state and find the sink number from pactl
#
function parsePactl() {
  SINK=''
  while read line; do
    sink_num_check=$(echo "$line" |sed -rn 's/^Sink Input #(.*)/\1/p')
    if [ "$sink_num_check" != "" ]; then
        #echo "SINK: $sink_num_check"
        cur_sink=$sink_num_check
    fi

    volume_check=$(echo $line | awk '/Volume:/ {printf "%s", $5}' | sed 's/%//' )
    if [ "$volume_check" != '' ]; then
      #echo "VOL: $volume_check"
      VOLUME=$volume_check
    fi

    app_name_check=$(echo "$line" |sed -rn 's/application.name = "([^"]*)"/\1/p')
    if [ "$app_name_check" == "$app_name" ]; then
       #echo "Found app"
       SINK=$cur_sink
       break
    fi
  done < <(pactl list sink-inputs | grep -e "Sink Input" -e application.name -e Volume:)
}

##############################
#
# get_volume
# Get the current volume for a specific sink
#
function get_volume() {
  SINK="$1"
  #echo "getVolume for sink $SINK"

  if [ "$SINK" == "" ]; then
    notifyError "No \$SINK defined for get_volume"
    exit
  fi

  sink_found=''

  while read line; do
    sink_num_check=$(echo "$line" |sed -rn 's/^Sink Input #(.*)/\1/p')
    #echo "sink_num_check: $sink_num_check"
    if [ "$sink_num_check" == "$SINK" ] && [ "$sink_found" == '' ]; then
      sink_found='true'
    fi

    #echo "sink_found=$sink_found"

    if [ "$sink_found" != '' ]; then
      #echo "line: $line"
      volume_check=$(echo $line | awk '/Volume:/ {printf "%s", $5}' | sed 's/%//' )
      if [ "$volume_check" != '' ]; then
        echo "$volume_check"
        break
      fi
    fi
  done < <(pactl list sink-inputs)

}

##############################
#
# app_toggle
# Switch between applications for the volume controller
#
function app_toggle() {
  SELECTED="$1"

  if [ ! -v APPS[@] ]; then
    notifyError "Failed to switch application. APPS array is not defined in $APP_CONF"
    exit
  fi

  for i in ${!APPS[@]}; do
    if [ "${APPS[$i]}" == "$SELECTED" ]; then
      NEXT=$((i+1))
      break;
    fi
  done

  SIZE=${#APPS[@]}

  if [ $NEXT -lt $SIZE ]; then
    echo "Selected: ${APPS[$NEXT]}"
    notify-send -u low -t 1500 --app-name="app_volume.sh" "Selected: ${APPS[$NEXT]}"

    # Cache current application
    echo "app_name=\"${APPS[$NEXT]}\"" > $APP_VOL_CONF
  else
    echo "Selected: ${APPS[0]}"
    notify-send -u low -t 1500 --app-name="app_volume.sh" "Selected: ${APPS[0]}"

    # Cache current application
    echo "app_name=\"${APPS[0]}\"" > $APP_VOL_CONF
  fi
}

#################################
#
# notify_volume
# send a notification of the current Application volume level
#
function notify_volume() {

  case "$app_name" in
    "Google Chrome")
      qdbus org.freedesktop.Notifications /org/kde/osdService org.kde.osdService.mediaPlayerVolumeChanged $VOLUME "Google Chrome" google-chrome
    ;;
    "spotify")
      qdbus org.freedesktop.Notifications /org/kde/osdService org.kde.osdService.mediaPlayerVolumeChanged $VOLUME "Spotify" com.spotify.Client
    ;;
    *)
      qdbus org.freedesktop.Notifications /org/kde/osdService org.kde.osdService.mediaPlayerVolumeChanged $VOLUME "$app_name" speaker
    ;;
  esac


#  notify-send -i speaker \
#              -a "App_Volume" \
#              -h int:value:$1 \
#              -u low \
#              -t 1500 \
#              -h string:x-canonical-private-synchronous:my-notification "Volume: $VOLUME%"
}

case "$1" in
  "up")
    parsePactl "$app_name"
    echo "SINK: $SINK"
    if [ "$SINK" == "" ]; then
      notifyError "No sink for $app_name was found"
      exit
    fi

    VOLUME=$(($VOLUME + 5))
    pactl set-sink-input-volume $SINK +5%
    notify_volume $VOLUME
  ;;
  "down")
    parsePactl "$app_name"
    echo "SINK: $SINK"
    if [ "$SINK" == "" ]; then
      notifyError "No sink for $app_name was found"
      exit
    fi

    VOLUME=$(($VOLUME - 5))
    pactl set-sink-input-volume $SINK -5%
    notify_volume $VOLUME
  ;;

  "toggle")
    app_toggle "$app_name"
  ;;
  *)
    echo "unknown command"
  ;;
esac