libpebble2 0.0.29__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- libpebble2/__init__.py +8 -0
- libpebble2/communication/__init__.py +389 -0
- libpebble2/communication/transports/__init__.py +64 -0
- libpebble2/communication/transports/pulse.py +108 -0
- libpebble2/communication/transports/qemu/__init__.py +113 -0
- libpebble2/communication/transports/qemu/protocol.py +165 -0
- libpebble2/communication/transports/serial.py +58 -0
- libpebble2/communication/transports/websocket/__init__.py +74 -0
- libpebble2/communication/transports/websocket/protocol.py +160 -0
- libpebble2/events/__init__.py +93 -0
- libpebble2/events/mixin.py +43 -0
- libpebble2/events/threaded.py +84 -0
- libpebble2/exceptions.py +70 -0
- libpebble2/protocol/__init__.py +16 -0
- libpebble2/protocol/appglance.py +66 -0
- libpebble2/protocol/appmessage.py +88 -0
- libpebble2/protocol/apps.py +84 -0
- libpebble2/protocol/audio.py +33 -0
- libpebble2/protocol/base/__init__.py +234 -0
- libpebble2/protocol/base/types.py +635 -0
- libpebble2/protocol/blobdb.py +74 -0
- libpebble2/protocol/data_logging.py +86 -0
- libpebble2/protocol/legacy2.py +157 -0
- libpebble2/protocol/logs.py +69 -0
- libpebble2/protocol/meta.py +21 -0
- libpebble2/protocol/music.py +109 -0
- libpebble2/protocol/phone.py +80 -0
- libpebble2/protocol/screenshots.py +38 -0
- libpebble2/protocol/system.py +241 -0
- libpebble2/protocol/timeline.py +94 -0
- libpebble2/protocol/transfers.py +152 -0
- libpebble2/protocol/voice.py +160 -0
- libpebble2/services/__init__.py +1 -0
- libpebble2/services/appglances.py +41 -0
- libpebble2/services/appmessage.py +211 -0
- libpebble2/services/blobdb.py +164 -0
- libpebble2/services/data_logging.py +141 -0
- libpebble2/services/getbytes.py +85 -0
- libpebble2/services/install.py +161 -0
- libpebble2/services/notifications.py +97 -0
- libpebble2/services/putbytes.py +105 -0
- libpebble2/services/screenshot.py +93 -0
- libpebble2/services/voice.py +180 -0
- libpebble2/util/__init__.py +1 -0
- libpebble2/util/bundle.py +203 -0
- libpebble2/util/hardware.py +64 -0
- libpebble2/util/stm32_crc.py +43 -0
- libpebble2/version.py +4 -0
- libpebble2-0.0.29.dist-info/METADATA +69 -0
- libpebble2-0.0.29.dist-info/RECORD +54 -0
- libpebble2-0.0.29.dist-info/WHEEL +5 -0
- libpebble2-0.0.29.dist-info/licenses/LICENSE +21 -0
- libpebble2-0.0.29.dist-info/top_level.txt +1 -0
- libpebble2-0.0.29.dist-info/zip-safe +1 -0
libpebble2/__init__.py
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
from __future__ import absolute_import
|
|
2
|
+
|
|
3
|
+
__author__ = 'katharine'
|
|
4
|
+
|
|
5
|
+
from binascii import hexlify
|
|
6
|
+
from collections import namedtuple
|
|
7
|
+
from enum import Enum
|
|
8
|
+
import logging
|
|
9
|
+
import struct
|
|
10
|
+
import threading
|
|
11
|
+
|
|
12
|
+
from .transports import BaseTransport, MessageTargetWatch
|
|
13
|
+
from libpebble2.events.threaded import ThreadedEventHandler
|
|
14
|
+
from libpebble2.exceptions import PacketDecodeError, ConnectionError, IncompleteMessage
|
|
15
|
+
from libpebble2.protocol.base import PebblePacket, PacketType
|
|
16
|
+
from libpebble2.protocol.system import (PhoneAppVersion, AppVersionResponse, WatchVersion, WatchVersionRequest,
|
|
17
|
+
WatchVersionResponse, WatchModel, ModelRequest, Model)
|
|
18
|
+
from libpebble2.util.hardware import PebbleHardware
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger("libpebble2.communication")
|
|
21
|
+
|
|
22
|
+
_EventType = Enum('_EventType', ('Watch', 'Transport'))
|
|
23
|
+
|
|
24
|
+
FirmwareVersion = namedtuple('FirmwareVersion', ('major', 'minor', 'patch', 'suffix'))
|
|
25
|
+
"""
|
|
26
|
+
Represents a firmware version, in the format ``major.minor.patch-suffix``.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PebbleConnection(object):
|
|
31
|
+
"""
|
|
32
|
+
PebbleConnection represents the connection to a pebble; all interaction with a pebble goes through it.
|
|
33
|
+
|
|
34
|
+
:param transport: The underlying transport layer to communicate with the Pebble.
|
|
35
|
+
:type transport: BaseTransport
|
|
36
|
+
:param log_packet_level: If not None, the log level at which to log decoded messages sent and received.
|
|
37
|
+
:type log_packet_level: int
|
|
38
|
+
:param log_protocol_level: int If not None, the log level at which to log raw messages sent and received.
|
|
39
|
+
:type log_protocol_level: int
|
|
40
|
+
"""
|
|
41
|
+
def __init__(self, transport, log_protocol_level=None, log_packet_level=None):
|
|
42
|
+
assert isinstance(transport, BaseTransport)
|
|
43
|
+
self.transport = transport
|
|
44
|
+
self.pending_bytes = b''
|
|
45
|
+
self.event_handler = ThreadedEventHandler()
|
|
46
|
+
self._register_internal_handlers()
|
|
47
|
+
self._watch_info = None
|
|
48
|
+
self._watch_model = None
|
|
49
|
+
self.log_protocol_level = log_protocol_level
|
|
50
|
+
self.log_packet_level = log_packet_level
|
|
51
|
+
|
|
52
|
+
def connect(self):
|
|
53
|
+
"""
|
|
54
|
+
Synchronously initialises a connection to the Pebble. Once it returns, a valid connection will be open.
|
|
55
|
+
"""
|
|
56
|
+
self.transport.connect()
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def connected(self):
|
|
60
|
+
"""
|
|
61
|
+
:return: ``True`` if currently connected to a Pebble; otherwise ``False``.
|
|
62
|
+
"""
|
|
63
|
+
return self.transport.connected
|
|
64
|
+
|
|
65
|
+
def pump_reader(self):
|
|
66
|
+
"""
|
|
67
|
+
Synchronously reads one message from the watch, blocking until a message is available.
|
|
68
|
+
All events caused by the message read will be processed before this method returns.
|
|
69
|
+
|
|
70
|
+
.. note::
|
|
71
|
+
You usually don't need to invoke this method manually; instead, see :meth:`run_sync` and :meth:`run_async`.
|
|
72
|
+
"""
|
|
73
|
+
origin, message = self.transport.read_packet()
|
|
74
|
+
if isinstance(origin, MessageTargetWatch):
|
|
75
|
+
self._handle_watch_message(message)
|
|
76
|
+
else:
|
|
77
|
+
self._broadcast_transport_message(origin, message)
|
|
78
|
+
|
|
79
|
+
def run_sync(self):
|
|
80
|
+
"""
|
|
81
|
+
Runs the message loop until the Pebble disconnects. This method will block until the watch disconnects or
|
|
82
|
+
a fatal error occurs.
|
|
83
|
+
|
|
84
|
+
For alternatives that don't block forever, see :meth:`pump_reader` and :meth:`run_async`.
|
|
85
|
+
"""
|
|
86
|
+
while self.connected:
|
|
87
|
+
try:
|
|
88
|
+
self.pump_reader()
|
|
89
|
+
except PacketDecodeError as e:
|
|
90
|
+
logger.warning("Packet decode failed: %s", e)
|
|
91
|
+
except ConnectionError:
|
|
92
|
+
break
|
|
93
|
+
|
|
94
|
+
def run_async(self):
|
|
95
|
+
"""
|
|
96
|
+
Spawns a new thread that runs the message loop until the Pebble disconnects.
|
|
97
|
+
``run_async`` will call :meth:`fetch_watch_info` on your behalf, and block until it receives a response.
|
|
98
|
+
"""
|
|
99
|
+
thread = threading.Thread(target=self.run_sync)
|
|
100
|
+
thread.daemon = True
|
|
101
|
+
thread.name = "PebbleConnection"
|
|
102
|
+
thread.start()
|
|
103
|
+
self.fetch_watch_info()
|
|
104
|
+
|
|
105
|
+
def _handle_watch_message(self, message):
|
|
106
|
+
"""
|
|
107
|
+
Processes a binary message received from the watch and broadcasts the relevant events.
|
|
108
|
+
|
|
109
|
+
:param message: A raw message from the watch, without any transport framing.
|
|
110
|
+
:type message: bytes
|
|
111
|
+
"""
|
|
112
|
+
if self.log_protocol_level is not None:
|
|
113
|
+
logger.log(self.log_protocol_level, "<- %s", hexlify(message).decode())
|
|
114
|
+
message = self.pending_bytes + message
|
|
115
|
+
|
|
116
|
+
while len(message) >= 4:
|
|
117
|
+
try:
|
|
118
|
+
packet, length = PebblePacket.parse_message(message)
|
|
119
|
+
except IncompleteMessage:
|
|
120
|
+
self.pending_bytes = message
|
|
121
|
+
break
|
|
122
|
+
except:
|
|
123
|
+
# At this point we've failed to deconstruct the message via normal means, but we don't want to end
|
|
124
|
+
# up permanently desynced (because we wiped a partial message), nor do we want to get stuck (because
|
|
125
|
+
# we didn't wipe anything). We therefore parse the packet length manually and skip ahead that far.
|
|
126
|
+
# If the expected length is 0, we wipe everything to ensure forward motion (but we are quite probably
|
|
127
|
+
# screwed).
|
|
128
|
+
expected_length, = struct.unpack('!H', message[:2])
|
|
129
|
+
if expected_length == 0:
|
|
130
|
+
self.pending_bytes = b''
|
|
131
|
+
else:
|
|
132
|
+
self.pending_bytes = message[expected_length + 4:]
|
|
133
|
+
raise
|
|
134
|
+
|
|
135
|
+
self.event_handler.broadcast_event("raw_inbound", message[:length])
|
|
136
|
+
if self.log_packet_level is not None:
|
|
137
|
+
logger.log(self.log_packet_level, "<- %s", packet)
|
|
138
|
+
message = message[length:]
|
|
139
|
+
self.event_handler.broadcast_event((_EventType.Watch, type(packet)), packet)
|
|
140
|
+
if length == 0:
|
|
141
|
+
break
|
|
142
|
+
self.pending_bytes = message
|
|
143
|
+
|
|
144
|
+
def _broadcast_transport_message(self, origin, message):
|
|
145
|
+
"""
|
|
146
|
+
Broadcasts an event originating from a transport that does not represent a message from the Pebble.
|
|
147
|
+
|
|
148
|
+
:param origin: The type of transport responsible for the message.
|
|
149
|
+
:type origin: .MessageTarget
|
|
150
|
+
:param message: The message from the transport
|
|
151
|
+
"""
|
|
152
|
+
self.event_handler.broadcast_event((_EventType.Transport, type(origin), type(message)), message)
|
|
153
|
+
|
|
154
|
+
def register_transport_endpoint(self, origin, message_type, handler):
|
|
155
|
+
"""
|
|
156
|
+
Register a handler for a message received from a transport that does not indicate a message from the connected
|
|
157
|
+
Pebble.
|
|
158
|
+
|
|
159
|
+
:param origin: The type of :class:`.MessageTarget` that triggers the message
|
|
160
|
+
:param message_type: The class of the message that is expected.
|
|
161
|
+
:param handler: A callback to be called when a message is received.
|
|
162
|
+
:type handler: callable
|
|
163
|
+
:return: A handle that can be passed to :meth:`unregister_endpoint` to remove the handler.
|
|
164
|
+
"""
|
|
165
|
+
return self.event_handler.register_handler((_EventType.Transport, origin, message_type), handler)
|
|
166
|
+
|
|
167
|
+
def register_endpoint(self, endpoint, handler):
|
|
168
|
+
"""
|
|
169
|
+
Register a handler for a message received from the Pebble.
|
|
170
|
+
|
|
171
|
+
:param endpoint: The type of :class:`.PebblePacket` that is being listened for.
|
|
172
|
+
:type endpoint: .PacketType
|
|
173
|
+
:param handler: A callback to be called when a message is received.
|
|
174
|
+
:type handler: callable
|
|
175
|
+
:return: A handle that can be passed to :meth:`unregister_endpoint` to remove the handler.
|
|
176
|
+
"""
|
|
177
|
+
return self.event_handler.register_handler((_EventType.Watch, endpoint), handler)
|
|
178
|
+
|
|
179
|
+
def register_raw_outbound_handler(self, handler):
|
|
180
|
+
"""
|
|
181
|
+
Register a handler for all outgoing messages to be sent to the Pebble. Transport framing is not included.
|
|
182
|
+
|
|
183
|
+
:param handler: A callback to be called when any message is received.
|
|
184
|
+
:type handler: callable
|
|
185
|
+
:return: A handle that can be passed to :meth:`unregister_endpoint` to remove the handler.
|
|
186
|
+
"""
|
|
187
|
+
return self.event_handler.register_handler("raw_outbound", handler)
|
|
188
|
+
|
|
189
|
+
def register_raw_inbound_handler(self, handler):
|
|
190
|
+
"""
|
|
191
|
+
Register a handler for all outgoing messages received from the Pebble. Transport framing is not included.
|
|
192
|
+
In most cases you should not need to use this; consider using :meth:`register_endpoint` instead.
|
|
193
|
+
|
|
194
|
+
:param handler: A callback to be called when any message is received.
|
|
195
|
+
:type handler: callable
|
|
196
|
+
:return: A handle that can be passed to :meth:`unregister_endpoint` to remove the handler.
|
|
197
|
+
"""
|
|
198
|
+
return self.event_handler.register_handler("raw_inbound", handler)
|
|
199
|
+
|
|
200
|
+
def unregister_endpoint(self, handle):
|
|
201
|
+
"""
|
|
202
|
+
Removes a handler registered by :meth:`register_transport_endpoint`, :meth:`register_endpoint`,
|
|
203
|
+
:meth:`register_raw_outbound_handler` or :meth:`register_raw_inbound_handler`.
|
|
204
|
+
|
|
205
|
+
:param handle: A handle returned by the register call to be undone.
|
|
206
|
+
"""
|
|
207
|
+
return self.event_handler.unregister_handler(handle)
|
|
208
|
+
|
|
209
|
+
def read_from_endpoint(self, endpoint, timeout=15):
|
|
210
|
+
"""
|
|
211
|
+
Blocking read from an endpoint. Will block until a message is received, or it times out. Also see
|
|
212
|
+
:meth:`get_endpoint_queue` if you are considering calling this in a loop.
|
|
213
|
+
|
|
214
|
+
.. warning::
|
|
215
|
+
Avoid calling this method from an endpoint callback; doing so is likely to lead to deadlock.
|
|
216
|
+
|
|
217
|
+
.. note::
|
|
218
|
+
If you're reading a response to a message you just sent, :meth:`send_and_read` might be more appropriate.
|
|
219
|
+
|
|
220
|
+
:param endpoint: The endpoint to read from.
|
|
221
|
+
:type endpoint: .PacketType
|
|
222
|
+
:param timeout: The maximum time to wait before raising :exc:`.TimeoutError`.
|
|
223
|
+
:return: The message read from the endpoint; of the same type as passed to ``endpoint``.
|
|
224
|
+
"""
|
|
225
|
+
return self.event_handler.wait_for_event((_EventType.Watch, endpoint), timeout=timeout)
|
|
226
|
+
|
|
227
|
+
def get_endpoint_queue(self, endpoint):
|
|
228
|
+
"""
|
|
229
|
+
Returns a :class:`.BaseEventQueue` from which messages to the given ``endpoint`` can be read.
|
|
230
|
+
|
|
231
|
+
This is useful if you need to make sure that you receive all messages to an endpoint, without risking
|
|
232
|
+
dropping some due to time in between :meth:`read_from_endpoint` calls.
|
|
233
|
+
|
|
234
|
+
:param endpoint: The endpoint to read from
|
|
235
|
+
:type endpoint: .PacketType
|
|
236
|
+
:return:
|
|
237
|
+
"""
|
|
238
|
+
return self.event_handler.queue_events((_EventType.Watch, endpoint))
|
|
239
|
+
|
|
240
|
+
def read_transport_message(self, origin, message_type, timeout=15):
|
|
241
|
+
"""
|
|
242
|
+
Blocking read of a transport message that does not indicate a message from the Pebble.
|
|
243
|
+
Will block until a message is received, or it times out.
|
|
244
|
+
|
|
245
|
+
.. warning::
|
|
246
|
+
Avoid calling this method from an endpoint callback; doing so is likely to lead to deadlock.
|
|
247
|
+
|
|
248
|
+
:param origin: The type of :class:`.MessageTarget` that triggers the message.
|
|
249
|
+
:param message_type: The class of the message to read from the transport.
|
|
250
|
+
:param timeout: The maximum time to wait before raising :exc:`.TimeoutError`.
|
|
251
|
+
:return: The object read from the transport; of the same type as passed to ``message_type``.
|
|
252
|
+
"""
|
|
253
|
+
return self.event_handler.wait_for_event((_EventType.Transport, origin, message_type), timeout=timeout)
|
|
254
|
+
|
|
255
|
+
def send_packet(self, packet):
|
|
256
|
+
"""
|
|
257
|
+
Sends a message to the Pebble.
|
|
258
|
+
|
|
259
|
+
:param packet: The message to send.
|
|
260
|
+
:type packet: .PebblePacket
|
|
261
|
+
"""
|
|
262
|
+
if self.log_packet_level:
|
|
263
|
+
logger.log(self.log_packet_level, "-> %s", packet)
|
|
264
|
+
serialised = packet.serialise_packet()
|
|
265
|
+
self.event_handler.broadcast_event("raw_outbound", serialised)
|
|
266
|
+
self.send_raw(serialised)
|
|
267
|
+
|
|
268
|
+
def send_and_read(self, packet, endpoint, timeout=15):
|
|
269
|
+
"""
|
|
270
|
+
Sends a packet, then returns the next response received from that endpoint. This method sets up a listener
|
|
271
|
+
before it actually sends the message, avoiding a potential race.
|
|
272
|
+
|
|
273
|
+
.. warning::
|
|
274
|
+
Avoid calling this method from an endpoint callback; doing so is likely to lead to deadlock.
|
|
275
|
+
|
|
276
|
+
:param packet: The message to send.
|
|
277
|
+
:type packet: .PebblePacket
|
|
278
|
+
:param endpoint: The endpoint to read from
|
|
279
|
+
:type endpoint: .PacketType
|
|
280
|
+
:param timeout: The maximum time to wait before raising :exc:`.TimeoutError`.
|
|
281
|
+
:return: The message read from the endpoint; of the same type as passed to ``endpoint``.
|
|
282
|
+
"""
|
|
283
|
+
queue = self.get_endpoint_queue(endpoint)
|
|
284
|
+
self.send_packet(packet)
|
|
285
|
+
try:
|
|
286
|
+
return queue.get(timeout=timeout)
|
|
287
|
+
finally:
|
|
288
|
+
queue.close()
|
|
289
|
+
|
|
290
|
+
def send_raw(self, message):
|
|
291
|
+
"""
|
|
292
|
+
Sends a raw binary message to the Pebble. No processing will be applied, but any transport framing should be
|
|
293
|
+
omitted.
|
|
294
|
+
|
|
295
|
+
:param message: The message to send to the pebble.
|
|
296
|
+
:type message: bytes
|
|
297
|
+
"""
|
|
298
|
+
if self.log_protocol_level:
|
|
299
|
+
logger.log(self.log_protocol_level, "-> %s", hexlify(message).decode())
|
|
300
|
+
self.transport.send_packet(message)
|
|
301
|
+
|
|
302
|
+
def _register_internal_handlers(self):
|
|
303
|
+
if self.transport.must_initialise:
|
|
304
|
+
self.register_endpoint(PhoneAppVersion, self._app_version_response)
|
|
305
|
+
|
|
306
|
+
def _app_version_response(self, packet):
|
|
307
|
+
packet = PhoneAppVersion(message=AppVersionResponse(
|
|
308
|
+
protocol_version=0xFFFFFFFF,
|
|
309
|
+
session_caps=0x80000000,
|
|
310
|
+
platform_flags=50,
|
|
311
|
+
response_version=2,
|
|
312
|
+
major_version=3,
|
|
313
|
+
minor_version=0,
|
|
314
|
+
bugfix_version=0,
|
|
315
|
+
protocol_caps=0xFFFFFFFFFFFFFFFF
|
|
316
|
+
))
|
|
317
|
+
self.send_packet(packet)
|
|
318
|
+
|
|
319
|
+
def fetch_watch_info(self):
|
|
320
|
+
"""
|
|
321
|
+
This method should be called before accessing :attr:`watch_info`, :attr:`firmware_version`
|
|
322
|
+
or :attr:`watch_platform`. Blocks until it has fetched the required information.
|
|
323
|
+
"""
|
|
324
|
+
self._watch_info = self.send_and_read(WatchVersion(data=WatchVersionRequest()), WatchVersion).data
|
|
325
|
+
|
|
326
|
+
@property
|
|
327
|
+
def watch_info(self):
|
|
328
|
+
"""
|
|
329
|
+
Returns information on the connected Pebble, including its firmware version, language, capabilities, etc.
|
|
330
|
+
|
|
331
|
+
.. note:
|
|
332
|
+
This is a blocking call if :meth:`fetch_watch_info` has not yet been called, which could lead to deadlock
|
|
333
|
+
if called in an endpoint callback.
|
|
334
|
+
|
|
335
|
+
:rtype: .WatchVersionResponse
|
|
336
|
+
"""
|
|
337
|
+
if self._watch_info is None:
|
|
338
|
+
self.fetch_watch_info()
|
|
339
|
+
return self._watch_info
|
|
340
|
+
|
|
341
|
+
@property
|
|
342
|
+
def firmware_version(self):
|
|
343
|
+
"""
|
|
344
|
+
Provides information on the connected Pebble, including its firmware version, language, capabilities, etc.
|
|
345
|
+
|
|
346
|
+
.. note:
|
|
347
|
+
This is a blocking call if :meth:`fetch_watch_info` has not yet been called, which could lead to deadlock
|
|
348
|
+
if called in an endpoint callback.
|
|
349
|
+
|
|
350
|
+
:rtype: .WatchVersionResponse
|
|
351
|
+
"""
|
|
352
|
+
version = self.watch_info.running.version_tag[1:]
|
|
353
|
+
parts = version.split('-', 1)
|
|
354
|
+
points = [int(x) for x in parts[0].split('.')]
|
|
355
|
+
while len(points) < 3:
|
|
356
|
+
points.append(0)
|
|
357
|
+
if len(parts) == 2:
|
|
358
|
+
suffix = parts[1]
|
|
359
|
+
else:
|
|
360
|
+
suffix = ''
|
|
361
|
+
return FirmwareVersion(*(points + [suffix]))
|
|
362
|
+
|
|
363
|
+
@property
|
|
364
|
+
def watch_model(self):
|
|
365
|
+
"""
|
|
366
|
+
|
|
367
|
+
:return: The model of the watch.
|
|
368
|
+
:rtype: ~libpebble2.protocol.system.Model
|
|
369
|
+
"""
|
|
370
|
+
if self._watch_model is None:
|
|
371
|
+
info_bytes = self.send_and_read(WatchModel(data=ModelRequest()), WatchModel).data.data
|
|
372
|
+
if len(info_bytes) == 4:
|
|
373
|
+
self._watch_model, = struct.unpack('>I', info_bytes)
|
|
374
|
+
else:
|
|
375
|
+
self._watch_model = Model.Unknown
|
|
376
|
+
return self._watch_model
|
|
377
|
+
|
|
378
|
+
@property
|
|
379
|
+
def watch_platform(self):
|
|
380
|
+
"""
|
|
381
|
+
A string naming the platform of the watch ('aplite', 'basalt', 'chalk', or 'unknown').
|
|
382
|
+
|
|
383
|
+
.. note:
|
|
384
|
+
This is a blocking call if :meth:`fetch_watch_info` has not yet been called, which could lead to deadlock
|
|
385
|
+
if called in an endpoint callback.
|
|
386
|
+
|
|
387
|
+
:rtype: str
|
|
388
|
+
"""
|
|
389
|
+
return PebbleHardware.hardware_platform(self.watch_info.running.hardware_platform)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
__author__ = 'katharine'
|
|
2
|
+
|
|
3
|
+
from six import with_metaclass
|
|
4
|
+
|
|
5
|
+
from abc import ABCMeta, abstractmethod, abstractproperty
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MessageTarget(object):
|
|
9
|
+
def __repr__(self):
|
|
10
|
+
return type(self).__name__ + "()"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MessageTargetWatch(MessageTarget):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BaseTransport(with_metaclass(ABCMeta)):
|
|
18
|
+
@abstractproperty
|
|
19
|
+
def must_initialise(self):
|
|
20
|
+
"""
|
|
21
|
+
:return: ``True`` if libpebble2 is responsible for negotiating the connection; otherwise ``False``.
|
|
22
|
+
"""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
@abstractproperty
|
|
26
|
+
def connected(self):
|
|
27
|
+
"""
|
|
28
|
+
:return: ``True`` if the transport is currently connected; otherwise ``False``.
|
|
29
|
+
"""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def connect(self):
|
|
34
|
+
"""
|
|
35
|
+
Synchronously connect to the Pebble. Once this method returns, libpebble2 should be able to safely send
|
|
36
|
+
messages to the connected Pebble.
|
|
37
|
+
|
|
38
|
+
Ordinarily, this method should only be called by :class:`.PebbleConnection`.
|
|
39
|
+
"""
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def read_packet(self):
|
|
44
|
+
"""
|
|
45
|
+
Synchronously read a message. This message could be from the Pebble(in which case it will be a
|
|
46
|
+
:class:`PebblePacket`), or it could be from the transport, in which case the result is transport-defined.
|
|
47
|
+
The origin of the result is indicated by the returned :class:`MessageTarget`.
|
|
48
|
+
|
|
49
|
+
:return: (:class:`MessageTarget`, :class:`libpebble2.protocol.base.PebblePacket`)
|
|
50
|
+
"""
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
def send_packet(self, message, target=MessageTargetWatch()):
|
|
55
|
+
"""
|
|
56
|
+
Send a message. This message could be to the Pebble (in which case it must be a :class:`PebblePacket`), or
|
|
57
|
+
to the transport (in which case the message type is transport-defined).
|
|
58
|
+
|
|
59
|
+
:param message: Message to send.
|
|
60
|
+
:type message: PebblePacket
|
|
61
|
+
:param target: Target for the message
|
|
62
|
+
:type target: MessageTarget
|
|
63
|
+
"""
|
|
64
|
+
pass
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from __future__ import absolute_import
|
|
2
|
+
__author__ = 'Liam McLoughlin'
|
|
3
|
+
|
|
4
|
+
import time
|
|
5
|
+
import struct
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from pebble import pulse2
|
|
9
|
+
except ImportError:
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
from . import BaseTransport, MessageTargetWatch
|
|
13
|
+
from libpebble2.exceptions import ConnectionError, PebbleError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PULSETransport(BaseTransport):
|
|
17
|
+
"""
|
|
18
|
+
Represents a direct connection to a physical/virtual Pebble uses the PULSEv2 interface.
|
|
19
|
+
This transport expects to be given a PULSE2 Link object.
|
|
20
|
+
|
|
21
|
+
:param connection: A PULSE2 Link object to tunnel Pebble Protocol over.
|
|
22
|
+
:type link: pulse2.link.Link
|
|
23
|
+
"""
|
|
24
|
+
must_initialise = True
|
|
25
|
+
|
|
26
|
+
PPOPULSE_PORT = 0x3e22
|
|
27
|
+
|
|
28
|
+
OPCODE_PROTOCOL_DATA = 0x1
|
|
29
|
+
OPCODE_PROTOCOL_OPEN = 0x2
|
|
30
|
+
OPCODE_PROTOCOL_CLOSE = 0x3
|
|
31
|
+
|
|
32
|
+
def __init__(self, link):
|
|
33
|
+
self.link = link
|
|
34
|
+
self.connection = None
|
|
35
|
+
self.buffer = b''
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def _chunks(list_items, chunk_length):
|
|
39
|
+
for i in range(0, len(list_items), chunk_length):
|
|
40
|
+
yield list_items[i:i+chunk_length]
|
|
41
|
+
|
|
42
|
+
def connect(self):
|
|
43
|
+
self.connection = self.link.open_socket('reliable', self.PPOPULSE_PORT)
|
|
44
|
+
if not self.connection:
|
|
45
|
+
raise ConnectionError('Failed to open PPoPULSE socket')
|
|
46
|
+
|
|
47
|
+
self._send_with_opcode(self.OPCODE_PROTOCOL_OPEN)
|
|
48
|
+
start_time = time.time()
|
|
49
|
+
while time.time() < start_time + 10.0:
|
|
50
|
+
opcode, _ = self._recv_with_opcode()
|
|
51
|
+
if opcode == self.OPCODE_PROTOCOL_OPEN:
|
|
52
|
+
break
|
|
53
|
+
else:
|
|
54
|
+
raise ConnectionError('Timeout waiting for PPoPULSE open ACK')
|
|
55
|
+
|
|
56
|
+
def disconnect(self):
|
|
57
|
+
if self.connected:
|
|
58
|
+
try:
|
|
59
|
+
self._send_with_opcode(self.OPCODE_PROTOCOL_CLOSE)
|
|
60
|
+
except pulse2.exceptions.SocketClosed:
|
|
61
|
+
pass
|
|
62
|
+
self.connection.close()
|
|
63
|
+
self.connection = None
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def connected(self):
|
|
67
|
+
return self.connection is not None
|
|
68
|
+
|
|
69
|
+
def read_packet(self):
|
|
70
|
+
while self.connected:
|
|
71
|
+
if len(self.buffer) >= 2:
|
|
72
|
+
length, = struct.unpack('!H', self.buffer[:2])
|
|
73
|
+
length += 4
|
|
74
|
+
|
|
75
|
+
if len(self.buffer) >= length:
|
|
76
|
+
msg_data = self.buffer[:length]
|
|
77
|
+
self.buffer = self.buffer[length:]
|
|
78
|
+
|
|
79
|
+
return MessageTargetWatch(), msg_data
|
|
80
|
+
|
|
81
|
+
opcode, data = self._recv_with_opcode()
|
|
82
|
+
if opcode == self.OPCODE_PROTOCOL_DATA:
|
|
83
|
+
self.buffer += data
|
|
84
|
+
|
|
85
|
+
def send_packet(self, message, target=MessageTargetWatch()):
|
|
86
|
+
assert isinstance(target, MessageTargetWatch)
|
|
87
|
+
for chunk in self._chunks(message, self.connection.mtu - 1):
|
|
88
|
+
self._send_with_opcode(self.OPCODE_PROTOCOL_DATA, chunk)
|
|
89
|
+
|
|
90
|
+
def _recv_with_opcode(self):
|
|
91
|
+
try:
|
|
92
|
+
packet = self.connection.receive(block=True)
|
|
93
|
+
except (AttributeError, pulse2.exceptions.SocketClosed):
|
|
94
|
+
self.connection = None
|
|
95
|
+
raise ConnectionError('PULSE transport closed')
|
|
96
|
+
|
|
97
|
+
assert len(packet) >= 1
|
|
98
|
+
opcode = packet[0]
|
|
99
|
+
data = packet[1:]
|
|
100
|
+
return opcode, data
|
|
101
|
+
|
|
102
|
+
def _send_with_opcode(self, opcode, body=None):
|
|
103
|
+
assert self.connected
|
|
104
|
+
|
|
105
|
+
data = bytes([opcode])
|
|
106
|
+
if body:
|
|
107
|
+
data += body
|
|
108
|
+
self.connection.send(data)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from __future__ import absolute_import
|
|
2
|
+
__author__ = 'katharine'
|
|
3
|
+
|
|
4
|
+
import socket
|
|
5
|
+
|
|
6
|
+
from .. import BaseTransport, MessageTarget, MessageTargetWatch
|
|
7
|
+
from .protocol import QemuPacket, QemuInboundPacket, QemuSPP, QemuRawPacket, HEADER_SIGNATURE, FOOTER_SIGNATURE
|
|
8
|
+
from libpebble2.exceptions import ConnectionError
|
|
9
|
+
from libpebble2.protocol.base.types import PacketDecodeError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MessageTargetQemu(MessageTarget):
|
|
13
|
+
"""
|
|
14
|
+
Indicates that a message is directed at QEMU, rather than the firmware running on it. If ``raw`` is ``True``,
|
|
15
|
+
the message should be a binary message (without framing), with QEMU protocol indicated by ``protocol``.
|
|
16
|
+
Otherwise, the message should be a :class:`.PebblePacket` from :mod:`.protocol`.
|
|
17
|
+
|
|
18
|
+
:param protocol: The protocol to send to, if sending a ``raw`` message
|
|
19
|
+
:type protocol: :any:`int`
|
|
20
|
+
:param raw: If ``True``, the message is pre-serialised and will be sent as-is after adding framing.
|
|
21
|
+
:type raw: :any:`bool`
|
|
22
|
+
"""
|
|
23
|
+
def __init__(self, protocol=None, raw=False):
|
|
24
|
+
self.protocol = protocol
|
|
25
|
+
self.raw = raw
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class QemuTransport(BaseTransport):
|
|
29
|
+
"""
|
|
30
|
+
Represents a connection to a `Pebble QEMU <https://github.com/pebble/qemu>`_ instance.
|
|
31
|
+
|
|
32
|
+
:param host: The host on which the QEMU instance is running.
|
|
33
|
+
:type host: str
|
|
34
|
+
:param port: The port on which the QEMU instance has exposed its Pebble QEMU Protocol port.
|
|
35
|
+
:type port: int
|
|
36
|
+
"""
|
|
37
|
+
#: Number of bytes read from the socket at a time.
|
|
38
|
+
BUFFER_SIZE = 2048
|
|
39
|
+
must_initialise = True
|
|
40
|
+
|
|
41
|
+
def __init__(self, host='127.0.0.1', port=12344, socket = None):
|
|
42
|
+
self.host = host
|
|
43
|
+
self.port = port
|
|
44
|
+
self.socket = socket
|
|
45
|
+
self.assembled_data = b''
|
|
46
|
+
self._connected = socket is not None
|
|
47
|
+
|
|
48
|
+
def connect(self):
|
|
49
|
+
try:
|
|
50
|
+
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
51
|
+
self.socket.connect((self.host, self.port))
|
|
52
|
+
except socket.error as e:
|
|
53
|
+
raise ConnectionError(str(e))
|
|
54
|
+
self._connected = True
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def connected(self):
|
|
58
|
+
return self.socket is not None and self._connected
|
|
59
|
+
|
|
60
|
+
def read_packet(self):
|
|
61
|
+
while True:
|
|
62
|
+
if len(self.assembled_data) > 0:
|
|
63
|
+
try:
|
|
64
|
+
packet, length = QemuInboundPacket.parse(self.assembled_data)
|
|
65
|
+
except PacketDecodeError:
|
|
66
|
+
pass
|
|
67
|
+
else:
|
|
68
|
+
self.assembled_data = self.assembled_data[length:]
|
|
69
|
+
if packet.signature == HEADER_SIGNATURE and packet.footer == FOOTER_SIGNATURE:
|
|
70
|
+
if isinstance(packet.data, QemuSPP):
|
|
71
|
+
return MessageTargetWatch(), packet.data.payload
|
|
72
|
+
else:
|
|
73
|
+
return MessageTargetQemu(packet.protocol), packet.data
|
|
74
|
+
else:
|
|
75
|
+
raise PacketDecodeError("QemuTransport: signature mismatch ({:x} = {:x}, {:x} = {:x})".format(
|
|
76
|
+
packet.signature,
|
|
77
|
+
HEADER_SIGNATURE,
|
|
78
|
+
packet.footer,
|
|
79
|
+
FOOTER_SIGNATURE,
|
|
80
|
+
))
|
|
81
|
+
try:
|
|
82
|
+
received = self.socket.recv(self.BUFFER_SIZE)
|
|
83
|
+
if len(received) == 0:
|
|
84
|
+
self._connected = False
|
|
85
|
+
raise ConnectionError("Disconnected.")
|
|
86
|
+
self.assembled_data += received
|
|
87
|
+
except socket.error:
|
|
88
|
+
self._connected = False
|
|
89
|
+
raise ConnectionError("Disconnected.")
|
|
90
|
+
|
|
91
|
+
def send_packet(self, message, target=MessageTargetWatch()):
|
|
92
|
+
try:
|
|
93
|
+
if isinstance(target, MessageTargetWatch):
|
|
94
|
+
start_idx = 0
|
|
95
|
+
bytes_left = len(message)
|
|
96
|
+
while bytes_left:
|
|
97
|
+
bytes_to_send = bytes_left
|
|
98
|
+
if bytes_to_send > self.BUFFER_SIZE:
|
|
99
|
+
bytes_to_send = self.BUFFER_SIZE
|
|
100
|
+
chunk = message[start_idx:start_idx+bytes_to_send]
|
|
101
|
+
self.socket.send(QemuPacket(data=QemuSPP(payload=chunk)).serialise())
|
|
102
|
+
bytes_left -= bytes_to_send
|
|
103
|
+
start_idx += bytes_to_send
|
|
104
|
+
elif isinstance(target, MessageTargetQemu):
|
|
105
|
+
if not target.raw:
|
|
106
|
+
self.socket.send(QemuPacket(data=message).serialise())
|
|
107
|
+
else:
|
|
108
|
+
self.socket.send(QemuRawPacket(protocol=target.protocol, data=message).serialise())
|
|
109
|
+
else:
|
|
110
|
+
assert False
|
|
111
|
+
except socket.error as e:
|
|
112
|
+
self._connected = False
|
|
113
|
+
raise ConnectionError(str(e))
|