Table of contents

  1. Overview
  2. CoAP TMF headers
    1. Options
    2. Codes
  3. Payload Format
  4. Device Identification
    1. Joiner Finalization Request/Response
    2. Joiner Application Request/Response
    3. Get Vendor Data Request
  5. Python Script
  6. References

Overview

CoAP TMF (Thread Management Framework) is a component of the Thread protocol that uses CoAP over UDP/IPv6 to manage and commission devices in a mesh network. It operates on port 61631 and defines a set of CoAP URI paths (e.g., /c/mg, /c/ur) for tasks like device discovery, configuration, and diagnostics. TMF payloads often use a TLV (Type-Length-Value) format and enable secure, lightweight communication for managing Thread network topology and device roles.

CoAP TMF headers

Options

According to section 10.4.2.1 Mandatory Options from the Specification, the required options:

  • Option (11) - construct the URI Path of a CoAP request. This option appears twice to encode the URI path of the form /x/y.
  • Option (12) - when it is explicitly specified in the message definition.
  • Option (17) - when it is explicitly specified in the message definition.
  • Option (23) [RFC 7959] MUST be supported by a CoAP server when explicitly required in the message definition for the indicated cases. In particular, this option is required for requesting the diagnostic Child Table TLV from a Router that supports a large number of Children (for example, up to 511). For typical Thread Devices, Block2 is not needed.

    Codes

    All CoAP requests used in a TMF message MUST use the POST method

Payload Format

The default payload format has the CoAP ContentFormat application/octet-stream (42). Payload formatting for TMF is a simple TLV format.

Device Identification

This section primarily focus on request/response command exposing device info. Specficially focusing on “TMF Provisioning and Discovery TLVs” (section 8.10.3 from Sepcification). These fields help the Joiner verify the correct Commissioner during finalization and assist in Thread network discovery and commissioning.

Joiner Finalization Request/Response

CoAP Request URI
    coap://[<JR>]:MJ/c/jf
Transaction Pattern
    Req+Rsp_Piggybacked (request part)
CoAP Payload
    State TLV
    Vendor Name TLV
    Vendor Model TLV
    Vendor SW Version TLV
[Vendor Data TLV]
Vendor Stack Version TLV
[Provisioning URL TLV]
CoAP Response Code
    2.04 Changed
Transaction Pattern
    Req+Rsp_Piggybacked (response part)
CoAP Payload
    State TLV

Joiner Application Request/Response

CoAP Request URI
    coap://[<C>]:MM/c/ja
Transaction Pattern
    Req+Rsp_Piggybacked (request part)
CoAP Payload
    Provisioning URL TLV
    Vendor Name TLV
    Vendor Model TLV
    Vendor SW Version TLV
    [Vendor Data TLV]
    Vendor Stack Version TLV
CoAP Response Code
    2.04 Changed
Transaction Pattern
    Req+Rsp_Piggybacked (response part)
CoAP Payload
    State TLV

Get Vendor Data Request

CoAP Request URI
    coap://[<TD>]:MM/c/mg
Transaction Pattern
    Req+Rsp_Piggybacked or Req+Rsp_Separate (request part)
CoAP Payload
    Get TLV

where Get TLV is list of 8-bit type identifiers, from TMF Provisioning and Discovery TLVs

CoAP Response Code
    2.04 Changed
Transaction Pattern
    Req+Rsp_Piggybacked or Req+Rsp_Separate (response part)
CoAP Payload
    Parameter TLVs

Python Script

import argparse
import socket
import sys

def build_coap_tmf_request():
    return bytes([
        0x42, 0x02, 0x12, 0x34,             # Header: ver=1, type=0 (CON), tkl=2, code=POST(0.02)
        0x76, 0x70,                         # Token
        0xb1, 0x63, 0x02, 0x6d, 0x67        # '/c/mg'
    ])

def build_coap_post_response():
    return bytes([
        0x62, 0x44, 0x12, 0x34,             # Header: ver=1, type=2 (ACK), Code=2.04
        0x76, 0x70,                         # Token
        0xB1, 0x63,                         # Uri-Path: delta=11, len=1 → 'c'
        0x02, 0x6D, 0x67,                   # Uri-Path: delta=0, len=2 → 'mg'
        0xFF,                               # Payload marker
        0x0D, 0x38,                         # Get TLV (type=13, length=56)
        0x21, 0x0a, *b"Acme Corp1",         # Vendor Name TLV
        0x22, 0x0b, *b"SmartSensor",        # Model Name TLV
        0x23, 0x08, *b"v1.2.3a\x00",        # SW Version TLV
        0x25, 0x07, *b"ACTIVE!",            # Data TLV
        0x26, 0x07, *b"stk-1.4"             # Stack Version TLV
    ])


def run_client(ip, port):
    packet = build_coap_tmf_request()
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

    try:
        sock.sendto(packet, (ip, port))
        print(f"Sent CoAP POST /c/mg packet to {ip}:{port}")
        sock.settimeout(5)
        data, addr = sock.recvfrom(1024)
        print(f"Received response from {addr}:")
        print(data.hex())
    except socket.timeout:
        print("No response received within timeout.")
    finally:
        sock.close()

def run_server(port):
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.bind(('', port))
    print(f"Listening on UDP port {port} for incoming CoAP packets...")

    try:
        while True:
            data, addr = sock.recvfrom(4096)
            print(f"Received {len(data)} bytes from {addr}:")
            print(data.hex())

            response = build_coap_post_response()
            sock.sendto(response, addr)
            print(f"Sent CoAP 2.04 Changed response to {addr}")
    except KeyboardInterrupt:
        print("\nServer stopped.")
    finally:
        sock.close()

def main():
    parser = argparse.ArgumentParser(description="Simple raw CoAP client/server")
    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument('--client', action='store_true', help='Run as CoAP client')
    group.add_argument('--server', action='store_true', help='Run as CoAP server')
    parser.add_argument('--ip', help='IP address of the server (required for client mode)')
    parser.add_argument('--port', type=int, default=5683, help='UDP port (default 5683 for CoAP)')

    args = parser.parse_args()

    if args.client:
        if not args.ip:
            print("Error: --ip is required when using --client")
            sys.exit(1)
        run_client(args.ip, args.port)
    elif args.server:
        run_server(args.port)

if __name__ == "__main__":
    main()

References

Thread_v1.4.0_Specification.pdf
The Constrained Application Protocol (CoAP) - RFC7252
Uniform Resource Names (URN) Namespaces