summaryrefslogtreecommitdiff
path: root/bluetooth-config
blob: daaf074c0b2fa5b9dcaaff42d834f97ce5681566 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
#!/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 bdaddresses retry

[ $( id -u ) -eq 0 ] || exerr "$0 must 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] [-a address] 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 its 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 set up stack,
  # ask the user if we shall start it up
  if ! [ "${found}" ]; then
    printf "No usable Bluetooth host controllers were found.\n"
    printf "These 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 set up 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 devic%.*s (scanning for names):\n" ${#count} ${#count} es

  # 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, do not 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 device 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 "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 cannot 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.\nThis daemon manages pairing 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 "To modify pairing information, edit this file and run\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 "(To get PIN, 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 "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."
      printf "\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 | \
      /usr/bin/grep "${bdaddress}" )
    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 | \
        grep "${bdaddress}" )
      if ! [ "${bthidd_known}" ]; then
        printf "failed.\n"
      else
        printf "success.\nTo re-read its config, bthidd must be restarted.\n"
        printf "Warning: If a Bluetooth keyboard is being used, the connection might be lost.\n"
        printf "It can be manually restarted later with\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