class EventSourceMixin(object): """ A convenient mixin to save on repeatedly exposing generic event handler functionality. """ def __init__(self): self.__handler = ThreadedEventHandler() def register_handler(self, event, handler): """ Registers a handler to be triggered by an event :param event: The event to handle :param handler: The handler callable. :return: A handle that can be used to unregister the handler. """ return self.__handler.register_handler(event, handler) def unregister_handler(self, handle): """ Unregisters an event handler. :param handle: The handle returned from :meth:`register_handler` """ self.__handler.unregister_handler(handle) def wait_for_event(self, event, timeout=10): """ Block waiting for the given event. Returns the event params. :param event: The event to handle. :return: The event params. :param timeout: The maximum time to wait before raising :exc:`.TimeoutError`. """ return self.__handler.wait_for_event(event, timeout=timeout) def _broadcast_event(self, event, *args): return self.__handler.broadcast_event(event, *args)
class PebbleConnection(object): """ PebbleConnection represents the connection to a pebble; all interaction with a pebble goes through it. :param transport: The underlying transport layer to communicate with the Pebble. :type transport: BaseTransport :param log_packet_level: If not None, the log level at which to log decoded messages sent and received. :type log_packet_level: int :param log_protocol_level: int If not None, the log level at which to log raw messages sent and received. :type log_protocol_level: int """ def __init__(self, transport, log_protocol_level=None, log_packet_level=None): assert isinstance(transport, BaseTransport) self.transport = transport self.pending_bytes = b'' self.event_handler = ThreadedEventHandler() self._register_internal_handlers() self._watch_info = None self._watch_model = None self.log_protocol_level = log_protocol_level self.log_packet_level = log_packet_level def connect(self): """ Synchronously initialises a connection to the Pebble. Once it returns, a valid connection will be open. """ self.transport.connect() @property def connected(self): """ :return: ``True`` if currently connected to a Pebble; otherwise ``False``. """ return self.transport.connected def pump_reader(self): """ Synchronously reads one message from the watch, blocking until a message is available. All events caused by the message read will be processed before this method returns. .. note:: You usually don't need to invoke this method manually; instead, see :meth:`run_sync` and :meth:`run_async`. """ origin, message = self.transport.read_packet() if isinstance(origin, MessageTargetWatch): self._handle_watch_message(message) else: self._broadcast_transport_message(origin, message) def run_sync(self): """ Runs the message loop until the Pebble disconnects. This method will block until the watch disconnects or a fatal error occurs. For alternatives that don't block forever, see :meth:`pump_reader` and :meth:`run_async`. """ while self.connected: try: self.pump_reader() except PacketDecodeError as e: logger.warning("Packet decode failed: %s", e) except ConnectionError: break def run_async(self): """ Spawns a new thread that runs the message loop until the Pebble disconnects. ``run_async`` will call :meth:`fetch_watch_info` on your behalf, and block until it receives a response. """ thread = threading.Thread(target=self.run_sync) thread.daemon = True thread.name = "PebbleConnection" thread.start() self.fetch_watch_info() def _handle_watch_message(self, message): """ Processes a binary message received from the watch and broadcasts the relevant events. :param message: A raw message from the watch, without any transport framing. :type message: bytes """ if self.log_protocol_level is not None: logger.log(self.log_protocol_level, "<- %s", hexlify(message).decode()) message = self.pending_bytes + message while len(message) >= 4: try: packet, length = PebblePacket.parse_message(message) except IncompleteMessage: self.pending_bytes = message break except: # At this point we've failed to deconstruct the message via normal means, but we don't want to end # up permanently desynced (because we wiped a partial message), nor do we want to get stuck (because # we didn't wipe anything). We therefore parse the packet length manually and skip ahead that far. # If the expected length is 0, we wipe everything to ensure forward motion (but we are quite probably # screwed). expected_length, = struct.unpack('!H', message[:2]) if expected_length == 0: self.pending_bytes = b'' else: self.pending_bytes = message[expected_length + 4:] raise self.event_handler.broadcast_event("raw_inbound", message[:length]) if self.log_packet_level is not None: logger.log(self.log_packet_level, "<- %s", packet) message = message[length:] self.event_handler.broadcast_event((_EventType.Watch, type(packet)), packet) if length == 0: break self.pending_bytes = message def _broadcast_transport_message(self, origin, message): """ Broadcasts an event originating from a transport that does not represent a message from the Pebble. :param origin: The type of transport responsible for the message. :type origin: .MessageTarget :param message: The message from the transport """ self.event_handler.broadcast_event((_EventType.Transport, type(origin), type(message)), message) def register_transport_endpoint(self, origin, message_type, handler): """ Register a handler for a message received from a transport that does not indicate a message from the connected Pebble. :param origin: The type of :class:`.MessageTarget` that triggers the message :param message_type: The class of the message that is expected. :param handler: A callback to be called when a message is received. :type handler: callable :return: A handle that can be passed to :meth:`unregister_endpoint` to remove the handler. """ return self.event_handler.register_handler((_EventType.Transport, origin, message_type), handler) def register_endpoint(self, endpoint, handler): """ Register a handler for a message received from the Pebble. :param endpoint: The type of :class:`.PebblePacket` that is being listened for. :type endpoint: .PacketType :param handler: A callback to be called when a message is received. :type handler: callable :return: A handle that can be passed to :meth:`unregister_endpoint` to remove the handler. """ return self.event_handler.register_handler((_EventType.Watch, endpoint), handler) def register_raw_outbound_handler(self, handler): """ Register a handler for all outgoing messages to be sent to the Pebble. Transport framing is not included. :param handler: A callback to be called when any message is received. :type handler: callable :return: A handle that can be passed to :meth:`unregister_endpoint` to remove the handler. """ return self.event_handler.register_handler("raw_outbound", handler) def register_raw_inbound_handler(self, handler): """ Register a handler for all outgoing messages received from the Pebble. Transport framing is not included. In most cases you should not need to use this; consider using :meth:`register_endpoint` instead. :param handler: A callback to be called when any message is received. :type handler: callable :return: A handle that can be passed to :meth:`unregister_endpoint` to remove the handler. """ return self.event_handler.register_handler("raw_inbound", handler) def unregister_endpoint(self, handle): """ Removes a handler registered by :meth:`register_transport_endpoint`, :meth:`register_endpoint`, :meth:`register_raw_outbound_handler` or :meth:`register_raw_inbound_handler`. :param handle: A handle returned by the register call to be undone. """ return self.event_handler.unregister_handler(handle) def read_from_endpoint(self, endpoint, timeout=10): """ Blocking read from an endpoint. Will block until a message is received, or it times out. Also see :meth:`get_endpoint_queue` if you are considering calling this in a loop. .. warning:: Avoid calling this method from an endpoint callback; doing so is likely to lead to deadlock. .. note:: If you're reading a response to a message you just sent, :meth:`send_and_read` might be more appropriate. :param endpoint: The endpoint to read from. :type endpoint: .PacketType :param timeout: The maximum time to wait before raising :exc:`.TimeoutError`. :return: The message read from the endpoint; of the same type as passed to ``endpoint``. """ return self.event_handler.wait_for_event((_EventType.Watch, endpoint), timeout=timeout) def get_endpoint_queue(self, endpoint): """ Returns a :class:`.BaseEventQueue` from which messages to the given ``endpoint`` can be read. This is useful if you need to make sure that you receive all messages to an endpoint, without risking dropping some due to time in between :meth:`read_from_endpoint` calls. :param endpoint: The endpoint to read from :type endpoint: .PacketType :return: """ return self.event_handler.queue_events((_EventType.Watch, endpoint)) def read_transport_message(self, origin, message_type, timeout=10): """ Blocking read of a transport message that does not indicate a message from the Pebble. Will block until a message is received, or it times out. .. warning:: Avoid calling this method from an endpoint callback; doing so is likely to lead to deadlock. :param origin: The type of :class:`.MessageTarget` that triggers the message. :param message_type: The class of the message to read from the transport. :param timeout: The maximum time to wait before raising :exc:`.TimeoutError`. :return: The object read from the transport; of the same type as passed to ``message_type``. """ return self.event_handler.wait_for_event((_EventType.Transport, origin, message_type), timeout=timeout) def send_packet(self, packet): """ Sends a message to the Pebble. :param packet: The message to send. :type packet: .PebblePacket """ if self.log_packet_level: logger.log(self.log_packet_level, "-> %s", packet) serialised = packet.serialise_packet() self.event_handler.broadcast_event("raw_outbound", serialised) self.send_raw(serialised) def send_and_read(self, packet, endpoint, timeout=10): """ Sends a packet, then returns the next response received from that endpoint. This method sets up a listener before it actually sends the message, avoiding a potential race. .. warning:: Avoid calling this method from an endpoint callback; doing so is likely to lead to deadlock. :param packet: The message to send. :type packet: .PebblePacket :param endpoint: The endpoint to read from :type endpoint: .PacketType :param timeout: The maximum time to wait before raising :exc:`.TimeoutError`. :return: The message read from the endpoint; of the same type as passed to ``endpoint``. """ queue = self.get_endpoint_queue(endpoint) self.send_packet(packet) try: return queue.get(timeout=timeout) finally: queue.close() def send_raw(self, message): """ Sends a raw binary message to the Pebble. No processing will be applied, but any transport framing should be omitted. :param message: The message to send to the pebble. :type message: bytes """ if self.log_protocol_level: logger.log(self.log_protocol_level, "-> %s", hexlify(message).decode()) self.transport.send_packet(message) def _register_internal_handlers(self): if self.transport.must_initialise: self.register_endpoint(PhoneAppVersion, self._app_version_response) def _app_version_response(self, packet): packet = PhoneAppVersion(message=AppVersionResponse( protocol_version=0xFFFFFFFF, session_caps=0x80000000, platform_flags=50, response_version=2, major_version=3, minor_version=0, bugfix_version=0, protocol_caps=0xFFFFFFFFFFFFFFFF )) self.send_packet(packet) def fetch_watch_info(self): """ This method should be called before accessing :attr:`watch_info`, :attr:`firmware_version` or :attr:`watch_platform`. Blocks until it has fetched the required information. """ self._watch_info = self.send_and_read(WatchVersion(data=WatchVersionRequest()), WatchVersion).data @property def watch_info(self): """ Returns information on the connected Pebble, including its firmware version, language, capabilities, etc. .. note: This is a blocking call if :meth:`fetch_watch_info` has not yet been called, which could lead to deadlock if called in an endpoint callback. :rtype: .WatchVersionResponse """ if self._watch_info is None: self.fetch_watch_info() return self._watch_info @property def firmware_version(self): """ Provides information on the connected Pebble, including its firmware version, language, capabilities, etc. .. note: This is a blocking call if :meth:`fetch_watch_info` has not yet been called, which could lead to deadlock if called in an endpoint callback. :rtype: .WatchVersionResponse """ version = self.watch_info.running.version_tag[1:] parts = version.split('-', 1) points = [int(x) for x in parts[0].split('.')] while len(points) < 3: points.append(0) if len(parts) == 2: suffix = parts[1] else: suffix = '' return FirmwareVersion(*(points + [suffix])) @property def watch_model(self): """ :return: The model of the watch. :rtype: ~libpebble2.protocol.system.Model """ if self._watch_model is None: info_bytes = self.send_and_read(WatchModel(data=ModelRequest()), WatchModel).data.data if len(info_bytes) == 4: self._watch_model, = struct.unpack('>I', info_bytes) else: self._watch_model = Model.Unknown return self._watch_model @property def watch_platform(self): """ A string naming the platform of the watch ('aplite', 'basalt', 'chalk', or 'unknown'). .. note: This is a blocking call if :meth:`fetch_watch_info` has not yet been called, which could lead to deadlock if called in an endpoint callback. :rtype: str """ return PebbleHardware.hardware_platform(self.watch_info.running.hardware_platform)
class PebbleConnection(object): """ PebbleConnection represents the connection to a pebble; all interaction with a pebble goes through it. :param transport: The underlying transport layer to communicate with the Pebble. :type transport: BaseTransport :param log_packet_level: If not None, the log level at which to log decoded messages sent and received. :type log_packet_level: int :param log_protocol_level: int If not None, the log level at which to log raw messages sent and received. :type log_protocol_level: int """ def __init__(self, transport, log_protocol_level=None, log_packet_level=None): assert isinstance(transport, BaseTransport) self.transport = transport self.event_handler = ThreadedEventHandler() self._register_internal_handlers() self._watch_info = None self._watch_model = None self.log_protocol_level = log_protocol_level self.log_packet_level = log_packet_level def connect(self): """ Synchronously initialises a connection to the Pebble. Once it returns, a valid connection will be open. """ self.transport.connect() @property def connected(self): """ :return: ``True`` if currently connected to a Pebble; otherwise ``False``. """ return self.transport.connected def pump_reader(self): """ Synchronously reads one message from the watch, blocking until a message is available. All events caused by the message read will be processed before this method returns. .. note:: You usually don't need to invoke this method manually; instead, see :meth:`run_sync` and :meth:`run_async`. """ origin, message = self.transport.read_packet() if isinstance(origin, MessageTargetWatch): self._handle_watch_message(message) else: self._broadcast_transport_message(origin, message) def run_sync(self): """ Runs the message loop until the Pebble disconnects. This method will block until the watch disconnects or a fatal error occurs. For alternatives that don't block forever, see :meth:`pump_reader` and :meth:`run_async`. """ while self.connected: try: self.pump_reader() except PacketDecodeError as e: logger.warning("Packet decode failed: %s", e) except ConnectionError: break def run_async(self): """ Spawns a new thread that runs the message loop until the Pebble disconnects. ``run_async`` will call :meth:`fetch_watch_info` on your behalf, and block until it receives a response. """ thread = threading.Thread(target=self.run_sync) thread.daemon = True thread.name = "PebbleConnection" thread.start() self.fetch_watch_info() def _handle_watch_message(self, message): """ Processes a binary message received from the watch and broadcasts the relevant events. :param message: A raw message from the watch, without any transport framing. :type message: bytes """ while len(message) >= 4: if self.log_protocol_level is not None: logger.log(self.log_protocol_level, "<- %s", hexlify(message).decode()) packet, length = PebblePacket.parse_message(message) self.event_handler.broadcast_event("raw_inbound", message[:length]) if self.log_packet_level is not None: logger.log(self.log_packet_level, "<- %s", packet) message = message[length:] self.event_handler.broadcast_event( (_EventType.Watch, type(packet)), packet) if length == 0: break def _broadcast_transport_message(self, origin, message): """ Broadcasts an event originating from a transport that does not represent a message from the Pebble. :param origin: The type of transport responsible for the message. :type origin: .MessageTarget :param message: The message from the transport """ self.event_handler.broadcast_event( (_EventType.Transport, type(origin), type(message)), message) def register_transport_endpoint(self, origin, message_type, handler): """ Register a handler for a message received from a transport that does not indicate a message from the connected Pebble. :param origin: The type of :class:`.MessageTarget` that triggers the message :param message_type: The class of the message that is expected. :param handler: A callback to be called when a message is received. :type handler: callable :return: A handle that can be passed to :meth:`unregister_endpoint` to remove the handler. """ return self.event_handler.register_handler( (_EventType.Transport, origin, message_type), handler) def register_endpoint(self, endpoint, handler): """ Register a handler for a message received from the Pebble. :param endpoint: The type of :class:`.PebblePacket` that is being listened for. :type endpoint: .PacketType :param handler: A callback to be called when a message is received. :type handler: callable :return: A handle that can be passed to :meth:`unregister_endpoint` to remove the handler. """ return self.event_handler.register_handler( (_EventType.Watch, endpoint), handler) def register_raw_outbound_handler(self, handler): """ Register a handler for all outgoing messages to be sent to the Pebble. Transport framing is not included. :param handler: A callback to be called when any message is received. :type handler: callable :return: A handle that can be passed to :meth:`unregister_endpoint` to remove the handler. """ return self.event_handler.register_handler("raw_outbound", handler) def register_raw_inbound_handler(self, handler): """ Register a handler for all outgoing messages received from the Pebble. Transport framing is not included. In most cases you should not need to use this; consider using :meth:`register_endpoint` instead. :param handler: A callback to be called when any message is received. :type handler: callable :return: A handle that can be passed to :meth:`unregister_endpoint` to remove the handler. """ return self.event_handler.register_handler("raw_inbound", handler) def unregister_endpoint(self, handle): """ Removes a handler registered by :meth:`register_transport_endpoint`, :meth:`register_endpoint`, :meth:`register_raw_outbound_handler` or :meth:`register_raw_inbound_handler`. :param handle: A handle returned by the register call to be undone. """ return self.event_handler.unregister_handler(handle) def read_from_endpoint(self, endpoint, timeout=10): """ Blocking read from an endpoint. Will block until a message is received, or it times out. Also see :meth:`get_endpoint_queue` if you are considering calling this in a loop. .. warning:: Avoid calling this method from an endpoint callback; doing so is likely to lead to deadlock. :param endpoint: The endpoint to read from. :type endpoint: .PacketType :param timeout: The maximum time to wait before raising :exc:`.TimeoutError`. :return: The message read from the endpoint; of the same type as passed to ``endpoint``. """ return self.event_handler.wait_for_event((_EventType.Watch, endpoint), timeout=timeout) def get_endpoint_queue(self, endpoint): """ Returns a :class:`.BaseEventQueue` from which messages to the given ``endpoint`` can be read. This is useful if you need to make sure that you receive all messages to an endpoint, without risking dropping some due to time in between :meth:`read_from_endpoint` calls. :param endpoint: The endpoint to read from :type endpoint: .PacketType :return: """ return self.event_handler.queue_events((_EventType.Watch, endpoint)) def read_transport_message(self, origin, message_type, timeout=10): """ Blocking read of a transport message that does not indicate a message from the Pebble. Will block until a message is received, or it times out. .. warning:: Avoid calling this method from an endpoint callback; doing so is likely to lead to deadlock. :param origin: The type of :class:`.MessageTarget` that triggers the message. :param message_type: The class of the message to read from the transport. :param timeout: The maximum time to wait before raising :exc:`.TimeoutError`. :return: The object read from the transport; of the same type as passed to ``message_type``. """ return self.event_handler.wait_for_event( (_EventType.Transport, origin, message_type), timeout=timeout) def send_packet(self, packet): """ Sends a message to the Pebble. :param packet: The message to send. :type packet: .PebblePacket """ if self.log_packet_level: logger.log(self.log_packet_level, "-> %s", packet) serialised = packet.serialise_packet() self.event_handler.broadcast_event("raw_outbound", serialised) self.send_raw(serialised) def send_raw(self, message): """ Sends a raw binary message to the Pebble. No processing will be applied, but any transport framing should be omitted. :param message: The message to send to the pebble. :type message: bytes """ if self.log_protocol_level: logger.log(self.log_protocol_level, "-> %s", hexlify(message).decode()) self.transport.send_packet(message) def _register_internal_handlers(self): if self.transport.must_initialise: self.register_endpoint(PhoneAppVersion, self._app_version_response) def _app_version_response(self, packet): packet = PhoneAppVersion( message=AppVersionResponse(protocol_version=0xFFFFFFFF, session_caps=0x80000000, platform_flags=50, response_version=2, major_version=3, minor_version=0, bugfix_version=0, protocol_caps=0xFFFFFFFFFFFFFFFF)) self.send_packet(packet) def fetch_watch_info(self): """ This method should be called before accessing :attr:`watch_info`, :attr:`firmware_version` or :attr:`watch_platform`. Blocks until it has fetched the required information. """ self.send_packet(WatchVersion(data=WatchVersionRequest())) self._watch_info = self.read_from_endpoint(WatchVersion).data @property def watch_info(self): """ Returns information on the connected Pebble, including its firmware version, language, capabilities, etc. .. note: This is a blocking call if :meth:`fetch_watch_info` has not yet been called, which could lead to deadlock if called in an endpoint callback. :rtype: .WatchVersionResponse """ if self._watch_info is None: self.fetch_watch_info() return self._watch_info @property def firmware_version(self): """ Provides information on the connected Pebble, including its firmware version, language, capabilities, etc. .. note: This is a blocking call if :meth:`fetch_watch_info` has not yet been called, which could lead to deadlock if called in an endpoint callback. :rtype: .WatchVersionResponse """ version = self.watch_info.running.version_tag[1:] parts = version.split('-', 1) points = [int(x) for x in parts[0].split('.')] while len(points) < 3: points.append(0) if len(parts) == 2: suffix = parts[1] else: suffix = '' return FirmwareVersion(*(points + [suffix])) @property def watch_model(self): """ :return: The model of the watch. :rtype: ~libpebble2.protocol.system.Model """ if self._watch_model is None: self.send_packet(WatchModel(data=ModelRequest())) info_bytes = self.read_from_endpoint(WatchModel).data.data if len(info_bytes) == 4: self._watch_model = struct.unpack('>I', info_bytes) else: self._watch_model = Model.Unknown return self._watch_model @property def watch_platform(self): """ A string naming the platform of the watch ('aplite', 'basalt', or 'unknown'). .. note: This is a blocking call if :meth:`fetch_watch_info` has not yet been called, which could lead to deadlock if called in an endpoint callback. :rtype: str """ return PebbleHardware.hardware_platform( self.watch_info.running.hardware_platform)