Ravn::

Bluez module

Utility functions for interacting with the system bluetooth stack.

This requires the following system configuration in /etc/dbus-1/system.d/ravn.conf:

<!DOCTYPE busconfig PUBLIC “-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN” “www.freedesktop.org/standards/dbus/1.0/busconfig.dtd”> <busconfig> <policy user=“ravn”> <allow own=“com.ravn.agent” /> </policy> <policy context=“default”> <allow send_destination=“com.ravn.agent” /> </policy> </busconfig>

Constants

BLUEZ_ADAPTER_NS

Current namespace for the adapter service interface. github.com/bluez/bluez/blob/master/doc/adapter-api.txt

BLUEZ_AGENTMANAGER_NS

Current namespace for the agent manager service interface. github.com/bluez/bluez/blob/master/doc/agent-api.txt

BLUEZ_AGENT_NS

Current namespace for the agent service interface. github.com/bluez/bluez/blob/master/doc/agent-api.txt

BLUEZ_DEVICE_NS

Current namespace for the device service interface. github.com/bluez/bluez/blob/master/doc/device-api.txt

BLUEZ_OBJMANAGER_NS

Current namespace for the objectmanager interface. dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces-objectmanager

BLUEZ_PROFILEMANAGER_NS

Current namespace for the profile service interface. github.com/bluez/bluez/blob/master/doc/profile-api.txt

PAIRING_AGENT_NS

The namespace for the Ravn pairing agent callback. (This requires dbus operating system configuration.)

PAIRING_AGENT_PATH

The path to the Ravn pairing agent callback. git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/agent-api.txt

SERIAL_DBUS_PATH

The DBus path of the serial agent profile.

SPP_PROFILE_UUID

The (shortened) UUID of the SPP profile

Public Class Methods

new( * )

Instantiate a new SerialProfile callback instance.

# File lib/ravn/bluez.rb, line 230
def initialize( * )
        @device = nil
        super
end

Public Instance Methods

adapter()

Return a memoized service interface for the bluetooth adapter, setting up any configuration on first instantiation.

# File lib/ravn/bluez.rb, line 136
def adapter
        unless @adapter
                @adapter = self.bluez_service[ '/org/bluez/' + Ravn::Bluez.adapter_name ][ BLUEZ_ADAPTER_NS ]

                self.power_on
                self.adapter_alias( 'ravn-' + Ravn.device_id )
                properties = self.adapter.all_properties

                self.log.info "Setting up bluetooth adapter %s %p {%s}: %s" % [
                        self.adapter.name,
                        properties['Alias'] || properties['Name'],
                        properties['Address'],
                        properties['Modalias']
                ]
        end
        return @adapter
end
adapter_address()

Return the mac address of the bluetooth controller.

# File lib/ravn/bluez.rb, line 156
def adapter_address
        return self.adapter[ 'Address' ]
end
adapter_alias( name=nil )

Sets the bluetooth controller to name, or returns the current alias.

# File lib/ravn/bluez.rb, line 323
def adapter_alias( name=nil )
        self.adapter[ 'Alias' ] = name if name
        return self.adapter[ 'Alias' ]
end
agent_manager()

Return a memoized agent manager service interface.

# File lib/ravn/bluez.rb, line 117
def agent_manager
        return @agent_manager ||= self.bluez_service[ '/org/bluez' ][ BLUEZ_AGENTMANAGER_NS ]
end
bluez_service()

Return a memoized bluez service interface object.

# File lib/ravn/bluez.rb, line 111
def bluez_service
        return @bluez_service ||= self.dbus.service( 'org.bluez' ).introspect
end
connection( path, fd )

A new connection from a client device.

# File lib/ravn/bluez.rb, line 241
def connection( path, fd )
        self.log.info "New connection: path=%p  fdno=%p" % [ path, fd ]
        self.device.tell( connected_client: [path, fd] )
end
dbus()

Return a memoized dbus system instance. This connects to dbus immediately.

# File lib/ravn/bluez.rb, line 105
def dbus
        return @dbus ||= DBus::SystemBus.instance
end
device( path )

Given a valid dbus device path, return a bluez service interface. If the device is nonexistent or invalid, returns nil.

# File lib/ravn/bluez.rb, line 163
def device( path )
        self.log.info "Fetching device at path=%p" % [ path ]
        return self.bluez_service[ path ][ BLUEZ_DEVICE_NS ]
rescue DBus::Error => err
        self.log.error "Unable to find device at: %s (%s)" % [ path, err.message ]
        return nil
end
device_paired?( path )

Returns true if bluez believes the device at path has been paired.

# File lib/ravn/bluez.rb, line 173
def device_paired?( path )
        device = path.respond_to?( :Pair ) ? path : self.device( path )
        return device && device[ 'Paired' ]
rescue
        return false
end
disconnection( path )

A connected client has broken their connection. NOTE: This currently does not appear to fire on a client socket close. Leaving this in place, but unfortunately most of the disconnection logic will need to be on Socket exceptions, for the time being.

# File lib/ravn/bluez.rb, line 259
def disconnection( path )
        self.log.info "Disconnected: path=%p" % [ path ]
        self.device.tell( disconnected_client: path )
end
discoverable( timeout=60 )

Make this device discoverable to other bluetooth devices for timeout seconds. Setting timeout to 0 makes discoverable permanent until manually disabled. A timeout of nil disables.

# File lib/ravn/bluez.rb, line 184
def discoverable( timeout=60 )
        self.log.info "Making this device discoverable, timeout=%p" % [ timeout ]
        if timeout
                self.adapter[ 'DiscoverableTimeout' ] = DBus.variant( 'u', timeout.abs )
                self.adapter[ 'Discoverable' ] = true
        else
                self.adapter[ 'Discoverable' ] = false
        end
end
macaddress_to_devpath( mac )

Convert a bluetooth mac address to a bluez device path.

# File lib/ravn/bluez.rb, line 337
def macaddress_to_devpath( mac )
        return "/org/bluez/%s/dev_%s" % [
                self.adapter_name,
                mac.upcase.gsub( ':', '_' )
        ]
end
object_manager()

Return a memoized object manager interface.

# File lib/ravn/bluez.rb, line 123
def object_manager
        return @object_manager ||= self.bluez_service[ '/' ][ BLUEZ_OBJMANAGER_NS ]
end
on_interface_added( &block )

Use the ObjectManager to notice found devices when discovery scanning is enabled. Yields device path and interface to the block.

# File lib/ravn/bluez.rb, line 419
def on_interface_added( &block )
        self.object_manager.on_signal( "InterfacesAdded", &block )
end
on_interface_removed( &block )

Use the ObjectManager to notice removed devices when scanning is enabled. Yields device path and interface to the block.

# File lib/ravn/bluez.rb, line 426
def on_interface_removed( &block )
        self.object_manager.on_signal( "InterfacesRemoved", &block )
end
pair( path )

Attempt to pair and trust the device at path, either a dbus device or a path to one. Returns true if successful, false if cancelled or the pairing request times out.

# File lib/ravn/bluez.rb, line 304
def pair( path )
        device = path.respond_to?( :Pair ) ? path : self.device( path )
        unless device
                self.log.warn "No device %p: failed to pair." % [ path ]
                return
        end

        device.Pair
        device[ 'Trusted' ] = true
        return true

rescue DBus::Error => err
        self.log.error "Unable to pair: %s" % [ err.message ]
        self.unpair( path ) # clear bluez cache
        return false
end
power_on()

Enable the controller. Note: if the controller is off, this blocks until ready. If already on, this is a no-op.

# File lib/ravn/bluez.rb, line 331
def power_on
        self.adapter[ 'Powered' ] = true
end
profile_manager()

Return a memoized profile manager interface.

# File lib/ravn/bluez.rb, line 129
def profile_manager
        return @profile_manager ||= self.bluez_service[ '/org/bluez' ][ BLUEZ_PROFILEMANAGER_NS ]
end
release()

DBus API: we have no additional cleanup to perform.

# File lib/ravn/bluez.rb, line 248
def release
        self.log.info "Releasing connection."
        # no-op
end
scanning?()

Returns true if the adapter is currently scanning.

# File lib/ravn/bluez.rb, line 215
def scanning?
        return self.adapter[ 'Discovering' ]
end
setup_pairing_callback()

Create a new pairing agent, and emit newly found devices to our Actor parent for pairing.

# File lib/ravn/bluez.rb, line 379
def setup_pairing_callback
        agent_class = Class.new( DBus::Object ) do

                ### DBus interface API.
                ###
                dbus_interface( BLUEZ_AGENT_NS ) do

                        ### We're pairing headless, and accepting all auto-generated passkeys.
                        ### Only need implement this single callback.
                        ###
                        ###  void RequestConfirmation(object device, uint32 passkey)
                        ###
                        ###    This method gets called when the service daemon
                        ###    needs to confirm a passkey for an authentication.
                        ###
                        ###    To confirm the value it should return an empty reply
                        ###    or an error in case the passkey is invalid.
                        ###
                        ###    Note that the passkey will always be a 6-digit number,
                        ###    so the display should be zero-padded at the start if
                        ###    the value contains less than 6 digits.
                        ###
                        ###    Possible errors: org.bluez.Error.Rejected
                        ###                     org.bluez.Error.Canceled
                        ###
                        dbus_method( :RequestConfirmation, "in device:o, in passkey:u" ) do |path, passkey|
                                nil
                        end
                end
        end

        pairing_agent = agent_class.new( PAIRING_AGENT_PATH )
        self.dbus.request_service( PAIRING_AGENT_NS ).export( pairing_agent )
        self.agent_manager.RegisterAgent( pairing_agent.path, 'DisplayYesNo' )
        self.agent_manager.RequestDefaultAgent( pairing_agent.path )
end
start_dbus()

Start a new dbus event loop, for catching async dbus events.

# File lib/ravn/bluez.rb, line 346
def start_dbus
        self.log.info "Starting bluez event loop for [%s] (%s)" % [
                self.adapter_address,
                self.adapter_alias
        ]

        dbus_instance = self.dbus
        @dbus_loop = nil

        Thread.new do
                @dbus_loop = DBus::Main.new
                @dbus_loop << dbus_instance
                begin
                        @dbus_loop.run
                rescue DBus::Error => err
                        self.log.error( err.message )
                end
        end
end
start_scanning( filter=nil )

Search for discoverable devices, optionally limiting to a mac address pattern filter.

# File lib/ravn/bluez.rb, line 197
def start_scanning( filter=nil )
        self.log.info "Scanning with filter=%p." % [ filter ]
        self.stop_scanning
        self.adapter.SetDiscoveryFilter( {} ) # clear any other pattern
        self.adapter.SetDiscoveryFilter( 'Pattern' => filter ) if filter
        self.adapter.StartDiscovery
end
start_socket( device )

Create a new SPP callback instance, and start the glib event loop.

# File lib/ravn/bluez.rb, line 221
def start_socket( device )

        pairing_class = Class.new( Bluez::Profile ) do

                extend Loggability
                log_to :ravn_hal


                ### Instantiate a new SerialProfile callback instance.
                def initialize( * )
                        @device = nil
                        super
                end


                # The parent Concurrent::Actor device that instantiated this object.
                attr_accessor :device


                ### A new connection from a client device.
                def connection( path, fd )
                        self.log.info "New connection: path=%p  fdno=%p" % [ path, fd ]
                        self.device.tell( connected_client: [path, fd] )
                end


                ### DBus API: we have no additional cleanup to perform.
                def release
                        self.log.info "Releasing connection."
                        # no-op
                end


                ### A connected client has broken their connection.
                ### NOTE: This currently does not appear to fire on a client socket
                ### close.  Leaving this in place, but unfortunately most of the
                ### disconnection logic will need to be on Socket exceptions, for
                ### the time being.
                def disconnection( path )
                        self.log.info "Disconnected: path=%p" % [ path ]
                        self.device.tell( disconnected_client: path )
                end

        end # pairing_class

        self.log.info "Setting up the DBus server profile loop for profile %s on channel %d." % [
                SPP_PROFILE_UUID,
                Ravn::Bluez.channel
        ]
        @profile_loop = pairing_class.new( SERIAL_DBUS_PATH, SPP_PROFILE_UUID, {
                name: 'Serial Port',
                role: Bluez::Profile::Server,
                authorization: false,
                channel: Ravn::Bluez.channel
        })
        @profile_loop.device = device

        return Thread.new do
                Thread.current.name = "Bluez I/O loop"
                @profile_loop.run
        end
end
stop_dbus()

Unregister the pairing agent and cleanly shut down the dbus event loop.

# File lib/ravn/bluez.rb, line 368
def stop_dbus
        return unless @dbus_loop
        @dbus_loop.quit
        self.agent_manager.UnregisterAgent( PAIRING_AGENT_PATH )
rescue DBus::Error => err
        self.log.error "Error while stopping dbus: %s" % [ err.message ]
end
stop_scanning()

Turn off scanning - checks if it is on first, as it’s a dbus error otherwise.

# File lib/ravn/bluez.rb, line 208
def stop_scanning
        self.log.info "Stopping scan."
        self.adapter.StopDiscovery if self.adapter[ 'Discovering' ]
end
stop_socket()

Unregister the serial agent and cleanly shut down the profile event loop.

# File lib/ravn/bluez.rb, line 286
def stop_socket
        return unless @profile_loop
        @profile_loop.stop
        self.profile_manager.UnregisterProfile( SERIAL_DBUS_PATH )
rescue DBus::Error => err
        self.log.error "Error while stopping socket: %s" % [ err.message ]
end
unpair( path )

Removes cached devices and any previous pairing state.

# File lib/ravn/bluez.rb, line 296
def unpair( path )
        self.adapter.RemoveDevice( path )
end