[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[elpa] externals/bluetooth 00497ca 03/57: adds bluetooth.el and .gitigno
From: |
Stefan Monnier |
Subject: |
[elpa] externals/bluetooth 00497ca 03/57: adds bluetooth.el and .gitignore |
Date: |
Thu, 7 Nov 2019 23:28:48 -0500 (EST) |
branch: externals/bluetooth
commit 00497ca5337233da3b760f26c5d874ce2563cf8d
Author: Raffael Stocker <address@hidden>
Commit: Raffael Stocker <address@hidden>
adds bluetooth.el and .gitignore
---
.gitignore | 4 +
bluetooth.el | 501 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 505 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a1f6744
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+*~undo-tree~
+*~
+\#*#
+*.elc
diff --git a/bluetooth.el b/bluetooth.el
new file mode 100644
index 0000000..90839df
--- /dev/null
+++ b/bluetooth.el
@@ -0,0 +1,501 @@
+;;; bluetooth.el --- A Major mode for Bluetooth devices -*- lexical-binding: t
-*-
+
+;; Copyright (C) 2019 Raffael Stocker
+
+;; Author: Raffael Stocker <address@hidden>
+;; Created: 13 Aug 2019
+;; Version: 0.1
+;; Package-Version:
+;; Package-Requires: ((dash) (let-alist) (dbus))
+;; Keywords: hardware
+;; Homepage:
+
+;; This file is NOT part of GNU Emacs
+
+;; This program is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or (at
+;; your option) any later version.
+
+;; This program is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+;; General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+
+;;; Commentary:
+
+;; This package implements basic Bluetooth management functionality, such as
+;; connecting and disconnecting devices, setting properties and aliases,
+;; putting the adapter in discovery mode and controlling its power supply.
+;; It also includes a pairing agent. No configuration is necessary.
+;; It uses the Bluez Bluetooth stack on GNU/Linux via the DBus interface.
+;; Therefore, it requires an Emacs with DBus support compiled-in.
+;;
+;; To use the package, invoke `list-bluetooth-devices'.
+
+;;; Code:
+
+(require 'dbus)
+(require 'cl-lib)
+(require 'let-alist)
+(require 'dash)
+
+(defconst bluetooth-buffer-name "*Bluetooth*" "Name of the buffer in
+which to list bluetooth devices.")
+
+(defconst bluetooth--mode-name "Bluetooth" "Pretty print mode name.")
+
+(defconst bluetooth--mode-info '(:eval (bluetooth--display-state))
+ "Mode info list.")
+
+;;; The state information list defines the kind of adapter state displayed
+;;; in the mode-line. The first element of a sublist is an adapter property,
+;;; the second is the displayed string if the property is non-nil and
+;;; the third is the displayed string if the property is nil. If a
+;;; display element is nil, nothing will be displayed for this property.
+(defvar bluetooth--mode-state '(("Powered" nil "off")
+ ("Discoverable" "discoverable" nil)
+ ("Pairable" "pairable" nil)
+ ("Discovering" "scan" nil))
+ "Mode line adapter state information.")
+
+;;; Bluez service name as defined by the Bluez API
+(defconst bluetooth--service "org.bluez" "D-Bus service name of Bluez.")
+
+;;; Bluez root path as defined by the Bluez API
+(defconst bluetooth--root "/org/bluez" "D-Bus path root for Bluez.")
+
+;;; our path name for the pairing agent
+(defvar bluetooth--own-path (concat dbus-path-emacs "/bluetooth"))
+
+;;; the interface name for the pairing agent
+(defvar bluetooth--own-intf (concat dbus-interface-emacs ".bluetooth"))
+
+(defconst bluetooth--agent-mngr-intf "org.bluez.AgentManager1"
+ "D-Bus interface name for the agent manager")
+
+(defconst bluetooth--agent-intf "org.bluez.Agent1"
+ "D-Bus interface name for the agent")
+
+(defvar bluetooth--method-objects '() "D-Bus method objects.")
+
+;;; API description:
+;;;
+;;; This is a plist of plists, providing API information for the
+;;; implemented D-Bus APIs.
+;;;
+;;; For instance, API :device has path-spec (bluetooth--adapter
+;;; bluetooth--device) and interface "org.bluez.Device1".
+;;;
+;;; The path-spec is a list of functions, defined below, that
+;;; return the designated constituents of the D-Bus path.
+(defvar bluetooth--api-info '(:device
+ (:path
+ (bluetooth--adapter bluetooth--device)
+ :interface "org.bluez.Device1")
+ :adapter
+ (:path
+ (bluetooth--adapter)
+ :interface "org.bluez.Adapter1"))
+ "Bluez D-Bus API information about paths and interfaces.")
+
+;;; The following functions provide the constituents of the path
+;;; spec in `bluetooth--api-info'.
+
+(defun bluetooth--adapter (dev-id)
+ "Return the adapter of DEV-ID."
+ (bluetooth--dev-state "Adapter" (assoc dev-id bluetooth--device-info)))
+
+(defun bluetooth--root (dev-id)
+ "Return the root, ignoring DEV-ID."
+ bluetooth--root)
+
+(defun bluetooth--device (dev-id)
+ "Return the device name of DEV-ID."
+ dev-id)
+
+;;; end of path spec functions
+
+;;; Default timeout for D-Bus commands
+(defvar bluetooth--timeout 5000 "Default timeout for Bluez D-Bus access.")
+
+;;; This variable holds the device information as obtained from D-Bus.
+(defvar bluetooth--device-info nil "Device info obtained from Bluez.")
+
+;;; This alist specifies all the commands. The format is as follows:
+;;;
+;;; command . COMMAND specifies the command name
+;;; key . [?c] specifies the key binding in `bluetooth-mode-map'
+;;; method . "Method" specifies a D-Bus method "Method"
+;;; toggle . "Tprop" specifies a D-Bus property "Tprop" that is toggled
+;;; by the command
+;;; set . "Prop" specifies a D-Bus property "Prop" that can be set by
+;;; the command
+;;; query . "Query" specifies a query issued in the minibuffer; this
+;;; makes sense only if `set' is also specified
+;;; api . [:device|:adapter] specifies the Bluez API to be used
+;;; for the D-Bus command
+;;; args . ARG-LIST adds ARG-LIST to the D-Bus method invocation; the
+;;; ARG-LIST will be spliced and evaluated before the method call
+;;; The following special keywords are interpreted:
+;;; :path-devid replace by full object path
+;;; (e.g. "/org/bluez/hci0/dev_...")
+;;; run . (FORM) runs the lisp FORM after the D-Bus command
+;;; docstring . "STRING" adds STRING as documentation to the command
+(defvar bluetooth--commands
+ '(((command . connect) (key . [?c]) (method . "Connect") (api . :device)
+ (docstring . "Connect to the Bluetooth device at point."))
+ ((command . disconnect) (key . [?d]) (method . "Disconnect")
+ (api . :device) (docstring . "Disconnect Bluetooth device at point."))
+ ((command . toggle-blocked) (key . [?b]) (toggle . "Blocked")
+ (api . :device) (docstring . "Mark Bluetooth device at point blocked."))
+ ((command . toggle-trusted) (key . [?t]) (toggle . "Trusted")
+ (api . :device) (docstring . "Mark Bluetooth device at point trusted."))
+ ((command . set-alias) (key . [?a]) (set . "Alias")
+ (query . "Alias (empty to reset): ") (api . :device)
+ (docstring . "Set alias of Bluetooth device at point."))
+ ((command . start-discovery) (key . [?r]) (method . "StartDiscovery")
+ (api . :adapter) (docstring . "Start discovery mode."))
+ ((command . stop-discovery) (key . [?R]) (method . "StopDiscovery")
+ (api . :adapter) (docstring . "Stop discovery mode."))
+ ((command . remove-device) (key . [?k]) (method . "RemoveDevice")
+ (api . :adapter) (args :object-path :path-devid)
+ (docstring . "Remove Bluetooth device at point."))
+ ((command . toggle-power) (key . [?s]) (toggle . "Powered")
+ (api . :adapter) (docstring . "Toggle power supply of adapter."))
+ ((command . pair) (key . [?P]) (method . "Pair")
+ (api . :device) (docstring . "Pair with a device."))
+ ((command . toggle-discoverable) (key . [?D]) (toggle . "Discoverable")
+ (api . :adapter) (docstring . "Toggle discoverable mode."))
+ ((command . toggle-pairable) (key . [?x]) (toggle . "Pairable")
+ (api . :adapter) (docstring . "Toggle pairable mode."))))
+
+;;; This function provides the list entries for the tabulated-list
+;;; view. It is called from `tabulated-list-print'.
+(defun bluetooth--list-entry-fcn ()
+ "Provide the list entries for the tabulated view."
+ (setq bluetooth--device-info
+ (bluetooth--get-device-info (bluetooth--get-devices)))
+ (bluetooth--compile-list-entries bluetooth--device-info))
+
+;;; This function updates the list view.
+(defun bluetooth--update-list ()
+ "Update the list view."
+ (with-current-buffer bluetooth-buffer-name
+ (tabulated-list-print t)))
+
+;;; List format for the main display buffer.
+;;; NOTE: the strings MUST correspond to Bluez device properties
+;;; as they are used to gather the information from Bluez.
+(defconst bluetooth--list-format
+ [("Alias" 30 t) ("Paired" 6 t) ("Connected" 9 t) ("Address" 17 t)
+ ("Blocked" 7 t) ("Trusted" 7 t)] "The list view format for bluetooth mode.")
+
+;;; This defines the major mode.
+(define-derived-mode bluetooth-mode tabulated-list-mode
+ bluetooth--mode-name
+ "Major mode for managing Bluetooth devices.
+This mode allows pairing with and connecting to Bluetooth
+devices, as well as setting properties."
+ (setq tabulated-list-format bluetooth--list-format
+ tabulated-list-entries #'bluetooth--list-entry-fcn
+ tabulated-list-padding 1)
+ (bluetooth--make-commands)
+ (tabulated-list-init-header))
+
+;;; This function returns a list of bluetooth adapters and devices
+;;; in the form
+;;; (("hci0"
+;;; ("dev_78_AB_BB_DA_6C_7E" "dev_90_F1_AA_06_24_72")))
+;;;
+;;; The first element of each (sub-) list is an adapter name, followed
+;;; by a list of devices known to this adapter.
+(defun bluetooth--get-devices ()
+ "Return a list of bluetooth adapters and devices connected to them."
+ (mapcar #'(lambda (a)
+ (list a (dbus-introspect-get-node-names
+ :system bluetooth--service (concat bluetooth--root "/"
a))))
+ (dbus-introspect-get-node-names
+ :system bluetooth--service bluetooth--root)))
+
+;;; Given a device list as obtained from `bluetooth--get-devices'
+;;; this function gathers all the properties of each device.
+;;; The data is returned in the following structure (alist of alists):
+;;;
+;; (("dev_78_AB_BB_DA_6C_7E"
+;; (("Address" . "78:AB:BB:DA:6C:7E")
+;; ("AddressType" . "public")
+;; ...
+;; ("ServicesResolved")))
+;; ("dev_90_F1_AA_06_24_72"
+;; (("Address" . "90:F1:AA:06:24:72")
+;; ("AddressType" . "public")
+;; ...
+;; ("ServicesResolved"))))
+;;
+(defun bluetooth--get-device-info (devices)
+ "Return a list with information about DEVICES."
+ (mapcan
+ #'(lambda (devlist)
+ (cl-loop for dev in (cadr devlist)
+ for path = (mapconcat #'identity
+ (list bluetooth--root (car devlist) dev)
+ "/")
+ collect (cons dev (list (dbus-get-all-properties
+ :system bluetooth--service path
+ "org.bluez.Device1")))))
+ devices))
+
+(defun bluetooth--dev-state (key device)
+ "Return state information regarding KEY for DEVICE."
+ (let ((value (cdr (assoc key (cadr device)))))
+ (cond ((stringp value) value)
+ ((eq nil value) "no")
+ (t "yes"))))
+
+;;; This function compiles a list of device information in the
+;;; format needed by `tabulated-list-print'.
+(defun bluetooth--compile-list-entries (device-info)
+ "Compile list entries based on previously gathered device info."
+ (mapcar #'(lambda (dev)
+ (list (car dev)
+ (cl-map 'vector #'(lambda (key) (bluetooth--dev-state key
dev))
+ (mapcar #'car bluetooth--list-format))))
+ device-info))
+
+;;; This function calls FUNCTION with ARGS given the device-id DEV-ID and
+;;; Bluez API. This is used on D-Bus functions.
+(defun bluetooth--call-method (dev-id api function &rest args)
+ "For DEV-ID, invoke D-Bus FUNCTION, passing ARGS."
+ (let ((path (mapconcat #'(lambda (f) (funcall f dev-id))
+ (plist-get (plist-get bluetooth--api-info api) :path)
+ "/"))
+ (interface (plist-get (plist-get bluetooth--api-info api) :interface)))
+ (apply function :system bluetooth--service path interface
+ (mapcar #'(lambda (x) (if (eq x :path-devid) (concat path "/"
dev-id) x))
+ args))))
+
+;;; The following functions are the workers for the commands.
+;;; They are used by `bluetooth--make-commands'.
+
+;;; Invoke a D-Bus method with or without parameters.
+(defun bluetooth--dbus-method (method api &rest args)
+ "Invoke METHOD on D-Bus API."
+ (apply #'bluetooth--call-method (tabulated-list-get-id) api
+ #'dbus-call-method-asynchronously method
+ #'(lambda () (tabulated-list-print t)) :timeout bluetooth--timeout
args))
+
+;;; Toggle a property.
+(defun bluetooth--dbus-toggle (property api)
+ "Toggle boolean PROPERTY on D-Bus API."
+ (let* ((dev-id (tabulated-list-get-id))
+ (value (bluetooth--call-method dev-id api
+ #'dbus-get-property property)))
+ (bluetooth--call-method dev-id api #'dbus-set-property property (not
value))
+ (tabulated-list-print t)))
+
+;;; Set a property.
+(defun bluetooth--dbus-set (property arg api)
+ "Set PROPERTY to ARG on D-Bus API."
+ (bluetooth--call-method (tabulated-list-get-id)
+ api #'dbus-set-property property arg)
+ (tabulated-list-print t))
+
+;;; end of worker function definitions
+
+;;; This function generates all the commands.
+;;; NOTE: The spaces after the unquotes are necessary for let-alist to expand
+;;; the dotted names.
+(defun bluetooth--make-commands ()
+ "Generate the commands specified in `bluetooth--commands'."
+ (dolist (cmd bluetooth--commands)
+ (let-alist cmd
+ (let ((command (intern (concat "bluetooth-" (symbol-name .command)))))
+ (defalias command
+ (cond
+ (.method `(lambda () (interactive)
+ (bluetooth--dbus-method , .method , .api ,@ .args)
+ ,@ .run))
+ (.toggle `(lambda () (interactive)
+ (let ((value (bluetooth--dbus-toggle , .toggle , .api)))
+ ,@ .run)))
+ (.set `(lambda (arg) (interactive ,(concat "M" .query))
+ (bluetooth--dbus-set , .set arg , .api)
+ ,@ .run)))
+ .docstring)
+ (define-key bluetooth-mode-map .key command)))))
+
+(defun bluetooth--display-state ()
+ "Get the current adapter state and display it.
+This function only uses the first adapter reported by Bluez."
+ (let* ((adapters (dbus-introspect-get-node-names
+ :system bluetooth--service bluetooth--root))
+ (resp (dbus-get-all-properties :system bluetooth--service
+ (concat bluetooth--root "/"
+ (car adapters))
+ "org.bluez.Adapter1"))
+ (info (mapconcat #'identity
+ (-keep #'(lambda (x) (if (cdr (assoc (first x) resp))
+ (second x) (third x)))
+ bluetooth--mode-state)
+ ",")))
+ (unless (string-blank-p info)
+ (concat "[" info "] "))))
+
+(defun bluetooth--cleanup ()
+ "Clean up."
+ (setq mode-line-misc-info (cl-remove bluetooth--mode-info
+ mode-line-misc-info))
+ (bluetooth--unregister-agent))
+
+;;; This command is the main entry point. It is meant to be called by
+;;; the user.
+;;;
+;;; Note that this command will redefine the commands and key bindings
+;;; as specified in `bluetooth--commands'. If you want to have
+;;; different key bindings, either edit this variable or change the
+;;; key bindings in a hook.
+
+;;;###autoload
+(defun list-bluetooth-devices ()
+ "Display a list of Bluetooth devices that are available."
+ (interactive)
+ (dbus-ping :system bluetooth--service bluetooth--timeout)
+ (with-current-buffer (switch-to-buffer bluetooth-buffer-name)
+ (erase-buffer)
+ (bluetooth-mode)
+ (bluetooth--register-agent)
+ (add-hook 'kill-buffer-hook #'bluetooth--cleanup nil t)
+ (setq mode-line-misc-info (cl-pushnew bluetooth--mode-info
+ mode-line-misc-info))
+ (tabulated-list-print)))
+
+;;; Bluetooth pairing agent code
+
+(defmacro bluetooth--with-alias (device &rest body)
+ "Evaluate BODY with DEVICE alias bound to ALIAS."
+ (declare (indent defun))
+ `(let ((alias (bluetooth--call-method
+ (car (last (split-string ,device "/"))) :device
+ #'dbus-get-property "Alias")))
+ ,@body))
+
+(defmacro bluetooth--maybe-cancel-reject (&rest body)
+ "Invoke BODY and maybe issue cancel and reject errors.
+`org.bluez.Error.Canceled' is issued on `keyboard-quit' and
+`org.bluez.Error.Rejected' is issued if BODY evaluates to nil."
+ (declare (indent defun))
+ `(or (condition-case nil
+ ,@body
+ (quit (signal 'dbus-error '("org.bluez.Error.Canceled"))))
+ (signal 'dbus-error '("org.bluez.Error.Rejected"))))
+
+(defun bluetooth--request-pin-code (device)
+ "Request a pin code for DEVICE."
+ (bluetooth--maybe-cancel-reject
+ (bluetooth--with-alias device
+ (save-match-data
+ (let* ((pin (read-passwd (format "Enter Bluetooth PIN for `%s': "
alias)))
+ (trimmed-pin (substring pin 0 (min (length pin) 16)))
+ (case-fold-search nil))
+ (unwind-protect
+ (cond ((= 0 (length trimmed-pin))
+ (message "PIN has zero length")
+ nil)
+ ((string-match "[^[:alnum:]]" trimmed-pin)
+ (message "PIN contains non-alphanumeric characters")
+ nil)
+ (t trimmed-pin))
+ (clear-string pin)
+ (clear-string trimmed-pin)))))))
+
+(defun bluetooth--display-pin-code (device pincode)
+ "Display the PINCODE for DEVICE."
+ (bluetooth--with-alias device
+ (message "Bluetooth PIN for `%s': %s" alias pincode)
+ :ignore))
+
+(defun bluetooth--request-passkey (device)
+ "Request passkey for DEVICE."
+ (bluetooth--maybe-cancel-reject
+ (bluetooth--with-alias device
+ (let ((pk (read-from-minibuffer
+ (format "Enter Bluetooth Passkey for `%s': (0..999999) "
+ alias))))
+ (min (max (string-to-number pk) 0) 999999)))))
+
+(defun bluetooth--display-passkey (device passkey entered)
+ "Display PASSKEY for DEVICE, ignoring ENTERED (for now)."
+ (bluetooth--with-alias device
+ (message "Bluetooth Passkey for `%s': %06d" alias passkey)
+ :ignore))
+
+(defun bluetooth--request-confirmation (device passkey)
+ "Request user confirmation that DEVICE's PASSKEY is correct."
+ (bluetooth--maybe-cancel-reject
+ (bluetooth--with-alias device
+ (y-or-n-p
+ (format "Is Bluetooth Passkey %06d for `%s' correct? " passkey alias))))
+ :ignore)
+
+(defun bluetooth--request-authorization (device)
+ "Authorize Bluetooth DEVICE."
+ (bluetooth--maybe-cancel-reject
+ (bluetooth--with-alias device
+ (y-or-n-p (format "Authorize Bluetooth device `%s'? " alias))))
+ :ignore)
+
+(defun bluetooth--authorize-service (device uuid)
+ "Authorize Bluetooth service UUID for DEVICE."
+ (bluetooth--maybe-cancel-reject
+ (bluetooth--with-alias device
+ (y-or-n-p
+ (format "Authorize Bluetooth service `%s' for device `%s'? " uuid
alias))))
+ :ignore)
+
+(defun bluetooth--cancel ()
+ (keyboard-quit)
+ (message "Pairing canceled"))
+
+(defconst bluetooth--methods
+ '("Release" "RequestPinCode" "DisplayPinCode" "RequestPasskey"
+ "DisplayPasskey" "RequestConfirmation" "RequestAuthorization"
+ "AuthorizeService" "Cancel")
+ "D-Bus method names for the agent interface.")
+
+;;; This procedure registers the pairing agent.
+(defun bluetooth--register-agent ()
+ "Register as a pairing agent."
+ ;; register all the methods
+ (save-match-data
+ (setq bluetooth--method-objects
+ (cl-loop for method in bluetooth--methods
+ with case-fold-search = nil
+ for fname = (concat "bluetooth-"
+ (replace-regexp-in-string
+ "[A-Z][a-z]+"
+ #'(lambda (x) (concat "-" (downcase x)))
+ method t))
+ collect (dbus-register-method :session dbus-service-emacs
+ bluetooth--own-path
+ bluetooth--agent-intf
+ method (intern fname) t))))
+ (dbus-register-service :session dbus-service-emacs)
+ (dbus-call-method :system bluetooth--service bluetooth--root
+ bluetooth--agent-mngr-intf "RegisterAgent"
+ :object-path bluetooth--own-path "KeyboardDisplay"))
+
+(defun bluetooth--unregister-agent ()
+ "Unregister the pairing agent."
+ (mapc #'dbus-unregister-object bluetooth--method-objects)
+ (dbus-call-method :system bluetooth--service bluetooth--root
+ bluetooth--agent-mngr-intf "UnregisterAgent"
+ :object-path bluetooth--own-path))
+
+(provide 'bluetooth)
+
+;;; bluetooth.el ends here
- [elpa] branch externals/bluetooth created (now 55304bb), Stefan Monnier, 2019/11/07
- [elpa] externals/bluetooth 6cdc794 01/57: Initial commit, Stefan Monnier, 2019/11/07
- [elpa] externals/bluetooth f16a702 07/57: fixes undefined function problem (first/second/third -> car etc.), Stefan Monnier, 2019/11/07
- [elpa] externals/bluetooth 2fbe9e4 04/57: adds URL, Stefan Monnier, 2019/11/07
- [elpa] externals/bluetooth 9d1369a 05/57: adds Readme.org and removes Readme.md, Stefan Monnier, 2019/11/07
- [elpa] externals/bluetooth 5db9e29 08/57: adds bluetooth--release, Stefan Monnier, 2019/11/07
- [elpa] externals/bluetooth a883d60 10/57: fixes spurious error on clean-up, Stefan Monnier, 2019/11/07
- [elpa] externals/bluetooth e6ca72d 06/57: corrects docstrings and Readme.org, Stefan Monnier, 2019/11/07
- [elpa] externals/bluetooth dd28c81 11/57: turns defvar into defconst where possible, Stefan Monnier, 2019/11/07
- [elpa] externals/bluetooth 00497ca 03/57: adds bluetooth.el and .gitignore,
Stefan Monnier <=
- [elpa] externals/bluetooth 0cac7d9 09/57: makes status display in mode line buffer-local, Stefan Monnier, 2019/11/07
- [elpa] externals/bluetooth 58dd9f9 12/57: adds 'Maintainer' header and cleans up 'Package-Requires', Stefan Monnier, 2019/11/07
- [elpa] externals/bluetooth 2560749 15/57: adds Imenu integration, Stefan Monnier, 2019/11/07
- [elpa] externals/bluetooth cc33d79 14/57: corrects project name in LICENSE, Stefan Monnier, 2019/11/07
- [elpa] externals/bluetooth 3743458 17/57: corrects list redisplay when mode is restarted, Stefan Monnier, 2019/11/07
- [elpa] externals/bluetooth 8cac69d 26/57: checks list-id for nil to avoid errors at actions w/o list item, Stefan Monnier, 2019/11/07
- [elpa] externals/bluetooth 4a40ee6 24/57: adds GATT UUIDs and a rudimentary display function, Stefan Monnier, 2019/11/07
- [elpa] externals/bluetooth f33cb12 02/57: Add LICENSE, Stefan Monnier, 2019/11/07
- [elpa] externals/bluetooth ee4e582 30/57: fixes compiler warnings about unused parameters, Stefan Monnier, 2019/11/07
- [elpa] externals/bluetooth 3365413 33/57: corrects the major mode check in `list-bluetooth-devices', Stefan Monnier, 2019/11/07