Skip to main content
MQTT Explorer provides a flexible decoder system to interpret binary MQTT payloads in various formats. Decoders transform raw binary data into human-readable formats, making it easier to inspect and debug MQTT messages.

Decoder Architecture

The decoder system is built around the MessageDecoder interface, which allows different decoders to be chained and applied to incoming messages:
interface MessageDecoder<T = string> {
  formats: T[]  // Supported format names
  canDecodeTopic?(topic: string): boolean  // Optional topic filter
  canDecodeData?(data: Base64Message): boolean  // Optional data filter
  decode(input: Base64Message, format: T | string | undefined): DecoderEnvelope
}

Available Decoders

MQTT Explorer includes three built-in decoders:

Sparkplug B

Decodes Sparkplug B protocol payloads automatically based on topic patterns

Binary Decoder

Interprets raw binary data as primitive types (int8, uint32, float, etc.)

String Decoder

Default decoder for UTF-8 string payloads

Base64 Message

Foundation class handling binary-to-string conversions

Binary Decoder

The Binary Decoder interprets raw bytes as primitive data types. It supports both single values and arrays.

Supported Formats

FormatDescriptionByte SizeExample Use Case
int8Signed 8-bit integer1Temperature sensors (-128 to 127)
int16Signed 16-bit integer (LE)2Small numeric values
int32Signed 32-bit integer (LE)4Timestamps, counters
int64Signed 64-bit integer (LE)8High-precision timestamps
uint8Unsigned 8-bit integer1Status codes, percentages
uint16Unsigned 16-bit integer (LE)2Port numbers
uint32Unsigned 32-bit integer (LE)4Large counters
uint64Unsigned 64-bit integer (LE)8File sizes
float32-bit floating point (LE)4Sensor readings
double64-bit floating point (LE)8High-precision measurements
All multi-byte integers and floats use Little Endian (LE) byte order.

Using the Binary Decoder

1

Select a topic with binary data

Click on any topic in the tree that contains binary payload data.
2

Open the format dropdown

In the topic details panel, click the format dropdown button (shows current format like “string”).
3

Choose a binary format

Select the appropriate format (e.g., float, uint32) from the dropdown menu.
4

View decoded value

The binary payload will be decoded and displayed as a readable number or array.

Implementation Details

The Binary Decoder reads data using Node.js Buffer methods:
const decodingOption = {
  int8: [Buffer.prototype.readInt8, 1],
  int16: [Buffer.prototype.readInt16LE, 2],
  int32: [Buffer.prototype.readInt32LE, 4],
  int64: [Buffer.prototype.readBigInt64LE, 8],
  uint8: [Buffer.prototype.readUint8, 1],
  uint16: [Buffer.prototype.readUint16LE, 2],
  uint32: [Buffer.prototype.readUint32LE, 4],
  uint64: [Buffer.prototype.readBigUint64LE, 8],
  float: [Buffer.prototype.readFloatLE, 4],
  double: [Buffer.prototype.readDoubleLE, 8],
} as const

Array Decoding

When a payload contains multiple values, the decoder automatically creates an array:
// Input: Buffer [0x01, 0x00, 0x02, 0x00]
// Output: [1, 2]
The payload length must be evenly divisible by the format’s byte size. If not, decoding fails with:
"Data type does not align with message"

String Decoder

The String Decoder is the default decoder for text-based payloads. It converts Base64-encoded message data to UTF-8 strings.
export const StringDecoder: MessageDecoder = {
  formats: ['string'],
  decode(input: Base64Message) {
    return { message: input, decoder: Decoder.NONE }
  },
}

Format Detection

MQTT Explorer automatically attempts to parse string content as JSON for pretty-printing:
{
  "temperature": 22.5,
  "humidity": 65,
  "timestamp": 1638360000
}

Base64Message Class

All decoders work with the Base64Message class, which provides efficient binary-to-string conversion:
class Base64Message {
  // Create from buffer
  static fromBuffer(buffer: Buffer): Base64Message
  
  // Create from string
  static fromString(str: string): Base64Message
  
  // Convert to buffer
  toBuffer(): Buffer
  
  // Convert to string
  toUnicodeString(): string
  
  // Format with type hint
  format(type: 'string' | 'json' | 'hex'): [string, 'json' | undefined]
  
  // Convert to hex representation
  static toHex(message: Base64Message): string
  
  // Create data URI
  static toDataUri(message: Base64Message, mimeType: string): string
}

Example Usage

// From buffer
const msg1 = Base64Message.fromBuffer(Buffer.from([0x48, 0x69]))

// From string
const msg2 = Base64Message.fromString("Hello")

// To hex
const hex = Base64Message.toHex(msg1)  // "0x48 0x69"

Decoder Envelope

Decoders return a DecoderEnvelope that contains the decoded message or error:
interface DecoderEnvelope {
  message?: Base64Message  // Decoded message
  error?: string          // Error if decoding failed
  decoder: Decoder        // Which decoder was used
}

enum Decoder {
  NONE,       // No special decoding
  SPARKPLUG,  // Sparkplug B decoder
}

Error Handling

When decoding fails, the envelope contains an error message:
// Example: Binary decoder alignment error
{
  error: "Data type does not align with message",
  decoder: Decoder.NONE
}

// Example: Sparkplug decoder failure
{
  error: "Failed to decode sparkplugb payload",
  decoder: Decoder.NONE
}

Custom Decoder Development

To create a custom decoder, implement the MessageDecoder interface:
1

Define your decoder

export const CustomDecoder: MessageDecoder = {
  formats: ['custom-format'],
  
  canDecodeTopic(topic: string) {
    // Optional: filter by topic pattern
    return topic.startsWith('custom/')
  },
  
  decode(input: Base64Message, format: string) {
    try {
      const buffer = input.toBuffer()
      // Your custom decoding logic here
      const decoded = yourDecodingFunction(buffer)
      
      return {
        message: Base64Message.fromString(JSON.stringify(decoded)),
        decoder: Decoder.NONE
      }
    } catch (error) {
      return {
        error: `Failed to decode: ${error.message}`,
        decoder: Decoder.NONE
      }
    }
  }
}
2

Register the decoder

Add your decoder to the decoders array in app/src/decoders/index.ts:
export const decoders = [
  SparkplugDecoder,
  CustomDecoder,  // Add here
  BinaryDecoder,
  StringDecoder
] as const
3

Test your decoder

Publish test messages to topics matching your pattern and verify decoding works correctly.
Decoders are tried in order. Place more specific decoders (like Sparkplug) before generic ones (like String).

Decoder UI Integration

The decoder system integrates with the UI through the TopicTypeButton component:
// User selects format from dropdown
node.viewModel.decoder = { decoder, format }

// Decoder is applied automatically when rendering
const decoded = decoder.decode(message.payload, format)

Visual Indicators

The UI shows decoder status:
  • Format button: Displays current format (e.g., “Sparkplug”, “float”, “string”)
  • Warning icon: Appears if decoding fails with error tooltip
  • “Decoded SparkplugB”: Label shown when Sparkplug decoder is active
The useDecoder hook provides reactive decoder updates:
export function useDecoder(
  treeNode: TreeNode<TopicViewModel> | undefined
): DecoderFunction {
  const viewModel = useViewModel(treeNode)
  const [decoder, setDecoder] = useState(viewModel?.decoder)
  
  useSubscription(viewModel?.onDecoderChange, setDecoder)
  
  return useCallback(
    message =>
      decoder && message.payload
        ? decoder.decoder.decode(message.payload, decoder.format)
        : { message: message.payload ?? undefined, decoder: Decoder.NONE },
    [decoder]
  )
}
This hook ensures the UI updates when the user changes the decoder format.

Best Practices

Choose the Right Format

Match the decoder format to your device’s actual data type to avoid misinterpretation

Handle Errors Gracefully

Always return error messages in the DecoderEnvelope rather than throwing exceptions

Optimize for Performance

Decoders run on every message update, so keep decoding logic efficient

Test Edge Cases

Verify behavior with empty payloads, misaligned data, and malformed content

See Also

Build docs developers (and LLMs) love