Custom decoders#

Network Analyzer provides functionality to create custom decoders. This section provides instructions on how to use Lua, a language embedded inside Network Analyzer, to develop custom payload decoders for Simplicity Studio.

Procedure#

There are five main steps to leverage this functionality.

Step 1: Writing a decoder#

All Lua decoders must implement the following six functions.

  • name(): Returns a string that will be used as the name of your decoder.

  • filterName(): Returns a string that will serve as the filterName that can be used to filter the packets decoded by this decoder later on.

  • description(): Returns a string that will act as the description of the frame in Network Analyzer.

  • enabled(): Returns a boolean value that corresponds to whether the decoder should be active by default when first loaded/when defaults are reapplied.

  • accept(event, packet): Decides which packets your decoder is interested in. It has two objects, of type Event and Packet. The packet object can be used to figure out the raw bytes of the entire packet or just the bytes of the payload. The event object can be used to identify and read information from prior frames via get() and contain() functions.

  • decode(event, fieldContext, formatter): Handles the actual decoding. It has two objects, of type Event and FieldContext. The decode process functions by using the append()`` and decode()`` functions to create and register different fields of interest. The formatter field is optional and is used for formatting the fields of your decoder.

More detailed decoder examples can be found under Example Decoders.

Step 2: Loading into Simplicity Studio#

After the decoder is ready to be tested, load it into Simplicity Studio.

  1. Open Simplicity Studio

  2. Go to Window -> Preferences -> Network Analyzer -> Decoding

  3. Click Add, select the LUA file that corresponds to our decoder, and then click Open.

You should now see the decoder under the table of Decoders.

Step 3: Editing the Decoder#

Once the decoder is loaded, you can now edit it in Simplicity Studio. Click Edit under the decoding screen to open a text editor where you can modify the file. Note that if you save your decoder while there are syntax errors, then you may need to re-add your decoder.

Step 4: Testing the Decoder#

Once the decoder is loaded into Studio, you can test it against specific packets by doing the following:

  1. From the Decoding page, click decoder in the table of custom decoders.

  2. Click Test.

  3. Set the desired options (for example, Input Format or Payload vs. Full Over The Air)

  4. Enter the raw payload or over-the-air packet bytes in the input textbox.

NOTE: The OUT box does not work and will be fixed in a future release.

You see how this works by looking at the example decoders and trying the example packets that are listed. You can also have additional inline debug information printed to screen with print() statements within the decoder.

Step 5: Use with Network Captures#

If the decoder is enabled (as indicated by the checkbox next to its entry in the Preference -> Decoding page), then you can decode messages using this decoder in Network Analyzer.

Note: To have logging during longer network captures, go to Preference -> Decoding and check Enable Debug Logging. This will write to file any calls to the following function:

log("message")

See Example decoders to see how this is done.

Field Formatters#

You can enhance the decoded information by implementing custom field formatters. Currently, Network Analyzer supports user-written functions that take in an integer and outputs a string that can be used as a description. Note that these formatters can sometimes get in null information, so these functions should be able to deal with null.

Example of a formatter:

function decoder.versionName(version)
    local versions = {
        [0] = "Version 1",
        [1] = "Version 2",
        [2] = "Version 3",
        [3] = "Version 4",
    }

    return versions[version] or "Unknown version"
end

In order to use this functionality, include the following line at the top of your custom decoder:

decoder.fieldFormatters = { "versionName" }

Each of the strings within this array should correspond to the names of the function that you are using as a formatter.

To use these formatters in your decoder, add a third argument to the decode functions that corresponds to a map of formatters that are indexed by the strings given in fieldFormatter.

For example, in function decode(event, fieldcontext, formatter), formatter is a map, indexed by function name, that corresponds to the formatter.

Once this is done, you can use a formatter by adding a third argument to many of fieldContext's append and decode functions.

fc.append("versionName", 1, formatter["versionName"]);

See Example decoders to see how this is done.

The list of currently supported append/decode functions that allow for a field formatter is provided in the next sections.

Event Object API#

void setStderr(final String err) - Sets the stderr of the decoder.

  • Parameters: err

void setStdout(final String out) - Sets the stdout of the decoder.

  • Parameters: out

Object get(final String eventKey, final String subkey) - Wrapper function to access the value associated in the decorated map. Mainly for use with custom user payload decoders.

  • Parameters:

    • EventKey — A string that must corresponds to the name of a specific frame. See EventKey Documentation.

    • Subkey — A string that corresponds to field names. See FieldContext API to see how to generate these fields.

  • Returns: Object Null if it does not exist.

int getInt(final String eventKey, final String subkey) - Wrapper function to access the value associated in the decorated map. Mainly for use with custom user payload decoders.

  • Parameters:

    • EventKey — A string that must corresponds to the name of a specific frame. See EventKey Documentation.

    • Subkey — A string that corresponds to field names. See FieldContext API to see how to generate these fields.

  • Returns: int Null if it does not exist.

boolean getBoolean(final String eventKey, final String subkey) - Wrapper function to access the value associated in the decorated map. Mainly for use w/ custom user payload decoders.

  • Parameters:

    • EventKey — A string that must corresponds to the name of a specific frame. See EventKey Documentation.

    • Subkey — A string that corresponds to field names. See FieldContext API to see how to generate these fields.

  • Returns: boolean Null if it does not exist.

boolean containsSubkey(final String eventKey, final String subkey - Wrapper function so that users can figure out if a frame/subkey combination is known by the decorated map.

  • Parameters:

    • EventKey — A string that must corresponds to the name of a specific frame. See EventKey Documentation.

    • Subkey — A string that corresponds to field names. See FieldContext API to see how to generate these fields.

  • Returns: boolean True if the subkey's corresponding field is found under the frame given by EventKey

boolean containsKey(final String eventKey) - Wrapper function so that users can figure out if an frame/eventKey is known by the the event.

  • Parameters: Subkey — A string that corresponds to field names. See FieldContext API to see how to generate these fields.

  • Returns: boolean True if the event encountered or recognizes the event key in question, false otherwise.

Packet Object API#

byte[] currentLayerBytes() - Returns raw byte array. Decryptors need this.

int currentOffset() - Returns the current offset where the decoders need to pick up.

void setSummary(final String summary) - Sets the summary string.

  • Parameters: summary

String summary() - Returns the packet summary string.

void setCorruption(final String message) - Backwards compatibility. JC interface needs this.

  • Parameters: message

void skip(final int skipBytes) - Skips bytes. Remove after packet decoders are cleaned up.

void setEndOffset(final int end)

  • Parameters: end

void setOffset(final int offset)

  • Parameters: offset

int micCount() - Returns number of all mics

int endOffset() - Returns the end of "over the air" packet.

  • Returns: end of over the air packet

int flagBits()

void setFlagBits(final int bits)

  • Parameters: bits

boolean isFragment() - Returns true if this packet is a fragment.

  • Parameters: `` —

  • Returns: boolean

FieldContext Object API#

void append(String name, int length) - Appends the number of bytes as a field. This method is more efficient than decode, so if you do not need the value of the field, use this.

Example Input and Output:

0x01234567 -> 0x01234567

  • Parameters:

    • name — The name of the field that is to be registered.

    • length — An integer number of bytes to read from the payload and save as name.

void appendBytes(String name, int length) - Appends the number of bytes as given by length as a byte field. This method is more efficient than decode, so if you do not needs the value of the field, use this.

Example Input and Output:

0x01234567 -> 01 23 45 67

  • Parameters:

    • name — The name of the field that is to be registered.

    • length — An integer number of bytes to read from the payload and save as name.

void appendEui64(String name) - Appends the next 8 bytes as the EUID of the sender. This method is more efficient than decode, so if you do not needs the value of the field, use this. The format will be Little Endian here.

Example Input and Output:

0x89ABCDEF01234567 -> 67452301EFCAB89

  • Parameters: name — The name of the field that is to be registered.

void appendFloat(String name, int length) - Appends the field as a float. This method is more efficient than decode, so if you do not needs the value of the field, use this.

Example Input and Output:

0x00000001x -> 1.4 E-45

  • Parameters:

    • name — The name of the field that is to be registered.

    • length — An integer number of bytes to read from the payload and save as name.

void appendFloatLE(String name, int length) - Appends the field as a float in little endian order. This method is more efficient than decode, so if you do not needs the value of the field, use this.

Example Input and Output:

  • Parameters:

    • name — The name of the field that is to be registered.

    • length — An integer number of bytes to read from the payload and save as name.

      0x01000001x -> 1.4 E-45

void appendRemainingBytesLE(String name) - Appends the remaining bytes in the packet in little endian order. This method is more efficient than decode, so if you do not needs the value of the field, use this.

  • Parameters: name — The name that will be used to refer to the field containing the remaining bytes

int decode(String name, int length) - Decodes the field as an int and returns value.

  • Parameters:

    • name — The name of the field that is to be registered.

    • length — An integer number of bytes to read from the payload and save as name.

  • Returns: int The value that was decoded from the payload and copied into the field called name.

int decodeLE(String name, int length) - Decodes the field as an int in little endian format and returns value.

  • Parameters:

    • name — The name of the field that is to be registered.

    • length — An integer number of bytes to read from the payload and save as name.

  • Returns: int The value that was decoded from the payload and copied into the field called name.

String decodeString(String name, int length) - Decodes length bytes as a string.

Example Output:

0x68656c6c6f20776f726c64 -> 'hello world'

  • Parameters:

    • name — The name of the field the string will be registered as.

    • length — The number of bytes to be decoded as a string.

void appendStringWithLength(String name) - Appends a string that first contains a length byte and then the remainder as bytes.

  • Returns: String

boolean decodeBit(String name) - Decodes and returns the value of the next bit in the payload.

  • Parameters: name — The name of the field that is to be registered.

  • Returns: boolean The value of the decoded bit.

int decodeBits(String name, int numOfBits) - Decodes bits as field name. Identical to bits(name.name(), numOfBits);

  • Parameters:

    • name — The name of the field that is to be registered.

    • numOfBits — The number of bits that are to be decoded jointly.

  • Returns: int The value of the decoded bits as an integer.

void bit(String name) - This method heavily depends on the last field being decoded. So it's meaningful to be used kinda only in context of last field. You would do this: fc.decode(flags); fc.bit(flag1); fc.bit(flag2); fc.bit(flag3);

As each field take length from flags and does internal bitshifting to calculate masks of flag1 flag2.

  • Parameters: name — The name of the field that the bit is to be decoded as.

void bits(String name, int numOfBits) - Appends numOfBits within a bitfield, labeling then name.

  • Parameters:

    • name — The name of the field that the bit is to be decoded as.

    • numOfBits — The number of bits that are to be decoded as a group.

void namelessBitField(int nBytes) - Declares the start of the nameless bit field.

  • Parameters: nBytes — The number of bytes to be treated as a nameless field.

void namelessBitFieldLE(int nBytes) - Declares the start of a nameless bit field.

  • Parameters: nBytes — The number of bytes to be treated as a nameless field.

void skipBits(int numOfBits) - Skips given number of bits in decoding.

  • Parameters: numOfBits — The number of bits to be skipped.

void skipBytes(int n) - Skips n bytes without decoding them.

void appendString(String name, int length) - Appends length bytes as a string. This method is more efficient than decode, so if you do not needs the value of the field, use this.

Example Output:

0x68656c6c6f20776f726c64 -> 'hello world'

  • Parameters:

    • name — The name of the field the string will be registered as.

    • length — The number of bytes to be decoded as a string.

void appendSummary(String append) - Appends a message to describe the packet in network analyzer. This can be useful for describing what kind of payload the packet is.

  • Parameters: append — A string that will be appended to the packet's summary.

void setSummary(String summary) - Sets the summary for what kind of event the payload could be. This will be what is read under the the network analyzer.

  • Parameters: summary — The summary that will be read under network analyzer.

void setPrefix(String prefix) - Sets a prefix for the payload.

A prefix will be prepended to the fields that are decoded by this decoder.

  • Parameters: prefix — A string that will be prepended to the names of fields for this decoder.

void applyPrefixToSummary() - Sets the summary to be the prefix associated with the protocol. See fc.setPrefix()

String codeNameSuffix() - Returns the codeNameSuffix, the suffix that is appeneded to each field.

  • Returns: String The codeNameSuffix currently set for the decoder.

**int currentOffset()** - Returns the current offset for next element to be decoded in the payload.

  • Returns: int The current offset within the packet.

boolean hasRemainingBytes() - Returns true if there are remaining bytes left in the payload that have not been updated.

  • Returns: boolean True if there are remaining bytes left.

int remainingBytes() - Returns the number of bytes left before the end of the payload

  • Returns: int The remaining bytes that have been not been processed within the payload.

void setAppendLengthsToFieldCodes(boolean flag) - Toggles the mode where the lengths of the fields are appended to the names.

If you are decoding field "x" with length, 2, then the actual name of the field will be "x_2" if the length appending mode is true, and only "x" if its false.

  • Parameters: flag — Setting this true will append lengths to the end of fields as stated above.

void setCodeNameSuffix(String codeNameSuffix) - Sets the codeNameSuffix

  • Parameters: codeNameSuffix — A string that will be appended to the ends of field names.

EventKey / Subkey Fields#

To see the full list of EventKeys that are currently available, go to Window -> Preferences -> Network Analyzer -> Decoding -> Frames and Fields.

This window displays the valid frames and associated fields for your selected stack and profile. Note that the string you should use as the EventKey is the text that is located within brackets (e.g., if there is line "IEEE 802.15.4 [fifteenFour]", then you should refer to that frame using "fifteenFour").

If you expand the tree under each frame, then you will be able to see all the valid fields, and the text string that can be used as a subkey to search for them.

Example Decoders#

Custom Decoder Skeleton#

-- Custom Decoder Skeleton (custom-decoder-skeleton.lua)
-- Users should use this as a framework to build their decoders.

-- Create a table to hold all the functions
local decoder = {}

-- This will let you set what the name of the frame should be when viewed in the network analyzer.
decoder.name = "Custom Decoder Skeleton"

-- This will determine what the decoder can be filtered by in the network analyzer when viewing all packets.
decoder.filterName = "SampleDecoder"

-- This is a function that is describing what the frame should have as its description
-- when you are viewing specific information in the network analyzer.
decoder.description = "This is an example of how you can describe a frame"

-- This function will let you set whether the decode should be enabled by default when
-- loading it for the first time or when resetting the settings to default.
decoder.enabled = true

-- This function will do the actual decoding of the payload.
-- You should primarily use the functions as defined for the FieldContext object fc to do this.
-- The functions for event can be used to process information from earlier frames.
function decoder.decode(event, fieldContext)
   -- Your decoding logic here
end

-- This function determines if an event is relevant to this decoder.
-- Note that this should be specific to the payload; if it is too generic, 
-- then event payloads belonging to other decoders may be mistakenly read and decoded by this decoder.
function decoder.accept(event, packet)
   return false
end

return decoder

Field Formatter Example#

-- Example Decoder for using a field formatter (field-formatter-example.lua)
-- Create a table to hold all the functions
local decoder = {}

decoder.name = "Field Formatter Example"
decoder.filterName = "fieldFormatterExample"
decoder.description = "This is an example of how you can log debug information and test your decoder"
decoder.enabled = true

-- The list of field formatters
local fieldFormatters = { "decimal", "enuming", "bitFields", "bitDescription" }

-- This function will do the actual decoding of the payload.
-- Note that the function call when using custom decoding requires
-- the addition of the third parameter formatter.
function decoder.decode(event, fc, formatter)
   fc:setAppendLengthsToFieldCodes(true)
   fc:append("append", 4, formatter["decimal"])
   fc:decodeLE("appendLe", 4, formatter["enuming"])
   fc:namelessBitField(4)
   fc:bits("decodeBits", 3, formatter["bitFields"])
   fc:bit("decodedBit", formatter["bitDescription"])
end

-- This function determines if an event is relevant to this decoder.
function decoder.accept(event, packet)
   return true
end

-- These are three examples of field formatters.
-- Note that these functions MUST be able to handle null values.
function decimal(value)
   return tostring(value)
end

function enuming(value)
   if value == 0 then
       return "ZERO"
   elseif value == 1 then
       return "ONE"
   elseif value == 2 then
       return "TWO"
   else
       return "NOT ONE OR ZERO"
   end
end

function bitFields(value)
   if value == tonumber("0001", 2) then
       return "1"
   elseif value == tonumber("0010", 2) then
       return "2"
   elseif value == tonumber("0100", 2) then
       return "3"
   else
       return "invalid"
   end
end

function bitDescription(value)
   return (value and "1" or "0")
end

return decoder

Payload Decoder Logging Example#

-- Sample Logging Examples for testing and debugging (payload-decoder-logging-example.lua)
-- Users should see this for how they may be able to do some initial logging.

-- Create a table to hold all the functions
local decoder = {}

decoder.name = "Payload Decoder Logging Example"
decoder.filterName = "debugExampleDecoder"
decoder.description = "This is an example of how you can log debug information and test your decoder"
decoder.enabled = true

function decoder.decode(event, fieldContext)
   -- This will show up under the test console, but not the log file.
   print("Hello world!")

   -- This will show up under the log file, but not the test console.
   local a = fieldContext:decode("var1", 2)
   log(a)
end

function decoder.accept(event, packet)
   return false
end

return decoder

Sensor Sink Example#

-- Example Packet w/ Length byte for Full Over the Air (sensor-sink.lua)
-- 384188E8FF01FFFF00000803111100000AD5C12D0610000084E10A00006F0D0000080101000FC0010184E10A00006F0D000000FB8B7AC0220903

-- Create a table to hold all the functions
local decoder = {}

decoder.name = "Sensor Sink Example"
decoder.filterName = "sensor"
decoder.description = "Lorem Ipsum"
decoder.enabled = true

function decoder.decode(event, fc)
   fc:appendEui64("SenderEuid64")
   local clusterId
   if event:containsSubkey("zigbeeApplicationSupport", "clusterId") == true then
       clusterId = event:getInt("zigbeeApplicationSupport", "clusterId")
   else
       clusterId = event:getInt("zigbeeApplicationSupport", "clusterIdV2")
   end
   if clusterId == 0x01 then
       fc:setSummary("Sink Advertise")
       fc:append("senderShortId", 2)
   elseif clusterId == 0x02 then
       fc:setSummary("Sensor Select Sink")
   elseif clusterId == 0x03 then
       fc:setSummary("Sink Ready")
   elseif clusterId == 0x04 then
       fc:setSummary("Sink Query")
   elseif clusterId == 0x0A then
       fc:setSummary("Sensor Data")
       fc:appendBytes("SensorData", 40)
   elseif clusterId == 0x64 then
       fc:setSummary("Sensor Hello")
       fc:appendString("hello", 5)
   end
end

function decoder.accept(event, packet)
   local accept = false
   local clusterId

   if event:containsSubkey("zigbeeApplicationSupport", "clusterId") == true then
       clusterId = event:getInt("zigbeeApplicationSupport", "clusterId")
   else
       clusterId = event:getInt("zigbeeApplicationSupport", "clusterIdV2")
   end

   local profileId = event:getInt("zigbeeApplicationSupport", "profileId")

   if (clusterId == 0x01 or clusterId == 0x02 or clusterId == 0x03 or clusterId == 0x04 or clusterId == 0x0A or clusterId == 68) and (profileId == 0xC00F) then
       accept = true
   end

   return accept
end

return decoder

Payload Decoding Example#

-- Example Packet w/ no length byte and payload only (payload-decoding-example.lua)
-- 01234567 89abcdef 01234567 89abcdef01234567 00000001 01000000 68656c6c6f20776f726c64 abcd abcd ffcdabcd 0f 00000000ff
-- Similar Example that is corrupted instead:
-- 01234567 89abcdef 01234567 89abcdef01234567 00000001 01000000 68656c6c6f20776f726c64 abcd abcd ffcdabcd ff 00000000ff

-- Create a table to hold all the functions
local decoder = {}

decoder.name = "Payload Decoding Example"
decoder.filterName = "exd"
decoder.description = "Lorem Ipsum"
decoder.enabled = true

function decoder.decode(event, fc)
   fc:setAppendLengthsToFieldCodes(true)
   fc:append("append", 4)
   fc:appendLE("appendLe", 4)
   fc:appendBytes("appendBytes", 4)
   fc:appendEui64("Eui64")
   fc:appendFloat("appendFloat", 4)
   fc:appendFloatLE("AppendFloatLe", 4)
   fc:appendString("AppendString", 11)
   fc:decode("decode", 2)
   fc:setPrefix("Prefix")
   fc:decodeLE("decodeLe", 2)
   fc:decodeBytes("decodeBytes", 4)
   fc:namelessBitField(1)
   fc:bit("bitTest0")
   fc:setSummary("Hello! ")

   if fc:decodeBit("Error") == true then
       fc:bits("Error Code", 3)
       fc:setCorruption("Error was set!")
       fc:setSummary("Failed!")
   else
       fc:decodeBits("Data", 3)
       fc:appendSummary("Success")
   end

   fc:setCodeNameSuffix("suffix")
   fc:skipBytes(4)
   fc:appendRemainingBytesLE("appendRemainingBytesLe")
end

function decoder.accept(event, packet)
   return true
end

return decoder