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 ifendpointis 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
Metacontaining 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,PacketDecodeErrorwill 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
Metainner class of the packet.self.Meta.endpointmust 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 PebblePackets or Fields with either a fixed number of entries, a fixed length (in bytes), or both. |
PascalList |
Represents a list of PebblePackets, 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
Enumthat represents the possible values of the field.
-
buffer_to_value(obj, buffer, offset, default_endianness='!')¶ Converts the bytes in
bufferatoffsetto a native Python value. Returns that value and the number of bytes consumed to create it.Parameters: - obj (PebblePacket) – The parent
PebblePacketof 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
endiannesswas not passed to theFieldconstructor.
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
PebblePacketof this field - value – The python value to serialise.
- default_endianness (str) – The default endianness of the value. Used if
endiannesswas not passed to theFieldconstructor.
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
dictmapping values ofdeterminantto eitherFields orPebblePackets that thisUnioncan represent. This dictionary is inverted for use in serialisation, so it should be a one-to-one mapping. - accept_missing (bool) – If
True, theUnionwill tolerate receiving unknown values, considering them to beNone. - length (int) – An optional
Fieldthat 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, length=None)¶ 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,
lengthbytes are skipped; during serialisation,length0x00 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
Fieldin thePebblePacket. For this effect, pass in aFieldforlength. To deserialise correctly, this field must appear before theFixedString. - The length is fixed by the protocol. For this effect, pass in an
intforlength. - 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
PebblePackets, each of which is prefixed with a byte indicating its length.Parameters: - member_type (type) – The type of
PebblePacketin the list. - count (Field) – If specified, the a
Fieldthat 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
PebblePackets orFields 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
countnorlengthis set, members will be read until the end of the buffer.Parameters: - member_type – Either a
Fieldinstance or aPebblePacketsubclass that represents the members of the list. - count – A
Fieldcontaining 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
Fieldcontaining 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
bytesobject. Thelengthcan 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.