Source code for bluedot.btcomm

from __future__ import unicode_literals

import socket
import sys
import errno

from .utils import (
    register_spp,
    get_mac,
    get_adapter_powered_status,
    get_adapter_discoverable_status,
    get_adapter_pairable_status,
    get_paired_devices,
    device_pairable,
    device_discoverable,
    device_powered,
)

from .threads import WrapThread

BLUETOOTH_TIMEOUT = 0.01


[docs]class BluetoothAdapter: """ Represents and allows interaction with a Bluetooth Adapter. The following example will get the Bluetooth adapter, print its powered status and any paired devices:: a = BluetoothAdapter() print("Powered = {}".format(a.powered)) print(a.paired_devices) :param str device: The Bluetooth device to be used, the default is "hci0", if your device only has 1 Bluetooth adapter this shouldn't need to be changed. """ def __init__(self, device = "hci0"): self._device = device self._address = get_mac(self._device) self._pairing_thread = None @property def device(self): """ The Bluetooth device name. This defaults to "hci0". """ return self._device @property def address(self): """ The `MAC address`_ of the Bluetooth adapter. .. _MAC address: https://en.wikipedia.org/wiki/MAC_address """ return self._address @property def powered(self): """ Set to ``True`` to power on the Bluetooth adapter. Depending on how Bluetooth has been powered down, you may need to use :command:`rfkill` to unblock Bluetooth to give permission to bluez to power on Bluetooth:: sudo rfkill unblock bluetooth """ return get_adapter_powered_status(self._device) @powered.setter def powered(self, value): device_powered(self._device, value) @property def discoverable(self): """ Set to ``True`` to make the Bluetooth adapter discoverable. """ return get_adapter_discoverable_status(self._device) @discoverable.setter def discoverable(self, value): device_discoverable(self._device, value) @property def pairable(self): """ Set to ``True`` to make the Bluetooth adapter pairable. """ return get_adapter_pairable_status(self._device) @pairable.setter def pairable(self, value): device_pairable(self._device, value) @property def paired_devices(self): """ Returns a sequence of devices paired with this adapater :code:`[(mac_address, name), (mac_address, name), ...]`:: a = BluetoothAdapter() devices = a.paired_devices for d in devices: device_address = d[0] device_name = d[1] """ return get_paired_devices(self._device)
[docs] def allow_pairing(self, timeout = 60): """ Put the adapter into discoverable and pairable mode. :param int timeout: The time in seconds the adapter will remain pairable. If set to ``None`` the device will be discoverable and pairable indefinetly. """ #if a pairing thread is already running, stop it and restart if self._pairing_thread: if self._pairing_thread.is_alive: self._pairing_thread.stop() #make the adapter pairable self.pairable = True self.discoverable = True if timeout != None: #start the pairing thread self._pairing_thread = WrapThread(target=self._expire_pairing, args=(timeout, )) self._pairing_thread.start()
def _expire_pairing(self, timeout): #wait till the timeout or the thread is stopped self._pairing_thread.stopping.wait(timeout) self.discoverable = False self.pairable = False
[docs]class BluetoothServer: """ Creates a Bluetooth server which will allow connections and accept incoming RFCOMM serial data. When data is received by the server it is passed to a callback function which must be specified at initiation. The following example will create a Bluetooth server which will wait for a connection and print any data it receives and send it back to the client:: from bluedot.btcomm import BluetoothServer from signal import pause def data_received(data): print(data) s.send(data) s = BluetoothServer(data_received) pause() :param data_received_callback: A function reference should be passed, this function will be called when data is received by the server. The function should accept a single parameter which when called will hold the data received. Set to ``None`` if received data is not required. :param bool auto_start: If ``True`` (the default), the Bluetooth server will be automatically started on initialisation, if ``False``, the method ``start`` will need to be called before connections will be accepted. :param str device: The Bluetooth device the server should use, the default is "hci0", if your device only has 1 Bluetooth adapter this shouldn't need to be changed. :param int port: The Bluetooth port the server should use, the default is 1. :param str encoding: The encoding standard to be used when sending and receiving byte data. The default is "utf-8". If set to ``None`` no encoding is done and byte data types should be used. :param bool power_up_device: If ``True``, the Bluetooth device will be powered up (if required) when the server starts. The default is ``False``. Depending on how Bluetooth has been powered down, you may need to use :command:`rfkill` to unblock Bluetooth to give permission to bluez to power on Bluetooth:: sudo rfkill unblock bluetooth :param when_client_connects: A function reference which will be called when a client connects. If ``None`` (the default), no notification will be given when a client connects :param when_client_disconnects: A function reference which will be called when a client disconnects. If ``None`` (the default), no notification will be given when a client disconnects """ def __init__(self, data_received_callback, auto_start = True, device = "hci0", port = 1, encoding = "utf-8", power_up_device = False, when_client_connects = None, when_client_disconnects = None): self._setup_adapter(device) self._data_received_callback = data_received_callback self._port = port self._encoding = encoding self._power_up_device = power_up_device self._when_client_connects = when_client_connects self._when_client_disconnects = when_client_disconnects self._running = False self._client_connected = False self._server_sock = None self._client_info = None self._client_sock = None self._conn_thread = None if auto_start: self.start() @property def device(self): """ The Bluetooth device the server is using. This defaults to "hci0". """ return self.adapter.device @property def adapter(self): """ A :class:`BluetoothAdapter` object which represents the Bluetooth device the server is using. """ return self._adapter @property def port(self): """ The port the server is using. This defaults to 1. """ return self._port @property def encoding(self): """ The encoding standard the server is using. This defaults to "utf-8". """ return self._encoding @property def running(self): """ Returns a ``True`` if the server is running. """ return self._running @property def server_address(self): """ The `MAC address`_ of the device the server is using. .. _MAC address: https://en.wikipedia.org/wiki/MAC_address """ return self.adapter.address @property def client_address(self): """ The `MAC address`_ of the client connected to the server. Returns ``None`` if no client is connected. .. _MAC address: https://en.wikipedia.org/wiki/MAC_address """ if self._client_info: return self._client_info[0] else: return None @property def client_connected(self): """ Returns ``True`` if a client is connected. """ return self._client_connected @property def data_received_callback(self): """ Sets or returns the function which is called when data is received by the server. The function should accept a single parameter which when called will hold the data received. Set to ``None`` if received data is not required. """ return self._data_received_callback @data_received_callback.setter def data_received_callback(self, value): self._data_received_callback = value @property def when_client_connects(self): """ Sets or returns the function which is called when a client connects. """ return self._when_client_connects @when_client_connects.setter def when_client_connects(self, value): self._when_client_connects = value @property def when_client_disconnects(self): """ Sets or returns the function which is called when a client disconnects. """ return self._when_client_disconnects @when_client_disconnects.setter def when_client_disconnects(self, value): self._when_client_disconnects = value
[docs] def start(self): """ Starts the Bluetooth server if its not already running. The server needs to be started before connections can be made. """ if not self._running: if self._power_up_device: self.adapter.powered = True if not self.adapter.powered: raise Exception("Bluetooth device {} is turned off".format(self.adapter.device)) #register the serial port profile with Bluetooth register_spp(self._port) #start Bluetooth server #open the Bluetooth socket self._server_sock = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_STREAM, socket.BTPROTO_RFCOMM) self._server_sock.settimeout(BLUETOOTH_TIMEOUT) try: self._server_sock.bind((self.server_address, self.port)) except (socket.error, OSError) as e: if e.errno == errno.EADDRINUSE: print("Bluetooth address {} is already in use - is the server already running?".format(self.server_address)) raise e self._server_sock.listen(1) #wait for client connection self._conn_thread = WrapThread(target=self._wait_for_connection) self._conn_thread.start() self._running = True
[docs] def stop(self): """ Stops the Bluetooth server if its running. """ if self._running: if self._conn_thread: self._conn_thread.stop() self._conn_thread = None
[docs] def send(self, data): """ Send data to a connected Bluetooth client :param str data: The data to be sent. """ # print(data) if self._client_connected: if self._encoding is not None: data = data.encode(self._encoding) try: self._send_data(data) except IOError as e: self._handle_bt_error(e)
def _send_data(self, data): """ Send raw data to the client. :param bytes data: The data to be sent. """ self._client_sock.sendall(data)
[docs] def disconnect_client(self): """ Disconnects the client if connected. Returns `True` if a client was disconnected. """ if self._client_connected: self._client_connected = False # call the callback if self.when_client_disconnects: WrapThread(target=self.when_client_disconnects).start() return True else: return False
def _setup_adapter(self, device): self._adapter = BluetoothAdapter(device) def _wait_for_connection(self): #keep going until the server is stopped while not self._conn_thread.stopping.is_set(): #wait for connection self._client_connected = False while not self._conn_thread.stopping.is_set(): try: # accept() will timeout after BLUETOOTH_TIMEOUT seconds self._client_sock, self._client_info = self._server_sock.accept() self._client_connected = True break except socket.timeout as e: self._handle_bt_error(e) #did a client connect? if self._client_connected: #call the call back if self.when_client_connects: WrapThread(target=self.when_client_connects).start() #read data self._read() #server has been stopped self._server_sock.close() self._server_sock = None self._running = False def _read(self): #read until the server is stopped or the client disconnects while self._client_connected: #read data from Bluetooth socket try: data = self._client_sock.recv(1024, socket.MSG_DONTWAIT) except IOError as e: self._handle_bt_error(e) data = b"" if data: if self._data_received_callback: if self._encoding: data = data.decode(self._encoding) self.data_received_callback(data) if self._conn_thread.stopping.wait(BLUETOOTH_TIMEOUT): break #close the client socket self._client_sock.close() self._client_sock = None self._client_info = None self._client_connected = False def _handle_bt_error(self, bt_error): assert isinstance(bt_error, IOError) #'timed out' is caused by the wait_for_connection loop if isinstance(bt_error, socket.timeout): pass #'resource unavailable' is when data cannot be read because there is nothing in the buffer elif bt_error.errno == errno.EAGAIN: pass #'connection reset' is caused when the client disconnects elif bt_error.errno == errno.ECONNRESET: self.disconnect_client() #'conection timeout' is caused when the server can no longer connect to read from the client # (perhaps the client has gone out of range) elif bt_error.errno == errno.ETIMEDOUT: self.disconnect_client() else: raise bt_error
[docs]class BluetoothClient(): """ Creates a Bluetooth client which can send data to a server using RFCOMM Serial Data. The following example will create a Bluetooth client which will connect to a paired device called "raspberrypi", send "helloworld" and print any data is receives:: from bluedot.btcomm import BluetoothClient from signal import pause def data_received(data): print(data) c = BluetoothClient("raspberrypi", data_received) c.send("helloworld") pause() :param str server: The server name ("raspberrypi") or server MAC address ("11:11:11:11:11:11") to connect to. The server must be a paired device. :param data_received_callback: A function reference should be passed, this function will be called when data is received by the client. The function should accept a single parameter which when called will hold the data received. Set to ``None`` if data received is not required. :param int port: The Bluetooth port the client should use, the default is 1. :param str device: The Bluetooth device to be used, the default is "hci0", if your device only has 1 Bluetooth adapter this shouldn't need to be changed. :param str encoding: The encoding standard to be used when sending and receiving byte data. The default is "utf-8". If set to ``None`` no encoding is done and byte data types should be used. :param bool power_up_device: If ``True``, the Bluetooth device will be powered up (if required) when the server starts. The default is ``False``. Depending on how Bluetooth has been powered down, you may need to use :command:`rfkill` to unblock Bluetooth to give permission to Bluez to power on Bluetooth:: sudo rfkill unblock bluetooth :param bool auto_connect: If ``True`` (the default), the Bluetooth client will automatically try to connect to the server at initialisation, if ``False``, the :meth:`connect` method will need to be called. """ def __init__(self, server, data_received_callback, port = 1, device = "hci0", encoding = "utf-8", power_up_device = False, auto_connect = True): self._server = server self._data_received_callback = data_received_callback self._port = port self._power_up_device = power_up_device self._encoding = encoding self._setup_adapter(device) self._connected = False self._client_sock = None self._conn_thread = None if auto_connect: self.connect() @property def device(self): """ The Bluetooth device the client is using. This defaults to "hci0". """ return self.adapter.device @property def server(self): """ The server name ("raspberrypi") or server `MAC address`_ ("11:11:11:11:11:11") to connect to. .. _MAC address: https://en.wikipedia.org/wiki/MAC_address """ return self._server @property def port(self): """ The port the client is using. This defaults to 1. """ return self._port @property def adapter(self): """ A :class:`BluetoothAdapter` object which represents the Bluetooth device the client is using. """ return self._adapter @property def encoding(self): """ The encoding standard the client is using. The default is "utf-8". """ return self._encoding @property def client_address(self): """ The MAC address of the device being used. """ return self.adapter.address @property def connected(self): """ Returns ``True`` when connected. """ return self._connected @property def data_received_callback(self): """ Sets or returns the function which is called when data is received by the client. The function should accept a single parameter which when called will hold the data received. Set to ``None`` if data received is not required. """ return self._data_received_callback @data_received_callback.setter def data_received_callback(self, value): self._data_received_callback = value
[docs] def connect(self): """ Connect to a Bluetooth server. """ if not self._connected: if self._power_up_device: self.adapter.powered = True if not self.adapter.powered: raise Exception("Bluetooth device {} is turned off".format(self.adapter.device)) #try and find the server name or MAC address in the paired devices list server_mac = None for device in self.adapter.paired_devices: if self._server == device[0] or self._server == device[1]: server_mac = device[0] break if server_mac == None: raise Exception("Server {} not found in paired devices".format(self._server)) #create a socket self._client_sock = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_STREAM, socket.BTPROTO_RFCOMM) self._client_sock.bind((self.adapter.address, self._port)) self._client_sock.connect((server_mac, self._port)) self._connected = True self._conn_thread = WrapThread(target=self._read) self._conn_thread.start()
[docs] def disconnect(self): """ Disconnect from a Bluetooth server. """ if self._connected: #stop the connection thread if self._conn_thread: self._conn_thread.stop() self._conn_thread = None #close the socket try: self._client_sock.close() finally: self._client_sock = None self._connected = False
[docs] def send(self, data): """ Send data to a Bluetooth server. :param str data: The data to be sent. """ if self._connected: if self._encoding is not None: data = data.encode(self._encoding) try: self._send_data(data) except IOError as e: self._handle_bt_error(e)
def _send_data(self, data): """ Send raw data to the client. :param bytes data: The data to be sent. """ self._client_sock.sendall(data) def _read(self): #read until the client is stopped or the client disconnects while self._connected: #read data from Bluetooth socket try: data = self._client_sock.recv(1024, socket.MSG_DONTWAIT) except IOError as e: self._handle_bt_error(e) data = b"" if data: #print("received [%s]" % data) if self._data_received_callback: if self._encoding: data = data.decode(self._encoding) self.data_received_callback(data) if self._conn_thread.stopping.wait(BLUETOOTH_TIMEOUT): break def _setup_adapter(self, device): self._adapter = BluetoothAdapter(device) def _handle_bt_error(self, bt_error): assert isinstance(bt_error, IOError) #'resource unavailable' is when data cannot be read because there is nothing in the buffer if bt_error.errno == errno.EAGAIN: pass #'connection reset' is caused when the client disconnects elif bt_error.errno == errno.ECONNRESET: self._connected = False #'conection timeout' is caused when the server can no longer connect to read from the client # (perhaps the client has gone out of range) elif bt_error.errno == errno.ETIMEDOUT: self._connected = False else: raise bt_error