WearOSWatch class

A class representing an Android WearOS device, currently targetting a Samsung Galaxy Watch over bluetooth. (This could theoretically be used with any bluetooth connected Android display.)

Constants

MAX_DRIFT_TIME

This constant indicates the maximum difference (in seconds) that the watch time can be from the compute pack time before a warning is emitted.

WATCH_EVENT_FILTER

A specific list of events that are to be sent to the watch

Attributes

gps_positions R

The hash of GPS position events to send coalesced on a timer.

gps_send_timer R

The TimerTask that sends aggregated GPS data on an interval.

profile_server_socket R

The object’s profile server socket

socket R

A SocketBroker that manages IO for the socket

socket_broker R

A SocketBroker that manages IO for the socket

update_timer R

The TimerTask that updates the current state periodically.

Public Class Methods

new( *args )

Create a new HAL device for talking to a WearOS watch.

# File lib/ravn/hal/device/wearos_watch.rb, line 77
def initialize( *args )
        @gps_positions  = {}
        @gps_send_timer = self.create_gps_send_timer
        @update_timer   = self.create_update_timer

        @socket_broker  = nil

rescue => err
        self.log.error "%p while initializing: %s" % [ err.class, err.full_message ]
ensure
        super
end

Public Instance Methods

call_interval_handlers( * )

Timer callback – call the Pushdown interval handlers for the state.

# File lib/ravn/hal/device/wearos_watch.rb, line 353
def call_interval_handlers( * )
        self.shadow_update_state
        self.update_state
end
check_broker()

Check the status of the SocketBroker, clearing it if it has terminated.

# File lib/ravn/hal/device/wearos_watch.rb, line 134
def check_broker
        if @socket_broker&.ask!( :terminated? )
                self.log.warn "My socket broker has terminated.  Clearing!"
                @socket_broker = nil
        end
end
create_gps_send_timer()

Create a Concurrent::TimerTask that will periodically send aggregated GPS data to the watch.

# File lib/ravn/hal/device/wearos_watch.rb, line 414
def create_gps_send_timer
        timer = Concurrent::TimerTask.new( &self.method(:send_gps_messages) )
        timer.execution_interval = self.class.gps_update_frequency
        timer.add_observer( Ravn::LoggingTaskObserver.new(:gps_send_timer) )

        return timer
end
create_update_timer()

Create a Concurrent::TimerTask that will call the state stack’s interval callbacks.

# File lib/ravn/hal/device/wearos_watch.rb, line 425
def create_update_timer
        timer = Concurrent::TimerTask.new( &self.method(:call_interval_handlers) )
        timer.execution_interval = self.class.state_update_frequency
        timer.add_observer( Ravn::LoggingTaskObserver.new(:update_timer) )

        return timer
end
device_path()

The bluez device path of the WearOS watch.

# File lib/ravn/hal/device/wearos_watch.rb, line 118
stateful_variable :device_path
gather_device_info()

Return a Hash that contains information describing this device for intra-device communication; overridden to set the correct vendor.

# File lib/ravn/hal/device/wearos_watch.rb, line 237
def gather_device_info
        return super.merge( vendor: 'samsung' )
end
handle_connected_client( path_and_fd )

Callback actions for when a watch opens an RFCOMM socket to us.

# File lib/ravn/hal/device/wearos_watch.rb, line 194
def handle_connected_client( path_and_fd )
        path, fd = *path_and_fd
        self.log.info "New client connection from %p" % [ path ]
        socket = Socket.for_fd( fd )
        @socket_broker = Ravn::SocketBroker.spawn!( :sppsocket, socket )
end
handle_disconnected_client( path )

Callback actions for when an active socket fails, due to client disconnection or overt error.

# File lib/ravn/hal/device/wearos_watch.rb, line 204
def handle_disconnected_client( path )
        self.log.warn "Lost client connection: %p" % [ path ]
        @socket_broker&.ask!( :terminate!, "Client disconnected." )
        @socket_broker = nil
end
handle_incoming_message( object )

Create a message out of the incoming object and publish it.

# File lib/ravn/hal/device/wearos_watch.rb, line 331
def handle_incoming_message( object )
        raise "malformed message %p; not a Hash" unless object.is_a?( Hash )

        object = symbolify_keys( object )
        type = object.delete( :type ) or
                raise "malformed message %p; no type specified" % [ object ]
        current_time = Ravn.time
        message_time = object[ :time ]

        self.log.warn "Watch time drift is greater than 5 minutes" if
                ( ( current_time - message_time ).to_i.abs > MAX_DRIFT_TIME )

        object.delete( :time ) # remove the watch time, the message
                                                   # will add it's own
        message = Ravn::HAL::Message.new( type, object )

        self.log.info "Got a message from the watch: %p" % [ message ]
        self.filter_up( message )
end
handle_terminate()

Shutdown the dbus event handler.

# File lib/ravn/hal/device/wearos_watch.rb, line 449
def handle_terminate
        self.stop
end
initial_state_data()

Pushdown API: Provide this device instance as state data.

# File lib/ravn/hal/device/wearos_watch.rb, line 122
def initial_state_data
        return self
end
is_paired?()

Predicate convenience method for state machine use.

# File lib/ravn/hal/device/wearos_watch.rb, line 128
def is_paired?
        return Ravn::Bluez.device_paired?( self.device_path )
end
on_event( data )

Concurrent::Actor API — Handle lifecycle events.

# File lib/ravn/hal/device/wearos_watch.rb, line 435
def on_event( data )
        event, *args = data

        self.log.debug "Handling %p event via the state." % [ event ]
        self.handle_state_event( event, *args )

rescue => err
        self.log.error "%p while handling event: %s" % [ err.class, err.full_message ]
ensure
        super
end
pair()

Initiate a pairing request, targetting the current device_path.

# File lib/ravn/hal/device/wearos_watch.rb, line 174
def pair
        self.log.warn "Attempting to pair with device at path: %p" % [ self.device_path ]
        return unless self.device_path
        return Ravn::Bluez.pair( self.device_path )
end
propagate_message( * )

Block normal message propagation to child actors.

# File lib/ravn/hal/device/wearos_watch.rb, line 257
def propagate_message( * )
        # No-op
end
relay_finished_message( message )
# File lib/ravn/hal/device/wearos_watch.rb, line 277
def relay_finished_message( message )
        self.log.debug "Sending a %p message to the watch: %p" % [ message.type, message.fields ]
        self.handle_state_event( :write, message )
end
relay_local_message( message )
# File lib/ravn/hal/device/wearos_watch.rb, line 285
def relay_local_message( message )
        if !message.callsign || message.callsign == Ravn::BDE.callsign
                self.relay_message( message )
        end
end
relay_message( message )
# File lib/ravn/hal/device/wearos_watch.rb, line 264
def relay_message( message )
        self.log.debug "Sending a %p message to the watch" % [ message.type ]
        self.log.debug "  message: %p" % [ message ]

        data = message.fields.merge( type: message.type )
        data.freeze

        self.handle_state_event( :write, data )
end
relay_timed_message( message )
# File lib/ravn/hal/device/wearos_watch.rb, line 294
def relay_timed_message( message )
        self.log.debug "Caching a %p message for the socket" % [ message.type ]
        self.gps_positions[ message.callsign ] = message.pos
end
send_gps_messages( * )

Send all current GPS network events to the socket.

# File lib/ravn/hal/device/wearos_watch.rb, line 301
def send_gps_messages( * )
        return if self.gps_positions.empty?
        data = {
                id: Ravn.uuid.generate,
                time: Ravn.time,
                type: 'net.gps.positions',
                data: self.gps_positions.dup
        }
        self.gps_positions.clear
        data.freeze

        self.log.debug "Sending batched GPS data: %p" % [ data ]
        self.handle_state_event( :write, data )
end
send_startup_message()

Send burst data to the paired device via the transmit channel. Raises if the transmit channel has not been opened.

# File lib/ravn/hal/device/wearos_watch.rb, line 319
def send_startup_message
        message = self.startup_message or raise "No startup message has been set yet."
        type = message.type
        data = message.fields.merge( type: type )
        data = stringify_keys( data )

        self.log.info "Sending startup message: %p." % [ data ]
        self.handle_state_event( :write, data )
end
setup_interface_hooks()

Install callback behaviors for bluez interface additions and removals. FIXME: This needs to be re-addressed when we have a working dbus event loop.

# File lib/ravn/hal/device/wearos_watch.rb, line 401
def setup_interface_hooks
        Ravn::Bluez.on_interface_added do |path, interface|
                self.parent.tell( interface_added: path )
        end

        Ravn::Bluez.on_interface_removed do |path, interface|
                self.parent.tell( interface_removed: path )
        end
end
start()

Start the WearOSWatch device.

# File lib/ravn/hal/device/wearos_watch.rb, line 212
def start
        super

        # FIXME: Need to investigate Ruby::DBus threading socket timeouts
        # before re-enabling the DBus async events.
        #
        # self.setup_interface_hooks
        # Ravn::Bluez.start_dbus
        Ravn::Bluez.setup_pairing_callback

        # Store hardcoded config mac address if present.
        #
        if ( mac = self.class.bluetooth_address )
                self.log.info "Overriding watch macaddress %p from configuration." % [ mac ]
                self.device_path = Ravn::Bluez.macaddress_to_devpath( mac )
        end

        self.log.info "Starting GPS and update timer."
        self.update_timer.execute
        self.gps_send_timer.execute
end
start_scanning()

Ask bluez to find broadcasting Samsung watches.

NOTE: This seems like a useless abstraction to Ravn::Bluez, but it remains here if we decide we’d like to filter discovered devices.

# File lib/ravn/hal/device/wearos_watch.rb, line 147
def start_scanning
        Ravn::Bluez.start_scanning
end
start_socket_callback()

Install the bluez SPP profile.

# File lib/ravn/hal/device/wearos_watch.rb, line 159
def start_socket_callback
        self.log.info "Starting the profile socket server."
        @profile_server_thread = Ravn::Bluez.start_socket( self.reference )
end
stop()

Stop all timers and dbus event loops.

# File lib/ravn/hal/device/wearos_watch.rb, line 243
def stop
        self.log.info "Stopping GPS and update timer."
        self.update_timer.shutdown
        self.gps_send_timer.shutdown

        self.stop_scanning
        self.stop_socket_callback
        Ravn::Bluez.stop_dbus

        super
end
stop_scanning()

Stop any open bluez scans.

# File lib/ravn/hal/device/wearos_watch.rb, line 153
def stop_scanning
        Ravn::Bluez.stop_scanning
end
stop_socket_callback()

Safely stop the socket event loop.

# File lib/ravn/hal/device/wearos_watch.rb, line 166
def stop_socket_callback
        self.log.info "Stopping the profile socket server."
        Ravn::Bluez.stop_socket
        @profile_server_thread.wait( 5 )
end
unpair()

Break any existing bond to the current device_path.

# File lib/ravn/hal/device/wearos_watch.rb, line 182
def unpair
        return unless self.device_path
        self.log.warn "Unpairing from %s" % [ self.device_path ]
        Ravn::Bluez.unpair( self.device_path )

        self.socket_broker&.ask!( :terminate! )
        self.socket_broker = nil
        self.device_path = nil
end
validate_data( value )

Ensure the given value is serializable, raising an error if it isn’t.

# File lib/ravn/hal/device/wearos_watch.rb, line 377
def validate_data( value )
        self.log.debug "Validating: %p" % [ value ]
        case value
        when Array
                value.each {|subval| self.validate_data(subval) }

        when Hash
                value.each do |key, val|
                        raise "Unserializable key %p" % [ key ] unless
                                key.is_a?( String ) || key.is_a?( Symbol )
                        self.validate_data( val )
                end

        when String, Symbol, Numeric, NilClass, TrueClass, FalseClass
                # No-op
        else
                raise "Unserializable value %p" % [ value ]
        end
end
write( object )

Queue up the given object for writing if the current state allows it.

# File lib/ravn/hal/device/wearos_watch.rb, line 360
def write( object )
        if object.respond_to?( :fields )
                type = object.type
                data = object.fields.merge( type: type )

                self.validate_data( data )

                self.log.debug "Writing fields to the watch: %p" % [ data ]
                self.socket_broker&.tell( data )
        else
                self.log.debug "Writing an object to the watch: %p" % [ object ]
                self.socket_broker&.tell( object )
        end
end