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.)
- 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
- 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.
Create a new HAL device for talking to a WearOS watch.
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
call_interval_handlers( * )
Timer callback – call the Pushdown interval handlers for the state.
def call_interval_handlers( * )
self.shadow_update_state
self.update_state
end
Check the status of the SocketBroker, clearing it if it has terminated.
def check_broker
if @socket_broker&.ask!( :terminated? )
self.log.warn "My socket broker has terminated. Clearing!"
@socket_broker = nil
end
end
Create a Concurrent::TimerTask that will periodically send aggregated GPS data to the watch.
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 a Concurrent::TimerTask that will call the state stack’s interval callbacks.
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
The bluez device path of the WearOS watch.
stateful_variable :device_path
Return a Hash that contains information describing this device for intra-device communication; overridden to set the correct vendor
.
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.
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.
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.
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 )
message = Ravn::HAL::Message.new( type, object )
self.log.info "Got a message from the watch: %p" % [ message ]
self.filter_up( message )
end
Shutdown the dbus event handler.
def handle_terminate
self.stop
end
Pushdown API: Provide this device instance as state data.
def initial_state_data
return self
end
Predicate convenience method for state machine use.
def is_paired?
return Ravn::Bluez.device_paired?( self.device_path )
end
Concurrent::Actor API — Handle lifecycle events.
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
Initiate a pairing request, targetting the current device_path
.
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
Block normal message propagation to child actors.
def propagate_message( * )
end
relay_finished_message( message )
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 )
def relay_local_message( message )
if !message.callsign || message.callsign == Ravn::BDE.callsign
self.relay_message( message )
end
end
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 )
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 all current GPS network events to the socket.
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 burst data to the paired device via the transmit channel. Raises if the transmit channel has not been opened.
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
Install callback behaviors for bluez interface additions and removals. FIXME: This needs to be re-addressed when we have a working dbus event loop.
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 the WearOSWatch device.
def start
super
Ravn::Bluez.setup_pairing_callback
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
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.
def start_scanning
Ravn::Bluez.start_scanning
end
Install the bluez SPP profile.
def start_socket_callback
self.log.info "Starting the profile socket server."
@profile_server_thread = Ravn::Bluez.start_socket( self.reference )
end
Stop all timers and dbus event loops.
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 any open bluez scans.
def stop_scanning
Ravn::Bluez.stop_scanning
end
Safely stop the socket event loop.
def stop_socket_callback
self.log.info "Stopping the profile socket server."
Ravn::Bluez.stop_socket
@profile_server_thread.wait( 5 )
end
Break any existing bond to the current device_path
.
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
Ensure the given value
is serializable, raising an error if it isn’t.
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
else
raise "Unserializable value %p" % [ value ]
end
end
Queue up the given object
for writing if the current state allows it.
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