2020-04-12

Bluetooth:Example How To Use "bluetoothctl" in Scripts

Here an example of how to use "bluetoothctl" in a bash scripts.

I needed a possibility to programmatically control the Bluetooth device on a raspberry pi.
(At least, list the discovered Bluetooth devices around.)

The only command line method/program I've found, that reliably works on Linux was "bluetoothctl".
But it is intended to be used interactively.
In order to overcome this challenge, I've used co-processes.

This script resets the Bluetooth local device and lists the discovered Bluetooth devices around.
(I've noticed that devices are easier discovered if one of the Bluetooth devices around is re-enabled.)

This script is intended to be only a starting point for the development of your own scripts.

#!/bin/bash
#===============================================================================
# Uses "bluetoothctl" in order to query bluetooth devices around.
# Starts "bluetoothctl" as coprocess, communicating with it simulating interative input.
# See https://www.gnu.org/software/bash/manual/html_node/Coprocesses.html#Coprocesses
# An alternative implementations would be:
#  - the usage of named pipes:
#    See https://en.wikipedia.org/wiki/Named_pipe
#  - or perhaps using expect:
#    See https://www.thegeekstuff.com/2010/10/expect-examples/
#===============================================================================


# ---- Constants ----
typeset -r C_MAX_TRIES=999
typeset -r C_WAIT_SECS=2


# ---- Global variables ----
typeset g_output=""
typeset g_result=""
typeset g_separator=""
typeset g_device=""

let g_count=0


#----------------------------------
# Functions
#----------------------------------

sendToCOPROC () {
  local send_cmd="${1}"
  echo -e "I: Sending to COPROC\n-----\n${send_cmd}\n-----\n"
  # See https://www.gnu.org/software/bash/manual/html_node/Coprocesses.html#Coprocesses
  echo -e "${send_cmd}\n" >&"${COPROC[1]}"
}


showHedxump () {
    echo "D:Hexdump BEGIN ---------------------"
    echo -e "${1}" | hd
    echo "D:Hexdump END -----------------------"
}

#------------------------------------------------------------------------------
# The 1st parameter must be the name of a variable that will receive the output
# of this function.
# If 2nd parameter (regular expression pattern) is given, "receiveFromCOPROC()"
# will return only the lines that match the given pattern.
#
# ATTENTION
#   This function must run in this same shell because of:
#     read -t 2 output <&"${COPROC[0]}
#   e.g. Do NOT call this function in this way:
#          outputVar="$(receiveFromCOPROC)"
#        This function must be called in this way: 
#          receiveFromCOPROC outputVar []
#-----------------------------------------------------------------------------------------------
receiveFromCOPROC () {
  if [[ -z "$1" ]]; then
     >&2 echo -e "\nE: receiveFromCOPROC() Missing first parameter"
     >&2 echo -e "   This parameter is a variable that will 'receive' the output of this function\n"
     exit 1
  fi
  local regexpPattern=""
  if [[ -n "$2" ]]; then
    regexpPattern="$2"
  fi
  echo "D: receiveFromCOPROC() Receiving COPROC output into variable '$1'"
  # "recFro_COPROC" is a reference to the variable given as 1st argument.
  # I am using such a complicated variable name, in order to avoid naming conflicts.
  # The output of "bluetoothctl" will be writen to "recFro_COPROC" and so also
  # to the referenced variable given as 1st parameter to this function.
  declare -n recFro_COPROC=$1
  local output=""
  local lineQty=0
  while [[ 1 -eq 1 ]]; do
    # See https://www.gnu.org/software/bash/manual/html_node/Coprocesses.html#Coprocesses
    read -t 2 output <&"${COPROC[0]}"
    # Remove color codes, carriage return characters and "[bluetooth]# " from bluetoothctl output
    output="$(sed 's/\x1B\[[0-9;]*[JKmsu]//g; s/\r//g; s/\[bluetooth\]# *//g' <<< "${output}")"
    if [[ -n "${regexpPattern}" ]]; then
      output="$(egrep -e "${regexpPattern}" <<< "${output}")"
    fi
    #echo -e "receiveFromCOPROC() output: ${output}"
    if [[ -z "${output}" ]]; then
      break
    fi
    if [[ ${lineQty} -eq 0 ]]; then
      recFro_COPROC="${output}"
    else
      recFro_COPROC="${recFro_COPROC}\n${output}"
    fi
    #echo "${output}"
    ((lineQty++))
  done
  #if [[ ${lineQty} -gt 0 ]]; then
  #  echo -e "D: receiveFromCOPROC() Received:\n####\n${recFro_COPROC}\n####\n"     
  #fi
}

doSleep () {
  echo "I: Sleep ${1} secs ..."
  sleep ${1}
}


#---------------------------------------
# Echoes an empty string if Ok
# Otherwise the error message
#---------------------------------------
isBluetoothServiceOk () {
  systemctl status bluetooth | grep "Active: failed"
}

repairIfNeeded () {
  typeset res="$(isBluetoothServiceOk)"
  if [[ -n "${res}" ]]; then
    echo "I: Bluetooth service has a problem. Restarting bluetooth service"
    systemctl restart bluetooth
    res="$(isBluetoothServiceOk)"
    if [[ -n "${res}" ]]; then
      echo "I: Bluetooth service still has a problem. Abort"
      exit 1
    else
      echo "I: Bluetooth service is OK now"
    fi
  else
    echo "I: Bluetooth service is OK"
  fi
}

isBluetoothCtlRunning () {
  pgrep -c "bluetoothctl"
}

killBluetooth () {
  echo "I: Killing bluetoothctl"
  pkill "bluetoothctl"
}

flushCOPROC () {
  local fl_output
  >&2 echo "D: flushCOPROC ()"
  receiveFromCOPROC fl_output
  echo -e "D: flushing\n++++"
  while [[ -n "$(egrep -e " Controller |Agent |power on|scan on|Discovery" <<< ${fl_output})" ]]; do
    receiveFromCOPROC fl_output
    echo "${fl_output}"
  done
  echo "++++"
}


listDevices () {
  local ld_output
  echo "D: listDevices ()"
  sendToCOPROC "devices"
  doSleep 1
  receiveFromCOPROC ld_output
  echo -e "D: Listing devices\n*****"
  while [[ -n "$(egrep -e " Device " <<< ${ld_output})" ]]; do
    receiveFromCOPROC ld_output
    echo "${ld_output}"
  done
  echo "*****"
}


sendCmdWaitAndShowOutput () {
  local cmd="${1}"
  local sleepTime="${2}"
  local sCWASO_output
  sendToCOPROC "${cmd}"
  doSleep "${sleepTime}"
  receiveFromCOPROC sCWASO_output
}



#============= Main ==============
if [[ "$(whoami)" != "root" ]]; then
  echo "E: Wrong user. This script must be started as root"
  exit 1
fi

if [[ $(isBluetoothCtlRunning) -gt 0 ]]; then
  echo "I: bluetoothctl is already started"
  killBluetooth
  #echo "I: systemctl restart bluetooth"
  #systemctl restart bluetooth
#else
#  repairIfNeeded
fi
echo "I: Restarting bluetooth to be sure that is works correctly"
systemctl restart bluetooth
doSleep 2

# See https://www.gnu.org/software/bash/manual/html_node/Coprocesses.html#Coprocesses
echo "I: Starting 'bluetoothctl' as coprocess"
coproc COPROC { bluetoothctl; }
doSleep 1

# Cleanup on exit (whenever the exit occurs)
trap '
  echo "I: Exit trap called"
  sendToCOPROC "exit"
  doSleep 3

  if [[ $(isBluetoothCtlRunning) -gt 0 ]]; then
    echo "E: NOK - bluetoothctl is still runnning"
    killBluetooth
  else
    echo "I: OK - bluetoothctl is stopped"
  fi
' EXIT


if [[ $(isBluetoothCtlRunning) -gt 0 ]]; then
  echo "I: OK - bluetoothctl is started"
else
  echo "E: NOK - bluetoothctl not started"
  exit 2
fi

sendCmdWaitAndShowOutput "power on" 1
sendCmdWaitAndShowOutput "discoverable on" 1
sendCmdWaitAndShowOutput "scan on" 1
#sendCmdWaitAndShowOutput "show" 1

#doSleep 10
#flushCOPROC


while [[ ${g_count} -lt ${C_MAX_TRIES} ]]; do
  ((g_count++))
  sendToCOPROC "devices"
  doSleep 1
  g_result=""
  g_separator=""
  while [[ 1 -eq 1 ]]; do
    g_output=""
    receiveFromCOPROC g_output "Device " 
    if [[ -n "${g_output}" ]]; then
      g_result="${g_result}${g_separator}${g_output}"
      g_separator="\n"
    else
      break
    fi
  done
  if [[ -n "${g_result}" ]]; then
    echo -e "I: Found bluetooth device(s)\n-------\n${g_result}\n-------\n"
    g_result="$(echo -e "${g_result}" | sort -u)"
    echo -e "I: Found bluetooth device(s) after sort removing duplicates\n-------\n${g_result}\n-------\n"
    break
  else
    echo "E: No bluetooth devide found"
  fi
done

#listDevices

sendToCOPROC 'scan off'


Output example:
# ./btscanWithCoproc.bash 
I: Restarting bluetooth to be sure that is works correctly
I: Sleep 2 secs ...
I: Starting 'bluetoothctl' as co-process
I: Sleep 1 secs ...
I: OK - bluetoothctl is started
I: Sending to COPROC
-----
power on
-----

I: Sleep 1 secs ...
D: receiveFromCOPROC() Receiving COPROC output into variable 'sCWASO_output'
I: Sending to COPROC
-----
discoverable on
-----

I: Sleep 1 secs ...
D: receiveFromCOPROC() Receiving COPROC output into variable 'sCWASO_output'
I: Sending to COPROC
-----
scan on
-----

I: Sleep 1 secs ...
D: receiveFromCOPROC() Receiving COPROC output into variable 'sCWASO_output'
I: Sending to COPROC
-----
devices
-----

I: Sleep 1 secs ...
D: receiveFromCOPROC() Receiving COPROC output into variable 'g_output'
E: No bluetooth devide found
I: Sending to COPROC
-----
devices
-----

I: Sleep 1 secs ...
D: receiveFromCOPROC() Receiving COPROC output into variable 'g_output'
D: receiveFromCOPROC() Receiving COPROC output into variable 'g_output'
D: receiveFromCOPROC() Receiving COPROC output into variable 'g_output'
I: Found bluetooth device(s)
-------
Device 80:**:**:**:**:B9 Galaxy S9
Device 30:**:**:**:**:96 30-**-**-**-**-96
Device 80:**:**:**:**:B9 Galaxy S9
Device 30:**:**:**:**:96 30-**-**-**-**-96
-------

I: Found bluetooth device(s) after sort removing duplicates
-------
Device 30:**:**:**:**:96 30-**-**-**-**-96
Device 80:**:**:**:**:B9 Galaxy S9
-------

I: Sending to COPROC
-----
scan off
-----

I: Exit trap called
I: Sending to COPROC
-----
exit
-----

I: Sleep 3 secs ...
I: OK - bluetoothctl is stopped
I hope this helps someone.
Andreas