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 and False, the message will not be registered for parsing when received, even if endpoint 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 by parse_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.

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 Enum that represents the possible values of the field.
buffer_to_value(obj, buffer, offset, default_endianness='!')

Converts the bytes in buffer at offset 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 the Field constructor.
Returns:

(value, length)

Return type:

(object, int)

struct_format = None

A format code for use in struct.pack(), if using the default implementation of buffer_to_value() and value_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 the Field constructor.
Returns:

The serialised value

Return type:

bytes

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 a UUID. 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 of determinant to either Fields or PebblePackets that this Union can represent. This dictionary is inverted for use in serialisation, so it should be a one-to-one mapping.
  • accept_missing (bool) – If True, the Union will tolerate receiving unknown values, considering them to be None.
  • length (int) – An optional Field that should contain the length of the Union. 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, 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.
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 the PebblePacket. For this effect, pass in a Field for length. To deserialise correctly, this field must appear before the FixedString.
  • The length is fixed by the protocol. For this effect, pass in an int for length.
  • The string uses the entire remainder of the packet. For this effect, omit length (or pass None).
Parameters:length (Field | int) – The length of the string.
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 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.
class libpebble2.protocol.base.types.FixedList(member_type, count=None, length=None)

Represents a list of either PebblePackets or Fields 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 nor length is set, members will be read until the end of the buffer.

Parameters:
  • member_type – Either a Field instance or a PebblePacket 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.
class libpebble2.protocol.base.types.BinaryArray(length=None, **kwargs)

An array of arbitrary bytes, represented as a Python bytes object. The length can be either a Field, an int, or omitted.

Parameters:length (Field | int) –

The length of the array:

  • If it’s a Field, the value of that field is read during deserialisation and written during seiralisation.
  • If it’s an int, that many bytes are always read.
  • If it is None, bytes are read until the end of the message.
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.