first shot at user friendly bluetooth-config script

Dirk Engling erdgeist at erdgeist.org
Sat Sep 26 23:29:36 UTC 2015


Find attached a script that tries to tie together essential parts of the
bluetooth sub-system in a human friendly manner.

Currently I implemented all the steps necessary to get a hid running,
and helping out the user along the way. Next up I want to implement
setting up everything necessary to get connected using the DUN profile,
if available.

Still some questions remain about pairing: besides grepping debug.log's
output, is there a reliable way to get pairing status? Looking into
/var/db/hcsecd.keys may give me positive results, but info like whether
a PIN_Code_Negative_Reply has been sent to the device because of a wrong
pin code would be helpful. It would also be helpful to know if the
device never sent a PIN_Code_Request in the first place. And how do you
use hcsecd to actively start pairing?

The script's style may still be a little concise, but is extensively
documented, I will factor out code blocks into functions later. I am
also trying to make it more usable from a script by adding a quiet mode
that is not interactive and tries to automatically resolve everything by
the info passed on command line.

Now I wonder, where the code will be heading. Is it likely to go into
the bluetooth framework or should I start writing a port? Do you have
suggestions where the user might be provided additional info on how to
continue, especially if stuff breaks?

Regards,

  erdgeist
-------------- next part --------------
#!/bin/sh

# define our bail out shortcut
exerr () { echo -e "Error: $*" >&2 ; exit 1; }

# Assuming we are called to do the pair-new-device subcommand first

main() {
unset node device started bdaddresses retry

[ $( id -u ) -eq 0 ] || exerr "$0 needs to modify files that belong to root. Re-run as root."

# Get command line options
while getopts :a:n: arg; do
  case ${arg} in
    n) node="$OPTARG";;
    a) device="$OPTARG";;
    ?) exerr "Syntax: $0 [-n node] cmd";;
  esac
done

known_nodes=$(/usr/sbin/hccontrol read_node_list 2>/dev/null | \
    /usr/bin/tail -n +2 | /usr/bin/cut -d ' ' -f 1)

# Check if netgraph knows about any HCI nodes
if ! [ "${known_nodes}" ]; then
  ng_nodes=$(/usr/sbin/ngctl list 2>/dev/null | \
    /usr/bin/grep -o "Name: .* Type: ubt" | /usr/bin/cut -d ' ' -f 2)

  [ "${ng_nodes}" ] || exerr "No bluetooth host controllers found."

  unset found
  for n in ${ng_nodes}; do
    if [ "${n}" = "${node%hci}" ]; then
      # If we found the node but it's stack is not set up, do it now
      /usr/sbin/service bluetooth start ${node%hci} || exit 1
      found="YES"
    fi
  done

  # If we have bluetooth controller nodes without a setup stack,
  # ask the user if we shall start it up
  if ! [ "${found}" ]; then
    printf "No usable bluetooth host controllers were found.\nThese host controllers exist in the system:\n  %s" " ${ng_nodes}"
    read -p "Choose a host controller to set up: [${ng_nodes%% *}]" node
    : ${node:="${ng_nodes%% *}"}
    /usr/sbin/service bluetooth start ${node} || exit 1
  fi

  # Re-read known nodes
  known_nodes=$(/usr/sbin/hccontrol read_node_list 2>/dev/null | \
    /usr/bin/tail -n +2 | /usr/bin/cut -d ' ' -f 1)
  # check if we succeeded in bringing it up
  [ "${known_nodes}" ] || exerr "Failed to setup bluetooth stack"
fi

# if a node was requested on command line, check if it is there
if [ "${node}" ]; then
  unset found
  for n in ${known_nodes}; do
    [ "${n}" = "${node}" ] && found="YES"
    [ "${n}" = "${node}hci" ] && node="${node}hci" && found="YES"
  done
  [ "${found}" ] || exerr "Node ${node} not found"
fi

[ "${node}" ] && node="-n ${node}"

while ! [ "${bdaddresses}" ]; do
  retry=X${retry}
  printf "Scanning for new bluetooth devices (Attempt %d of 5) ... " ${#retry}
  bdaddresses=$( /usr/sbin/hccontrol -N ${node} inquiry 2>/dev/null | \
    /usr/bin/grep -o "BD_ADDR: .*" | /usr/bin/cut -d ' ' -f 2 )

  # Count entries and, if a device was requested on command line,
  # try to find it
  unset found count
  for bdaddress in ${bdaddresses}; do
    count=X${count}
    if [ "${bdaddress}" = "${device}" ]; then
      found=YES
      bdaddresses="${device}"
      count=X
      break
    fi
  done

  # If device was requested on command line but is not found,
  # or no devices found at all, rescan until retry is exhausted
  if ! [ "${found}" -o "${count}" -a -z "${device}" ]; then
    printf "failed.\n"
    if [ "${#retry}" -eq 5 ]; then
      [ "${device}" ] && exerr "Device ${device} not found"
      exerr "No new bluetooth devices found"
    fi
    unset bdaddresses
    sleep 2
    continue
  fi

  printf "done.\nFound %d new bluetooth device(s) (scanning for names):\n" ${#count}

  # Looping again for the faster feedback
  unset count
  for bdaddress in ${bdaddresses}; do
    count=X${count}
    bdname=$( /usr/bin/bthost -b "${bdaddress}" 2>/dev/null )
    friendlyname=$( /usr/sbin/hccontrol Remote_Name_Request ${bdaddress} 2> /dev/null | \
      /usr/bin/grep -o "Name: .*" | /usr/bin/cut -d ' ' -f 2- )

    # sdpcontrol should be able to pull vendor and product id via sdp
    printf "[%2d] %s\t\"%s\" (%s)\n" ${#count} "${bdaddress}" "${friendlyname}" "${bdname}"

    eval bdaddress_${#count}=\${bdaddress}
    eval bdname_${#count}=\${bdname}
    eval friendlyname_${#count}=\${friendlyname}
  done

  # If a device was pre-selected, don't query the user
  [ "${device}" ] && topair=1 || unset topair

  # Even if only one device was found, user may chose 0 to rescan
  while ! [ "${topair}" ]; do
    read -p "Select which device you want to pair with [1-${#count}, 0 to rescan]: " topair
    if ! [ "${topair}" -ge 0 -a "${topair}" -le "${#count}" ] 2>/dev/null ; then
      printf "Value out of range: %s.\n" {topair}
      unset topair
    fi
  done

  [ "${topair}" -eq "0" ] && unset bdaddresses retry
done

eval bdaddress=\${bdaddress_${topair}}
eval bdname=\${bdname_${topair}}
eval friendlyname=\${friendlyname_${topair}}

# Do we need to add an entry to /etc/bluetooth/hosts?
if ! [ "${bdname}" ]; then
  printf "\nAdding device ${bdaddress} to /etc/bluetooth/hosts.\n"

  while ! [ "${bdname}" ]; do
    read -p "Please enter friendly name. [${friendlyname}]: " REPLY
    : ${REPLY:="${friendlyname}"}

    if [ "${REPLY}" ]; then
      # Remove white space and non-friendly characters
      bdname=$( printf "%s" "${REPLY}" | tr -c '[:alnum:]-,.' _ )
      [ "${REPLY}" != "${bdname}" ] && printf "Notice: Using sanitized name \"%s\" in /etc/bluetooth/hosts.\n" "${bdname}"
    fi
  done

  printf "%s\t%s\n" "${bdaddress}" "${bdname}" >> /etc/bluetooth/hosts
fi

# If scanning for the name did not succeed, resort to bdname
: ${friendlyname:="${bdname}"}

# now over to hcsecd

# Since hcsecd does not allow querying for known devices, we need to
# check for bdaddr entries manually.
#
# Also we can not really modify the PIN in an existing entry. So we
# need to prompt the user to manually do it and restart this script
if ! /usr/sbin/service hcsecd enabled; then
  printf "\nWarning: hcsecd is not enabled on your system.\nThis daemon manages paring requests.\n"
  read -p "Enable hcsecd? [yes]: " REPLY
  case "${REPLY}" in no|n|NO|N|No|nO) ;; *) /usr/sbin/sysrc hcsecd_enable="YES";; esac
fi
secd_config=$( /usr/sbin/sysrc -n hcsecd_config )
secd_entries=$( /usr/bin/grep -Eo "bdaddr[[:space:]]+(${bdaddress}|${bdname})" ${secd_config} | awk '{ print $2; }' )

if [ "${secd_entries}" ]; then
  printf "\nWarning: An entry for device %s is already present in %s.\n" ${secd_entries} ${secd_config}
  printf "If you want to modifiy pairing information, edit this file and run the command\n  service hcsecd restart\n"
  read -p "Continue? [yes]: " REPLY
  case "${REPLY}" in no|n|NO|N|No|nO) exit;; esac
else
  printf "\nWriting pairing information description block to %s.\n" ${secd_config}
  printf "(Notice: To get PIN, you might want to put device in pairing mode, first.)\n"
  read -p "Enter PIN [nopin]: " pin
  [ "${pin}" ] && pin=\""${pin}"\" || pin="nopin"

  # Write out new hcsecd config block
  printf "\ndevice {\n\tbdaddr\t%s;\n\tname\t\"%s\";\n\tkey\tnokey\;\n\tpin\t%s\;\n}\n" \
    "${bdaddress}" "${friendlyname}" "${pin}" >> ${secd_config}

  # ... and make daemon reload config, TODO: hcsecd should provide a reload hook
  /usr/sbin/service hcsecd restart

  # TODO: we should check if hcsecd succeeded pairing and revert to an old version
  # of hcsecd.conf so we can undo adding the block above and retry with a new PIN
  # also, if there's a way to force devices to re-pair, try this
fi

# now check for specific services to be provided by the device
# first up: HID

if /usr/sbin/sdpcontrol -a "${bdaddress}" search HID | \
   /usr/bin/grep -q "^Record Handle: "; then

  printf "\nThis device provides human interface device services.\n"
  read -p "Do you want to set it up? [yes]: " REPLY
  case "${REPLY}" in no|n|NO|N|No|nO) ;;
  *)
    if ! /usr/sbin/service bthidd enabled; then
      printf "\nWarning: bthidd is not enabled on your system.\nThis daemon manages bluetooth HID devices.\n"
      read -p "Enable bthidd? [yes]: " REPLY
      case "${REPLY}" in no|n|NO|N|No|nO) ;; *) /usr/sbin/sysrc bthidd_enable="YES";; esac
    fi

    # Check if bthidd already knows about this device
    bthidd_known=$( /usr/sbin/bthidcontrol -a "${bdaddress}" known )
    if [ "${bthidd_known}" ]; then
      printf "Notice: Device %s already known to bthidd.\n" "${bdaddress}"
    else
      bthidd_config=$( /usr/sbin/sysrc -n bthidd_config )
      printf "Writing HID descriptor block to %s ... " "${bthidd_config}"
      /usr/sbin/bthidcontrol -a "${bdaddress}" query >> "${bthidd_config}"

      # Re-read config to see if we succeeded adding the device
      bthidd_known=$( /usr/sbin/bthidcontrol -a "${bdaddress}" known )
      if ! [ "${bthidd_known}" ]; then
        printf "failed.\n"
      else
        printf "success.\nIn order to re-read its config, bthidd must be restarted.\n"
        printf "Warning: If you're using a bluetooth keyboard, connection may be lost.\n"
        printf "You may manually restart it later using\n  service bthidd restart\n"
        read -p "Restart bthidd now? [yes]: " REPLY
        case "${REPLY}" in no|n|NO|N|No|nO) ;; *) /usr/sbin/service bthidd restart;; esac
      fi
    fi
  ;;
  esac
fi

}

# After function definitions, main() can use them
main "$@"

exit

# TODO
# * If device is a keyboard, offer a text entry test field and if it does
#   not succeed, leave some clues for debugging (i.e. if the node responds
#   to pings, maybe switch keyboard on/off, etc)
# * Same if device is a mouse, i.e. hexdump /dev/sysmouse.
# * If device offers DUN profiles, ask the user if an entry in
#   /etc/ppp/ppp.conf should be created
# * If OPUSH or SPP is offered, refer to the respective man pages to give
#   some clues how to continue



More information about the freebsd-bluetooth mailing list