Skip to content

Custom decoding

Custom decoding allows blueSPY users to implement their own higher-layers of packet decoding on top of the existing ones. A user writes a C/C++ library which provides a function that blueSPY will call when it encounters the specified events. This function can then add higher level events to represent the user defined protocol data from one or more built-in events.

Custom decoders should be used only in the case where you want to create new events to be shown in blueSPY. If you want to perform actions external to blueSPY such as logging or network requests based on blueSPY events, this is possible with the standard blueSPY API.

Writing a custom decoder

An example custom decoder is included in the blueSPY release inside the example_custom_decoder directory.

All custom decoders must include a function called init which is called when the decoder library is loaded. When compiling your custom decoder, ensure that this function is visible in the shared section of the resulting shared library so that blueSPY can call it.

You will want to define one or more callback functions and then register them in the init function with bluespy_register_event_callback(event_type, callback).

The event type specifies which events the callback should be called on. You can register more than one callback for a single event type and can register one callback to many event types.

Callback

A callback function should have the following format:

void my_callback(bluespy_event_id id);

Within the callback, you can use the bluespy_add_event function to add events representing your higher layer protocol.

A custom event is specified by the following struct.

typedef struct bluespy_custom_event {
    bluespy_event_id* children;
    unsigned int n_children;
    bluespy_query_value (*query)(const struct bluespy_custom_event* self, const char* query_str,
                                 bool prefer_string);
    bluespy_custom_event_layer layer;
} bluespy_custom_event;
Once you have passed an event to bluespy_add_event you should not change it. If you wish to add an event with multiple children, wait until you have the full list of children before submitting it. You can add packet IDs to a buffer you define and then, once you see the final packet in the procedure, create a custom event with all those IDs as its children and clear the buffer.

The layer member is used to determine when to show your custom event. For example, choosing the layer logical mean it will be shown in the Logical layer view and above in blueSPY. You should select a layer which is higher than the layer(s) of the children. For example, if you select logical than all children events should appear in the Baseband view in blueSPY. Choosing automatic will put your custom event on the layer above its children which is usually what you want.

In order to store information about your events you can subclass the custom event (in C++), or have it as the first member of another struct (C-style inheritance), then pass that to bluespy_add_event, and later cast the self pointer to your custom type in your query function.

Other callbacks

reset

You can provide a reset callback which blueSPY will call at the start of each capture (live capture or loading a file). This can be used to reset global state machines or memory management.

final

This is called after blueSPY processes the last packet. It is useful for emitting events for a multi-packet procedure that is cut off by the end of the capture

Children

blueSPY combines events from the bottom up through the Baseband/Logical/Transaction/Application aggregation layers you can find in the GUI. One or more events at the Baseband layer (the children) can be combined into one Logical (e.g. L2CAP) packet (the parent). If you submit an event that has baseband children, it will appear in the 'Logical' level or above. You can add further custom events that have your own custom events as children to combine into higher layers. All events must have at least one child. All events can have at most one parent, so if you create two custom events that share a child, the child will only get the latter custom event as a parent. This means that you are overriding the parents that blueSPY creates.

Queries

The query member is a pointer to a function that is used to determine features of your event. There are many standard queries in blueSPY, e.g. the text in the Summary column in the GUI will use the "summary" query on your custom event, and the Raw view will use "payload". You can see a full list of queries in Help->Query List. Several queries such as "duration" and "rssi" will have default values based on the children if you do not customise them. You can also add queries of your own; these will not show in the blueSPY GUI but can be used by within your custom decoders. Custom query names must consist of alphanumeric characters and underscores, and start with a letter (the name should match the regex [A-Za-z][A-Za-z0-9_]*).

You may return different values on later calls to query based on new packets. For example you may wish to update information about a connection in the details view.

query will be called in a different thread to bluespy_add_event, so you should be careful when looking at state that may be changed by bluespy_add_event. (bluespy_add_event itself is only ever called in one thread).

Details

One of the queries you should implement is "details". This query is used to show the event as a tree structure in the Details tab.

You must return json in the expected format in order for blueSPY to parse it and show it correctly.

[
  {"name": "Name", "value": "Value"},
  {
    "name": "Name2",
    "value": [
      {"name": "Name3", "value": 1},
      {"name": "Name4", "value": 2}
    ]
  }
]

The expected JSON structure is shown above. It must be an array of objects. Each object represents a field that will be shown in the UI. A field object must include two keys: "name" which corresponds to a string value and "value" which corresponds to a string, number, boolean or array which recursively follows these rules.

Fields can also include the keys "bitpos" and "bitwidth". These are used to indicate which bits in the packet this field corresponds to so that this can be highlighted in the Raw tab.

Other special queries

There are a few other queries which have special behaviour when implemented by a custom decoder.

  • corrupt: Show with a warning symbol and hidden by default.
  • encrypted: Show with a padlock symbol and hidden by default.
  • decrypted: Show with an unlocked padlock symbol.

Custom Queries

In addition to implementing the queries blueSPY already has, you can register and implement new queries by providing the registerCustomQueries callback.

These queries will be shown in the GUI so you can add them as columns or filter based on them.

Allocation

Event objects must exist for as long as the capture file is open. To aid with this, there is a bluespy_allocate (and in C++ bluespy::allocate) function that automatically frees the objects when they are no longer needed. You should not call bluespy_allocate from each query, as this will cause ever increasing memory usage. Due to the multithreaded nature of blueSPY if you open a new capture the cleanup for the old capture's events may occur concurrently with the new capture's callbacks.

Loading Custom decoders

You can have blueSPY load custom decoders in one of two ways:

  1. You can add the compiled decoders to the blueSPY data directory. This is ~/.local/share/RFcreations/blueSPY/custom_decoders on Linux, ~/Library/Application Support/RFcreations/blueSPY/custom_decoders on MacOS and C:\Users\<UserName>\AppData\Roaming\RFcreations\blueSPY\custom_decoders on Windows. This will ensure that the custom decoders are loaded every time you open blueSPY.

  2. You can set the BLUESPY_CUSTOM_PROTOCOL_DIR environment variable to be the path of a directory containing the compiled decoders. This is useful for testing your decoder during development or for decoders you don't want to be using most of the time.

Query documentation

To avoid requiring you to implement a large number of queries for your custom events, we provide some default implementations.

The following queries are passed the first child if you don't implement them: - time

If you don't implement them, the following queries are passed to the child if there is exactly one child. They return a default value otherwise. - payload - payload_hex - payload_summary - sender.* - receiver.* - access_code - status - device_summary - CID

If you don't implement them, the following queries have a specific implementation - duration: Time from the start of the first child to the end of the last child. - rssi: Average rssi of children - channel/channel_LE_idx: Channel of child if all children have the same channel. "" otherwise. - corrupt: True if any of the children are corrupt.

All other queries return a default value if not implemented.