Protocol handling¶
libpebble2 provides a simple DSL for defining Pebble Protocol messages, accounting for various quirks in the Pebble Protocol, such as the four different ways of defining strings and mixed endianness.
Defining messages¶
All messages inherit from PebblePacket
, which uses metaclass magic (from PacketType
) to parse
the definitions. An empty message would look like this:
class SampleMessage(PebblePacket):
pass
This message is not very interesting — it represents a zero-length, unidentifiable packet. Despite this, it can be
useful in conjunction with certain field types, such as Union
.
Metadata¶
To add some useful information about our message, we can define a Meta
inner class inside it:
class SampleMessage(PebblePacket):
class Meta:
endpoint = 0xbead
endianness = '<'
This defines our SampleMessage
as being a little-endian Pebble Protocol message that should be sent to endpoint
0xbead.
The following attributes on Meta
are meaningful (but all are optional):
endpoint
— defines the Pebble Protocol endpoint to which the message should be sent.endianness
— defines the endianness of the message. Use'<'
for little-endian or'>'
for big-endian.register
— if specified andFalse
, the message will not be registered for parsing when received, even ifendpoint
is specified. This can be useful if the protocol design is asymmetric and ambiguous.
Note
Meta
is not inherited if you subclass a PebblePacket
. In particular, you will probably want to
re-specify endianness
when doing this. The default endianness is big-endian.
We can now use this class to send an empty message to the watch, or receive one back!
>>> pebble.send_packet(SampleMessage())
Fields¶
Empty messages are rarely useful. To actually send some information, we can add more attributes to our messages. For instance, let’s say we want to specify a time message that looks like this:
Offset | Length | Type | Value |
---|---|---|---|
0 | 4 | uint32_t | Seconds since 1970 (unix time, UTC) |
4 | 2 | uint16_t | UTC offset in minutes, including DST |
6 | 1 | uint8_t | Length of the timezone region name |
7 | ... | char * | The timezone region name |
We could represent that packet like this:
class SetUTC(PebblePacket):
unix_time = Uint32()
utc_offset_mins = Int16()
tz_name = PascalString()
The lengths and offsets are determined automatically. Also notice that we didn’t have to include the length explicitly
— including a length byte before a string is a sufficiently common pattern that it has a dedicated PascalString
field. This definition works:
>>> from binascii import hexlify
>>> message = SetUTC(unix_time=1436165495, utc_offset_mins=-420, tz_name=u"America/Los_Angeles")
>>> hexlify(message.serialise())
'559a2577fe5c13416d65726963612f4c6f735f416e67656c6573'
>>> SetUTC.parse('559a2577fe5c13416d65726963612f4c6f735f416e67656c6573'.decode('hex'))
(SetUTC(unix_time=1436165495, utc_offset=-420, tz_name=America/Los_Angeles), 26)
(parse()
returns a (message, consumed_bytes)
tuple.)
Which is nice, but isn’t usable as a Pebble Protocol message — after all, we don’t have an endpoint. It also turns out
that this isn’t actually a message you can send to the Pebble; rather, it’s merely one of four possible messages to
the “Time” endpoint. How can we handle that? With a Union
! Let’s build the whole Time message:
class GetTimeRequest(PebblePacket):
pass
class GetTimeResponse(PebblePacket):
localtime = Uint32()
class SetLocaltime(PebblePacket):
localtime = Uint32()
class SetUTC(PebblePacket):
unix_time = Uint32()
utc_offset_mins = Int16()
tz_name = PascalString()
class TimeMessage(PebblePacket):
class Meta:
endpoint = 0xb
endianness = '>' # big endian
command = Uint8()
message = Union(command, {
0x00: GetTimeRequest,
0x01: GetTimeResponse,
0x02: SetLocaltime,
0x03: SetUTC,
})
TimeMessage
is now our Pebble Protocol message. Its Meta
class contains two pieces of information; the endpoint
and the endianness of the message (which is actually the default). It consists of two fields: a command
, which is just a
uint8_t
, and a message
. Union applies the endianness specified in TimeMessage
to the other classes it
references.
During deserialisation, the Union
will use the value of command
to figure
out which member of the union to use, then use that class to parse the remainder of the message. During serialisation,
Union
will inspect the type of the provided message
:
>>> message = TimeMessage(message=SetUTC(unix_time=1436165495, utc_offset_mins=-420, tz_name=u"America/Los_Angeles"))
# We don't have to set command because Union does that for us.
>>> hexlify(message.serialise_packet())
'001b000b03559a2577fe5c13416d65726963612f4c6f735f416e67656c6573'
>>> PebblePacket.parse_message('001b000b03559a2577fe5c13416d65726963612f4c6f735f416e67656c6573'.decode('hex'))
(TimeMessage(kind=3, message=SetUTC(unix_time=1436165495, utc_offset=-420, tz_name=America/Los_Angeles)), 31)
>>> pebble.send_packet(message)
And there we go! We encoded a pebble packet, then asked the general PebblePacket
to deserialise it for us.
But wait: how did PebblePacket
know to return a TimeMessage
?
When defining a subclass of PebblePacket
, it will automatically be registered in an internal “packet registry”
if it has an endpoint
specified. Sometimes this behaviour is undesirable; in this case, you can specify
register = False
to disable this behaviour.
API¶
Packets¶
-
class
libpebble2.protocol.base.
PebblePacket
(**kwargs)¶ Represents some sort of Pebble Protocol message.
A PebblePacket can have an inner class named
Meta
containing some information about the property:endpoint The Pebble Protocol endpoint that is represented by this message. endianness The endianness of the packet. The default endianness is big-endian, but it can be overridden by packets and fields, with the priority: register If set to False
, the packet will not be registered and thus will be ignored byparse_message()
. This is useful when messages are ambiguous, and distinguished only by whether they are sent to or from the Pebble.A sample packet might look like this:
class AppFetchResponse(PebblePacket): class Meta: endpoint = 0x1771 endianness = '<' register = False command = Uint8(default=0x01) response = Uint8(enum=AppFetchStatus)
Parameters: **kwargs – Initial values for any properties on the object. -
classmethod
parse
(message, default_endianness='!')¶ Parses a message without any framing, returning the decoded result and length of message consumed. The result will always be of the same class as
parse()
was called on. If the message is invalid,PacketDecodeError
will be raised.Parameters: - message (bytes) – The message to decode.
- default_endianness – The default endianness, unless overridden by the fields or class metadata.
Should usually be left at
None
. Otherwise, use'<'
for little endian and'>'
for big endian.
Returns: (decoded_message, decoded length)
Return type: (
PebblePacket
,int
)
-
classmethod
parse_message
(message)¶ Parses a message received from the Pebble. Uses Pebble Protocol framing to figure out what sort of packet it is. If the packet is registered (has been defined and imported), returns the deserialised packet, which will not necessarily be the same class as this. Otherwise returns
None
.Also returns the length of the message consumed during deserialisation.
Parameters: message (bytes) – A serialised message received from the Pebble. Returns: (decoded_message, decoded length)
Return type: ( PebblePacket
,int
)
-
serialise
(default_endianness=None)¶ Serialise a message, without including any framing.
Parameters: default_endianness (str) – The default endianness, unless overridden by the fields or class metadata. Should usually be left at None
. Otherwise, use'<'
for little endian and'>'
for big endian.Returns: The serialised message. Return type: bytes
-
serialise_packet
()¶ Serialise a message, including framing information inferred from the
Meta
inner class of the packet.self.Meta.endpoint
must be defined to call this method.Returns: A serialised message, ready to be sent to the Pebble.
-
classmethod
Field types¶
Padding |
Represents some unused bytes. |
Boolean |
Represents a bool . |
Uint8 |
Represents a uint8_t . |
Uint16 |
Represents a uint16_t . |
Uint32 |
Represents a uint32_t . |
Uint64 |
Represents a uint64_t . |
Int8 |
Represents an int8_t . |
Int16 |
Represents an int16_t . |
Int32 |
Represents an int32_t . |
Int64 |
Represents an int64_t . |
FixedString |
Represents a “fixed-length” string. |
NullTerminatedString |
Represents a null-terminated, UTF-8 encoded string (i.e. |
PascalString |
Represents a UTF-8-encoded string that is prefixed with a length byte. |
FixedList |
Represents a list of either PebblePacket s or Field s with either a fixed number of entries, a fixed length (in bytes), or both. |
PascalList |
Represents a list of PebblePacket s, each of which is prefixed with a byte indicating its length. |
Union |
Represents a union of some other set of fields or packets, determined by some other field (determinant ). |
Embed |
Embeds another PebblePacket . |
-
class
libpebble2.protocol.base.types.
Field
(default=None, endianness=None, enum=None)¶ Base class for Pebble Protocol fields. This class does nothing; only subclasses are useful.
Parameters: - default – The default value of the field, if nothing else is specified.
- endianness (str) – The endianness of the field. By default, inherits from packet, or its parent packet, etc.
Use
"<"
for little endian or">"
for big endian. - enum (Enum) – An
Enum
that represents the possible values of the field.
-
buffer_to_value
(obj, buffer, offset, default_endianness='!')¶ Converts the bytes in
buffer
atoffset
to a native Python value. Returns that value and the number of bytes consumed to create it.Parameters: - obj (PebblePacket) – The parent
PebblePacket
of this field - buffer (bytes) – The buffer from which to extract a value.
- offset (int) – The offset in the buffer to start at.
- default_endianness (str) – The default endianness of the value. Used if
endianness
was not passed to theField
constructor.
Returns: (value, length)
Return type: - obj (PebblePacket) – The parent
-
struct_format
= None¶ A format code for use in
struct.pack()
, if using the default implementation ofbuffer_to_value()
andvalue_to_bytes()
-
value_to_bytes
(obj, value, default_endianness='!')¶ Converts the given value to an appropriately encoded string of bytes that represents it.
Parameters: - obj (PebblePacket) – The parent
PebblePacket
of this field - value – The python value to serialise.
- default_endianness (str) – The default endianness of the value. Used if
endianness
was not passed to theField
constructor.
Returns: The serialised value
Return type: - obj (PebblePacket) – The parent
-
class
libpebble2.protocol.base.types.
Int8
(default=None, endianness=None, enum=None)¶ Represents an
int8_t
.
-
class
libpebble2.protocol.base.types.
Uint8
(default=None, endianness=None, enum=None)¶ Represents a
uint8_t
.
-
class
libpebble2.protocol.base.types.
Int16
(default=None, endianness=None, enum=None)¶ Represents an
int16_t
.
-
class
libpebble2.protocol.base.types.
Uint16
(default=None, endianness=None, enum=None)¶ Represents a
uint16_t
.
-
class
libpebble2.protocol.base.types.
Int32
(default=None, endianness=None, enum=None)¶ Represents an
int32_t
.
-
class
libpebble2.protocol.base.types.
Uint32
(default=None, endianness=None, enum=None)¶ Represents a
uint32_t
.
-
class
libpebble2.protocol.base.types.
Int64
(default=None, endianness=None, enum=None)¶ Represents an
int64_t
.
-
class
libpebble2.protocol.base.types.
Uint64
(default=None, endianness=None, enum=None)¶ Represents a
uint64_t
.
-
class
libpebble2.protocol.base.types.
Boolean
(default=None, endianness=None, enum=None)¶ Represents a
bool
.
-
class
libpebble2.protocol.base.types.
UUID
(default=None, endianness=None, enum=None)¶ Represents a UUID, represented as a 16-byte array (
uint8_t[16]
). The Python representation is aUUID
. Endianness is ignored.
-
class
libpebble2.protocol.base.types.
Union
(determinant, contents, accept_missing=False, length=None)¶ Represents a union of some other set of fields or packets, determined by some other field (
determinant
).Example usage:
command = Uint8() data = Union(command, { 0: SomePacket, 1: SomeOtherPacket, 2: AnotherPacket })
Parameters: - determinant (Field) – The field that is used to determine which possible entry to use.
- contents (dict) – A
dict
mapping values ofdeterminant
to eitherField
s orPebblePacket
s that thisUnion
can represent. This dictionary is inverted for use in serialisation, so it should be a one-to-one mapping. - accept_missing (bool) – If
True
, theUnion
will tolerate receiving unknown values, considering them to beNone
. - length (int) – An optional
Field
that should contain the length of theUnion
. If provided, the field will be filled in on serialisation, and taken as a maximum length during deserialisation.
-
class
libpebble2.protocol.base.types.
Embed
(packet)¶ Embeds another
PebblePacket
. Useful for implementing repetitive packets.Parameters: packet (PebblePacket) – The packet to embed.
-
class
libpebble2.protocol.base.types.
Padding
(length)¶ Represents some unused bytes. During deserialisation,
length
bytes are skipped; during serialisation,length
0x00 bytes are added.Parameters: length (int) – The number of bytes of padding.
-
class
libpebble2.protocol.base.types.
PascalString
(null_terminated=False, count_null_terminator=True, *args, **kwargs)¶ Represents a UTF-8-encoded string that is prefixed with a length byte.
Parameters: - null_terminated (bool) – If
True
, a zero byte is appended to the string and included in the length during serialisation. The string is always terminated at the first zero byte during deserialisation, regardless of the value of this argument. - count_null_terminator (bool) – If
True
, any appended zero byte is not counted in the length of the string. This actually comes up.
- null_terminated (bool) – If
-
class
libpebble2.protocol.base.types.
NullTerminatedString
(default=None, endianness=None, enum=None)¶ Represents a null-terminated, UTF-8 encoded string (i.e. a C string).
-
class
libpebble2.protocol.base.types.
FixedString
(length=None, **kwargs)¶ Represents a “fixed-length” string. “Fixed-length” here has one of three possible meanings:
- The length is determined by another
Field
in thePebblePacket
. For this effect, pass in aField
forlength
. To deserialise correctly, this field must appear before theFixedString
. - The length is fixed by the protocol. For this effect, pass in an
int
forlength
. - The string uses the entire remainder of the packet. For this effect, omit
length
(or passNone
).
Parameters: length ( Field
|int
) – The length of the string.- The length is determined by another
-
class
libpebble2.protocol.base.types.
PascalList
(member_type, count=None)¶ Represents a list of
PebblePacket
s, each of which is prefixed with a byte indicating its length.Parameters: - member_type (type) – The type of
PebblePacket
in the list. - count (Field) – If specified, the a
Field
that contains the number of entries in the list. On serialisation, the count is filled in with the number of entries. On deserialisation, it is interpreted as a maximum; it is not an error for the packet to end prematurely.
- member_type (type) – The type of
-
class
libpebble2.protocol.base.types.
FixedList
(member_type, count=None, length=None)¶ Represents a list of either
PebblePacket
s orField
s with either a fixed number of entries, a fixed length (in bytes), or both. There are no dividers between entries; the members must be fixed-length.If neither
count
norlength
is set, members will be read until the end of the buffer.Parameters: - member_type – Either a
Field
instance or aPebblePacket
subclass that represents the members of the list. - count – A
Field
containing the number of elements in the list. On serialisation, will be set to the number of members. On deserialisation, is treated as a maximum. - length – A
Field
containing the number of bytes in the list. On serialisation, will be set to the length of the serialised list. On deserialisation, is treated as a maximum.
- member_type – Either a
-
class
libpebble2.protocol.base.types.
BinaryArray
(length=None, **kwargs)¶ An array of arbitrary bytes, represented as a Python
bytes
object. Thelength
can be either aField
, anint
, or omitted.Parameters: length (Field | int) – The length of the array:
-
class
libpebble2.protocol.base.types.
Optional
(actual_field, **kwargs)¶ Represents an optional field. It is usually an error during deserialisation for fields to be omitted. If that field is
Optional
, it will be left at its default value and ignored.Parameters: actual_field (Field) – The field that is being made optional.