<?xml version="1.0" encoding="utf-8"?>
<!-- name="GENERATOR" content="github.com/mmarkdown/mmark Mmark Markdown Processor - mmark.miek.nl" -->
<rfc version="3" ipr="trust200902" docName="draft-bustamante-iotmp-00" submissionType="IETF" category="std" xml:lang="en" xmlns:xi="http://www.w3.org/2001/XInclude" indexInclude="true">

<front>
<title abbrev="IOTMP">Internet of Things Message Protocol (IOTMP)</title><seriesInfo value="draft-bustamante-iotmp-00" stream="IETF" status="standard" name="Internet-Draft"></seriesInfo>
<author initials="A." surname="Luis Bustamante" fullname="Alvaro Luis Bustamante"><organization>Internet of Thinger SL</organization><address><postal><country>Spain</country>
</postal><email>alvaro@thinger.io</email>
</address></author><date year="2026" month="March" day="30"></date>
<area>ART</area>
<workgroup>Internet Engineering Task Force</workgroup>
<keyword>IoT</keyword>
<keyword>protocol</keyword>
<keyword>binary</keyword>
<keyword>streaming</keyword>
<keyword>constrained</keyword>

<abstract>
<t>IOTMP (Internet of Things Message Protocol) is a compact, binary, application-layer protocol for bidirectional communication between IoT devices and servers over persistent connections. It provides a resource-oriented model with native support for remote procedure calls, real-time data streaming, and API introspection. IOTMP uses PSON (Packed Sensor Object Notation) as its data encoding format, achieving significantly lower wire overhead than text-based alternatives. The protocol is designed for constrained devices with limited memory and bandwidth, while remaining expressive enough for complex IoT applications. IOTMP operates over TCP, TLS, or WebSocket transports and supports symmetric operation where both clients and servers may expose and invoke resources.</t>
</abstract>

</front>

<middle>

<section anchor="introduction"><name>Introduction</name>

<section anchor="purpose"><name>Purpose</name>
<t>IOTMP (Internet of Things Message Protocol) is an application-layer protocol designed for efficient, bidirectional communication between IoT devices and servers. It provides a resource-oriented, request-response model with native support for real-time data streaming, optimized for constrained devices with minimal memory and bandwidth.</t>
</section>

<section anchor="design-goals"><name>Design Goals</name>

<ul spacing="compact">
<li><strong>Compactness:</strong> Minimize wire overhead through binary encoding (PSON) and varint-based framing.</li>
<li><strong>Bidirectional RPC:</strong> Enable both client-to-server and server-to-client remote procedure calls over a single persistent connection.</li>
<li><strong>Resource Orientation:</strong> Model device capabilities as named resources with typed I/O, enabling auto-discovery and introspection.</li>
<li><strong>Native Streaming:</strong> Support periodic and event-driven data streams without additional protocol layers.</li>
<li><strong>Transport Agnostic:</strong> Operate over TCP, TLS, or WebSocket transports.</li>
<li><strong>Embedded-Friendly:</strong> Fit within the memory and processing constraints of microcontrollers (e.g., ESP32, Arduino, Zephyr-based devices).</li>
</ul>
</section>

<section anchor="rationale-for-pson"><name>Rationale for PSON</name>
<t>IOTMP uses PSON (Packed Sensor Object Notation) as its data encoding format rather than an existing binary encoding such as CBOR <xref target="RFC8949"></xref>. The primary motivation is architectural: PSON and IOTMP share the same encoding primitives, eliminating the need for two independent encoding systems on constrained devices.</t>
<t><strong>Protocol-Encoding Integration.</strong> IOTMP's message framing layer (Section 7) and PSON use identical encoding primitives: a tag-byte structure where type bits and an inline value are packed into a single byte, and varint encoding for variable-length integers. A single encoder/decoder implementation on a constrained device serves both the protocol framing layer (message fields) and the application data layer (payloads). Adopting an external encoding like CBOR would require a second, independent type system alongside the protocol's own field encoding -- two tag formats, two integer encodings, two sets of type definitions -- increasing code size and implementation complexity for no functional benefit.</t>
<t>This integration is particularly valuable on the most constrained targets (Cortex-M0/M0+, 2-16 KB RAM), where every kilobyte of code matters. A complete IOTMP + PSON implementation shares encoding routines between framing and payload, keeping the combined footprint under 400 lines of C.</t>
<t><strong>Comparison with CBOR.</strong> In terms of wire size, PSON and CBOR produce comparable encodings for typical IoT data. Neither format has a significant size advantage over the other. The differences are in scope and complexity: CBOR is a general-purpose encoding designed to cover a wide range of use cases, including semantic tags (CBOR type 6, with hundreds of IANA-registered values), indefinite-length encoding, half-precision floats, and extensible simple values. PSON deliberately omits these features, which add decoder complexity without benefit for IoT applications. This reduced scope, combined with the shared primitives with IOTMP, is what makes PSON the appropriate choice for this protocol -- not a claim of superior compression.</t>
<t>PSON is specified in a companion document: <xref target="PSON"></xref>.</t>
</section>

<section anchor="scope"><name>Scope</name>
<t>This specification defines:</t>

<ul spacing="compact">
<li>The message framing format</li>
<li>The set of message types and their semantics</li>
<li>The PSON binary encoding format</li>
<li>The connection lifecycle (authentication, keepalive, disconnection)</li>
<li>The resource model and streaming semantics</li>
</ul>
<t>This specification does NOT define:</t>

<ul spacing="compact">
<li>Transport-level security configuration (deferred to TLS)</li>
<li>Application-level authorization policies</li>
<li>Specific resource naming conventions</li>
</ul>
</section>

<section anchor="applicability"><name>Applicability</name>
<t>IOTMP is designed for scenarios where IoT devices maintain persistent connections with a server (broker) and require bidirectional communication. The protocol is most appropriate when:</t>

<ul spacing="compact">
<li><strong>Devices need to be both controlled and observed</strong> over the same connection -- reading sensor data, invoking actions, and streaming telemetry without separate protocols for each pattern.</li>
<li><strong>Low wire overhead matters</strong> -- constrained devices on cellular, satellite, or low-bandwidth links where every byte counts.</li>
<li><strong>The server needs to initiate operations on the device</strong> -- pushing configuration, invoking remote procedures, or starting/stopping data streams, without the device polling.</li>
<li><strong>Real-time interaction is required</strong> -- interactive sessions (terminals, proxies), live dashboards, or event-driven streaming with sub-second latency.</li>
<li><strong>API introspection is valuable</strong> -- auto-discovery of device capabilities, schema-based validation, and automatic UI or API generation from resource descriptions.</li>
</ul>
<t>IOTMP is NOT the best fit when:</t>

<ul spacing="compact">
<li><strong>One-to-many broadcast is the primary pattern</strong> -- MQTT's publish/subscribe model is better suited for fan-out messaging to many subscribers on a topic.</li>
<li><strong>Devices cannot maintain persistent connections</strong> -- battery-powered sensors that wake briefly to send a single reading are better served by CoAP over UDP, which supports connectionless operation.</li>
<li><strong>Interoperability with existing ecosystems is required</strong> -- deployments that must integrate with existing MQTT or LwM2M infrastructure should use those protocols unless a bridge is acceptable.</li>
<li><strong>The application is purely request-response with no device state</strong> -- plain HTTP may be simpler when there is no need for persistent connections, streaming, or server-initiated operations.</li>
</ul>
</section>
</section>

<section anchor="terminology"><name>Terminology</name>
<t>The key words &quot;MUST&quot;, &quot;MUST NOT&quot;, &quot;REQUIRED&quot;, &quot;SHALL&quot;, &quot;SHALL NOT&quot;, &quot;SHOULD&quot;, &quot;SHOULD NOT&quot;, &quot;RECOMMENDED&quot;, &quot;NOT RECOMMENDED&quot;, &quot;MAY&quot;, and &quot;OPTIONAL&quot; in this document are to be interpreted as described in BCP 14 <xref target="RFC2119"></xref> <xref target="RFC8174"></xref> when, and only when, they appear in all capitals, as shown here.</t>

<ul spacing="compact">
<li><strong>Client:</strong> An IoT device or application that initiates a connection to a server.</li>
<li><strong>Server:</strong> The endpoint that accepts client connections and manages device state.</li>
<li><strong>Resource:</strong> A named endpoint on a client that can be invoked (read, written, or called) by the server.</li>
<li><strong>Stream:</strong> A continuous flow of data from a resource, initiated by a START_STREAM message and terminated by STOP_STREAM.</li>
<li><strong>Stream ID:</strong> A 16-bit unsigned integer used to correlate requests with responses and to identify active streams. The ID space is partitioned: client-initiated requests use even IDs (0, 2, 4, ...), server-initiated requests use odd IDs (1, 3, 5, ...).</li>
<li><strong>PSON:</strong> Packed Sensor Object Notation -- the binary encoding format used for structured data within IOTMP messages.</li>
<li><strong>Varint:</strong> A variable-length integer encoding where each byte uses 7 bits for data and 1 bit (MSB) as a continuation flag.</li>
</ul>
</section>

<section anchor="protocol-overview"><name>Protocol Overview</name>
<t>IOTMP operates over a persistent, full-duplex connection. After transport establishment, the client authenticates via a CONNECT/OK handshake. Once authenticated, either side can send messages at any time.</t>
<t>The protocol supports five interaction patterns:</t>

<ol spacing="compact">
<li><strong>Request-Response:</strong> A RUN or DESCRIBE message is sent with a Stream ID; the peer responds with OK or ERROR carrying the same Stream ID.</li>
<li><strong>Streaming:</strong> A START_STREAM message initiates periodic or event-driven STREAM_DATA messages from a resource. STOP_STREAM terminates the stream.</li>
<li><strong>Fire-and-Forget:</strong> DISCONNECT messages require no response.</li>
<li><strong>Keepalive:</strong> The client periodically sends a KEEP_ALIVE message; the server MUST echo it back. This mechanism does not use Stream IDs. The server determines client liveness by monitoring incoming messages of any type.</li>
<li><strong>Server-Initiated RPC:</strong> The server MAY send RUN messages to the client to read properties, set values, or invoke device resources.</li>
</ol>

<artwork><![CDATA[+--------+                              +--------+
| Client |                              | Server |
+---+----+                              +---+----+
    |           CONNECT [credentials]       |
    |-------------------------------------->|
    |                  OK                   |
    |<--------------------------------------|
    |                                       |
    |           DESCRIBE (full API)         |
    |<--------------------------------------|
    |           OK [resource list]          |
    |-------------------------------------->|
    |                                       |
    |      START_STREAM [resource, interval]|
    |<--------------------------------------|
    |           OK                          |
    |-------------------------------------->|
    |           STREAM_DATA [payload]       |
    |-------------------------------------->|
    |           STREAM_DATA [payload]       |
    |-------------------------------------->|
    |               ...                     |
    |           STOP_STREAM                 |
    |<--------------------------------------|
    |           OK                          |
    |-------------------------------------->|
    |                                       |
    |     RUN [WRITE_BUCKET, data]          |
    |-------------------------------------->|
    |           OK                          |
    |<--------------------------------------|
    |                                       |
    |           KEEP_ALIVE                  |
    |-------------------------------------->|
    |           KEEP_ALIVE                  |
    |<--------------------------------------|
    |                                       |
    |           DISCONNECT                  |
    |-------------------------------------->|
    +---------------------------------------+
]]></artwork>
</section>

<section anchor="transport-layer"><name>Transport Layer</name>

<section anchor="supported-transports"><name>Supported Transports</name>
<t>IOTMP is designed to operate over reliable, ordered, byte-stream transports:</t>
<table>
<thead>
<tr>
<th>Transport</th>
<th>Default Port</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td>TCP</td>
<td>25204</td>
<td>Unencrypted TCP connection</td>
</tr>

<tr>
<td>TLS over TCP</td>
<td>25206</td>
<td>TLS-encrypted TCP connection</td>
</tr>

<tr>
<td>WebSocket</td>
<td>443</td>
<td>WebSocket upgrade over HTTPS, for NAT traversal</td>
</tr>
</tbody>
</table><t>Implementations SHOULD support TLS. Unencrypted TCP SHOULD only be used in trusted networks or during development.</t>
</section>

<section anchor="connection-establishment"><name>Connection Establishment</name>

<ol spacing="compact">
<li>The client establishes a transport-level connection (TCP handshake, TLS negotiation, or WebSocket upgrade).</li>
<li>Upon successful transport connection, the client MUST send a CONNECT message as the first IOTMP message.</li>
<li>The server MUST respond with OK or ERROR.</li>
<li>No other message types SHALL be sent by the client before receiving a successful CONNECT response.</li>
</ol>
</section>

<section anchor="websocket-transport"><name>WebSocket Transport</name>
<t>When operating over WebSocket, the following requirements apply:</t>

<ul spacing="compact">
<li>The client MUST request the subprotocol <tt>&quot;iotmp&quot;</tt> in the <tt>Sec-WebSocket-Protocol</tt> header during the WebSocket handshake.</li>
<li>The server MUST confirm the <tt>&quot;iotmp&quot;</tt> subprotocol in the handshake response. If the server does not confirm it, the client MUST close the connection.</li>
<li>Each WebSocket message MUST contain exactly one complete IOTMP message. IOTMP messages MUST NOT be fragmented across multiple WebSocket frames.</li>
<li>The WebSocket message type MUST be binary (opcode 0x02).</li>
</ul>
</section>

<section anchor="byte-ordering"><name>Byte Ordering</name>
<t>All multi-byte numeric values (floats, doubles) MUST be encoded in <strong>little-endian</strong> byte order. Varint encoding is byte-order independent by design.</t>
<t><strong>Rationale:</strong> Traditional Internet protocols use big-endian (&quot;network byte order&quot;), a convention established in the 1980s when the dominant networking hardware (Motorola 68000, SPARC, IBM mainframes) was big-endian. Today, virtually all microcontrollers and processors used in IoT are little-endian: ARM Cortex-M (the dominant embedded architecture), ESP32 (Xtensa), RISC-V, and x86. No widely deployed IoT microcontroller uses big-endian as its native byte order.</t>
<t>On the most constrained ARM cores (Cortex-M0/M0+), which lack a hardware byte-reversal instruction (REV), swapping a 32-bit float requires 4 instructions per value. For streaming telemetry -- the most common IoT workload -- this overhead is incurred on every sample. Little-endian encoding allows constrained devices to write float and double values directly from memory to the wire with no conversion.</t>
<t>This approach only affects <xref target="IEEE754"></xref> floats and doubles. Integers use varint encoding, which is byte-order independent. The same design choice is made by Protocol Buffers <xref target="ProtocolBuffers"></xref>, which encodes fixed-width floats and doubles in little-endian regardless of platform.</t>
</section>
</section>

<section anchor="message-framing"><name>Message Framing</name>
<t>Every IOTMP message on the wire consists of a <strong>header</strong> followed by a <strong>body</strong>.</t>

<section anchor="frame-format"><name>Frame Format</name>

<artwork><![CDATA[+------------------+------------------+---------------------+
|  Message Type    |   Body Size      |       Body          |
|  (varint)        |   (varint)       |  (Body Size bytes)  |
+------------------+------------------+---------------------+
]]></artwork>

<ul spacing="compact">
<li><strong>Message Type:</strong> A varint encoding one of the defined message type values <xref target="message-types"></xref>.</li>
<li><strong>Body Size:</strong> A varint indicating the number of bytes in the body. A value of 0 indicates an empty body (used for KEEP_ALIVE).</li>
<li><strong>Body:</strong> A sequence of tagged fields <xref target="message-fields"></xref>, encoded as <tt>Body Size</tt> bytes.</li>
</ul>
</section>

<section anchor="varint-encoding"><name>Varint Encoding</name>
<t>IOTMP uses Protocol Buffers-style variable-length integer encoding:</t>

<ul spacing="compact">
<li>Each byte uses bits [6:0] for data and bit [7] as a continuation flag.</li>
<li>If bit [7] is set, more bytes follow.</li>
<li>If bit [7] is clear, this is the last byte.</li>
<li>Least significant group comes first (little-endian byte order).</li>
</ul>
<t><strong>Examples:</strong></t>
<table>
<thead>
<tr>
<th>Decimal</th>
<th>Hex</th>
<th>Varint Bytes (hex)</th>
</tr>
</thead>

<tbody>
<tr>
<td>0</td>
<td>0x00</td>
<td><tt>00</tt></td>
</tr>

<tr>
<td>1</td>
<td>0x01</td>
<td><tt>01</tt></td>
</tr>

<tr>
<td>127</td>
<td>0x7F</td>
<td><tt>7F</tt></td>
</tr>

<tr>
<td>128</td>
<td>0x80</td>
<td><tt>80 01</tt></td>
</tr>

<tr>
<td>300</td>
<td>0x012C</td>
<td><tt>AC 02</tt></td>
</tr>

<tr>
<td>16384</td>
<td>0x4000</td>
<td><tt>80 80 01</tt></td>
</tr>
</tbody>
</table><t>Implementations MUST NOT encode varints longer than 4 bytes, representing values up to 2^28 - 1 (268,435,455). A receiver that encounters a varint that does not terminate within 4 bytes MUST treat it as a decode error and close the connection.</t>
</section>

<section anchor="maximum-message-size"><name>Maximum Message Size</name>
<t>Implementations MUST support messages up to at least 32,768 bytes (32 KB). Implementations MAY support larger messages. Both sides MAY declare their maximum message size using the &quot;ms&quot; parameter: the client in the CONNECT message and the server in the OK response <xref target="connect-message"></xref>. Each side MUST NOT send messages exceeding the peer's declared maximum. If &quot;ms&quot; is not declared, the default of 32,768 bytes is assumed. A receiver that encounters a message exceeding its maximum size SHOULD close the connection.</t>
<t>IOTMP does not define message-level fragmentation. For data transfers that exceed the maximum message size, applications SHOULD use streaming <xref target="streaming"></xref>: a START_STREAM opens a persistent channel over which arbitrarily large data can be sent as a sequence of STREAM_DATA messages, each within the negotiated size limit.</t>
</section>
</section>

<section anchor="message-types"><name>Message Types</name>
<t>The Message Type field identifies the purpose of each message:</t>
<table>
<thead>
<tr>
<th>Value</th>
<th>Name</th>
<th>Direction</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td>0x00</td>
<td>RESERVED</td>
<td>--</td>
<td>Reserved for future use. MUST NOT be sent.</td>
</tr>

<tr>
<td>0x01</td>
<td>OK</td>
<td>Both</td>
<td>Successful response to a request.</td>
</tr>

<tr>
<td>0x02</td>
<td>ERROR</td>
<td>Both</td>
<td>Error response to a request.</td>
</tr>

<tr>
<td>0x03</td>
<td>CONNECT</td>
<td>Client -&gt; Server</td>
<td>Authentication request with credentials.</td>
</tr>

<tr>
<td>0x04</td>
<td>DISCONNECT</td>
<td>Both</td>
<td>Graceful connection termination.</td>
</tr>

<tr>
<td>0x05</td>
<td>KEEP_ALIVE</td>
<td>Client -&gt; Server (echo: Server -&gt; Client)</td>
<td>Connection liveness probe. Body MUST be empty.</td>
</tr>

<tr>
<td>0x06</td>
<td>RUN</td>
<td>Both</td>
<td>Execute a resource on the peer.</td>
</tr>

<tr>
<td>0x07</td>
<td>DESCRIBE</td>
<td>Both</td>
<td>Request resource metadata/API description.</td>
</tr>

<tr>
<td>0x08</td>
<td>START_STREAM</td>
<td>Both</td>
<td>Begin streaming data from a resource.</td>
</tr>

<tr>
<td>0x09</td>
<td>STOP_STREAM</td>
<td>Both</td>
<td>Stop an active stream.</td>
</tr>

<tr>
<td>0x0A</td>
<td>STREAM_DATA</td>
<td>Both</td>
<td>Carry stream payload data.</td>
</tr>
</tbody>
</table><t>Values 0x0B through 0xFF are reserved for future use.</t>

<section anchor="response-correlation"><name>Response Correlation</name>
<t>Messages that expect a response (CONNECT, RUN, DESCRIBE, START_STREAM, STOP_STREAM) MUST include a Stream ID field. The response (OK or ERROR) MUST carry the same Stream ID to correlate with the request.</t>
<t><strong>Note:</strong> The Stream ID serves two roles depending on the message type. For one-time request-response exchanges (RUN, DESCRIBE), the Stream ID is a short-lived correlation identifier that is released when the response is received. For streaming operations (START_STREAM), the same Stream ID becomes a long-lived identifier for the stream, used in subsequent STREAM_DATA and STOP_STREAM messages until the stream is terminated. In both cases, the lifecycle rules in <xref target="stream-id-partitioning"></xref> apply.</t>
<t><strong>Request Timeout:</strong> If a response (OK or ERROR) is not received within a reasonable time, the sender SHOULD consider the request failed and release the Stream ID. Implementations SHOULD use a default timeout of 30 seconds for RUN, DESCRIBE, START_STREAM, and STOP_STREAM requests. The CONNECT message has a separate timeout (RECOMMENDED: 10 seconds, <xref target="denial-of-service"></xref>). Implementations MAY use longer timeouts for specific resources that are known to require extended processing.</t>
</section>

<section anchor="stream-id-partitioning"><name>Stream ID Partitioning</name>
<t>Since IOTMP is a symmetric protocol where both sides may initiate requests concurrently, the Stream ID space is partitioned to avoid collisions:</t>

<ul spacing="compact">
<li><strong>Client-initiated requests</strong> MUST use <strong>even</strong> Stream IDs (0, 2, 4, ..., 65534).</li>
<li><strong>Server-initiated requests</strong> MUST use <strong>odd</strong> Stream IDs (1, 3, 5, ..., 65535).</li>
</ul>
<t>This partitioning ensures that responses (OK, ERROR) and stream data (STREAM_DATA) can be unambiguously correlated with the originating request, even when both sides send requests simultaneously with the same numeric value.</t>
<t><strong>Stream ID Lifecycle:</strong></t>
<t>A Stream ID is considered <strong>active</strong> from the moment the request message is sent until:</t>

<ul spacing="compact">
<li>For request-response (RUN, DESCRIBE): the corresponding OK or ERROR is received, OR</li>
<li>For streams (START_STREAM): the stream is terminated by STOP_STREAM and its OK response.</li>
</ul>
<t>An endpoint MUST NOT reuse a Stream ID that is currently active. Once a Stream ID is released, it MAY be reused for a new request. Implementations SHOULD favor low-valued Stream IDs to minimize varint encoding overhead.</t>
<t>A receiver that encounters a request with a Stream ID from the wrong partition (e.g., a client-initiated message with an odd Stream ID) MUST respond with ERROR (400) and SHOULD log the violation.</t>
</section>

<section anchor="message-direction"><name>Message Direction</name>
<t>IOTMP is a symmetric protocol. After authentication, both sides MAY send any message type except CONNECT and KEEP_ALIVE. The directional constraints are:</t>

<ul spacing="compact">
<li><strong>Client -&gt; Server only:</strong> CONNECT (authentication request), KEEP_ALIVE (liveness probe).</li>
<li><strong>Server -&gt; Client only (in response):</strong> KEEP_ALIVE echo <xref target="keepalive"></xref>.</li>
<li><strong>Both directions:</strong> OK, ERROR, DISCONNECT, RUN, DESCRIBE, START_STREAM, STOP_STREAM, STREAM_DATA.</li>
</ul>
</section>
</section>

<section anchor="message-fields"><name>Message Fields</name>
<t>The message body consists of zero or more tagged fields. Each field is encoded as:</t>

<artwork><![CDATA[+---------------------------+
|  Field Tag (1 byte)       |
+---------------------------+
|  Field Value (variable)   |
+---------------------------+
]]></artwork>

<section anchor="field-tag-encoding"><name>Field Tag Encoding</name>
<t>The field tag is a single byte combining the field number and wire type:</t>

<artwork><![CDATA[Tag = (field_number << 3) | wire_type
]]></artwork>

<ul spacing="compact">
<li><strong>field_number</strong> (bits [7:3]): Identifies which field this is (5 bits, values 0-31).</li>
<li><strong>wire_type</strong> (bits [2:0]): Determines how the field value is encoded.</li>
</ul>
</section>

<section anchor="wire-types"><name>Wire Types</name>
<table>
<thead>
<tr>
<th>Value</th>
<th>Name</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td>0x00</td>
<td>varint</td>
<td>Value is a varint-encoded unsigned integer.</td>
</tr>

<tr>
<td>0x01</td>
<td>bytes</td>
<td>Value is a varint-encoded length followed by raw bytes. The content is opaque to the protocol and MAY carry any application-defined binary format.</td>
</tr>

<tr>
<td>0x02</td>
<td>pson</td>
<td>Value is PSON encoded <xref target="data-encoding-pson"></xref>.</td>
</tr>
</tbody>
</table><t>Wire types 0x03-0x07 are reserved for future use.</t>
</section>

<section anchor="defined-fields"><name>Defined Fields</name>
<table>
<thead>
<tr>
<th>Field Number</th>
<th>Name</th>
<th>Allowed Wire Types</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td>0x01</td>
<td>STREAM_ID</td>
<td>varint</td>
<td>16-bit stream identifier for request/response correlation.</td>
</tr>

<tr>
<td>0x02</td>
<td>PARAMETERS</td>
<td>varint, pson</td>
<td>Request parameters (e.g., server operation code, stream interval).</td>
</tr>

<tr>
<td>0x03</td>
<td>PAYLOAD</td>
<td>pson, bytes</td>
<td>Primary data payload (credentials, resource data, error info).</td>
</tr>

<tr>
<td>0x04</td>
<td>RESOURCE</td>
<td>varint, pson</td>
<td>Resource identifier (string name or 16-bit hash).</td>
</tr>
</tbody>
</table><t>Field 0x00 is reserved and MUST NOT be used. Reserving field zero allows zero-initialized field variables to be distinguished from valid field numbers and serves as a sentinel value for error detection -- a convention shared with Protocol Buffers and other binary protocols. Fields 0x05-0x07 are reserved for future use. A maximum of 8 fields (0x00-0x07) are supported per message.</t>
</section>

<section anchor="field-presence"><name>Field Presence</name>
<t>Fields are OPTIONAL. A receiver MUST handle messages with any subset of fields present. The <tt>field_mask</tt> is implicit in the encoding -- a field is present if and only if its tag appears in the body. Fields MAY appear in any order within the message body. A receiver MUST NOT assume a specific field ordering.</t>
</section>

<section anchor="field-encoding-rules"><name>Field Encoding Rules</name>

<ul spacing="compact">
<li><strong>STREAM_ID:</strong> MUST be encoded as varint (wire type 0x00). Values range from 0 to 65535.</li>
<li><strong>PARAMETERS:</strong> When carrying a simple integer (e.g., a server operation code or stream interval), SHOULD be encoded as varint. When carrying structured data (e.g., CONNECT parameters), MUST be encoded as pson.</li>
<li><strong>PAYLOAD:</strong> MUST be encoded as pson. Implementations MAY use wire type bytes (0x01) to carry opaque binary data in application-defined formats.</li>
<li><strong>RESOURCE:</strong> When carrying a resource name (string), MUST be encoded as pson. When carrying a resource hash (unsigned integer), SHOULD be encoded as varint for compactness; implementations MAY also encode it as pson. Receivers MUST accept both wire types for integer values. See <xref target="resource-hashing"></xref> for resource hashing.</li>
</ul>
<t>A formal CDDL grammar defining all message structures and field requirements is provided in <xref target="formal-message-grammar-cddl"></xref>.</t>
</section>
</section>

<section anchor="data-encoding-pson"><name>Data Encoding: PSON</name>
<t>IOTMP uses PSON (Packed Sensor Object Notation) as its data encoding format. PSON is a compact, self-describing binary encoding that efficiently represents the data types commonly found in IoT applications: integers, floating-point numbers, booleans, strings, binary data, arrays, and key-value maps.</t>
<t>PSON is specified in a separate document: <xref target="PSON"></xref>. This section provides a brief summary; the PSON specification is the normative reference for encoding and decoding rules.</t>

<section anchor="summary"><name>Summary</name>
<t>Each PSON value begins with a tag byte that encodes both the wire type (3 bits) and an inline value (5 bits). Small values (0-30) are encoded in a single byte. Eight wire types are defined:</t>
<table>
<thead>
<tr>
<th>Wire Type</th>
<th>Name</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td>0</td>
<td>unsigned_t</td>
<td>Unsigned integer</td>
</tr>

<tr>
<td>1</td>
<td>signed_t</td>
<td>Negative integer (stored as absolute value)</td>
</tr>

<tr>
<td>2</td>
<td>floating_t</td>
<td>IEEE 754 float or double</td>
</tr>

<tr>
<td>3</td>
<td>discrete_t</td>
<td>Boolean (false/true) or null</td>
</tr>

<tr>
<td>4</td>
<td>string_t</td>
<td>UTF-8 string</td>
</tr>

<tr>
<td>5</td>
<td>bytes_t</td>
<td>Raw binary data</td>
</tr>

<tr>
<td>6</td>
<td>map_t</td>
<td>Key-value map (keys are strings)</td>
</tr>

<tr>
<td>7</td>
<td>array_t</td>
<td>Ordered array</td>
</tr>
</tbody>
</table><t>For the complete encoding rules, wire format, and examples, see <xref target="PSON"></xref>.</t>
</section>
</section>

<section anchor="connection-lifecycle"><name>Connection Lifecycle</name>

<section anchor="state-machine"><name>State Machine</name>

<artwork><![CDATA[                    +------------------+
                    |   DISCONNECTED   |
                    +--------+---------+
                             | Transport connect
                    +--------v---------+
                    | SOCKET_CONNECTING|
                    +--------+---------+
                             | Success
                    +--------v---------+
                    | SOCKET_CONNECTED |
                    +--------+---------+
                             | Send CONNECT
                    +--------v---------+
                    | AUTHENTICATING   |
                    +--------+---------+
                      OK /       \ ERROR
              +---------v+     +v----------+
              |AUTHENTICATED|  |AUTH_FAILED |
              +---------+-+    +------------+
                        |
                +-------v-------+
                |     READY     |<--------------+
                +-------+-------+               |
                        |                       |
              +---------v---------+             |
              |  Process Messages  |------------+
              |  Keepalive         |
              |  Check Streams     |
              +---------+---------+
                        | Transport error / DISCONNECT
               +--------v----------+
               |SOCKET_DISCONNECTED|
               +--------+----------+
                        | Exponential backoff
                        +------> (reconnect)
]]></artwork>
</section>

<section anchor="connect-message"><name>CONNECT Message</name>
<t>The client MUST send a CONNECT message as the first message after transport establishment.</t>
<t><strong>CONNECT Fields:</strong></t>
<table>
<thead>
<tr>
<th>Field</th>
<th>Content</th>
</tr>
</thead>

<tbody>
<tr>
<td>STREAM_ID</td>
<td>An even 16-bit value for response correlation (client partition, <xref target="stream-id-partitioning"></xref>).</td>
</tr>

<tr>
<td>PAYLOAD</td>
<td>Authentication data. Format depends on the authentication type (&quot;at&quot; parameter).</td>
</tr>

<tr>
<td>PARAMETERS</td>
<td>(OPTIONAL) A PSON map with connection options.</td>
</tr>
</tbody>
</table><t><strong>Connection Parameters (PARAMETERS field):</strong></t>
<table>
<thead>
<tr>
<th>Key</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>

<tbody>
<tr>
<td>&quot;v&quot;</td>
<td>uint</td>
<td>Protocol version. Current version: 1.</td>
<td>1</td>
</tr>

<tr>
<td>&quot;ka&quot;</td>
<td>uint</td>
<td>Keepalive interval in seconds. Max: 1800.</td>
<td>60</td>
</tr>

<tr>
<td>&quot;at&quot;</td>
<td>uint</td>
<td>Authentication type (see below).</td>
<td>0</td>
</tr>

<tr>
<td>&quot;ms&quot;</td>
<td>uint</td>
<td>Maximum message size in bytes the client can accept. Min: 1024.</td>
<td>32768</td>
</tr>
</tbody>
</table><t><strong>Authentication Types:</strong></t>
<t>The &quot;at&quot; parameter determines the format and interpretation of the PAYLOAD field:</t>
<table>
<thead>
<tr>
<th>Code</th>
<th>Name</th>
<th>PAYLOAD Format</th>
</tr>
</thead>

<tbody>
<tr>
<td>0</td>
<td>Credentials</td>
<td>A PSON array of 3 strings: <tt>[namespace, device_id, credential]</tt>. The <tt>namespace</tt> identifies the organizational scope (account, project, or tenant) under which the device is registered. The <tt>device_id</tt> uniquely identifies the device within the namespace. The <tt>credential</tt> is the shared secret.</td>
</tr>

<tr>
<td>1</td>
<td>Token</td>
<td>A PSON string containing a bearer token (e.g., JWT, API key, or opaque token). The server determines the device identity from the token claims or by lookup.</td>
</tr>

<tr>
<td>2</td>
<td>Certificate</td>
<td>Device identity is established via the client certificate presented during the TLS handshake (mTLS). This type MUST only be used over TLS with client certificate authentication. PAYLOAD is OPTIONAL: when the certificate identifies only the namespace (e.g., via CN or SAN), the PAYLOAD MUST be a PSON array of 2 strings <tt>[namespace, device_id]</tt> so the server can identify the specific device. When the certificate identifies both namespace and device, the PAYLOAD MAY be absent or empty. If PAYLOAD is present, the server MUST verify that the namespace matches the certificate identity and MUST reject the connection with ERROR (401) on mismatch. See <xref target="certificate-authentication-security"></xref> for security considerations on fleet certificates.</td>
</tr>
</tbody>
</table><t>Authentication types 3-255 are reserved for future use (e.g., OAuth 2.0 device flow, challenge-response). A server that receives an unrecognized authentication type MUST respond with ERROR (400).</t>
<t>If the &quot;at&quot; parameter is omitted, type 0 (Credentials) is assumed and the PAYLOAD MUST follow the Credentials format.</t>
<t><strong>Server Response:</strong></t>

<ul spacing="compact">
<li><strong>OK:</strong> Authentication succeeded. The connection enters the READY state. The Stream ID used in the CONNECT message is released upon receiving this response and MAY be reused. The server MAY include a PARAMETERS field with a PSON map containing its own connection parameters (e.g., &quot;ms&quot; to declare the server's maximum message size). The client MUST NOT send messages exceeding the server's declared maximum.</li>
<li><strong>ERROR:</strong> Authentication failed or version not supported. The Stream ID is released. The server SHOULD close the connection.</li>
</ul>
<t><strong>Extensibility of Connection Parameters:</strong></t>
<t>The PARAMETERS maps in both the CONNECT and OK messages are extensible. Implementations MUST ignore unknown keys in PARAMETERS and process known keys normally. Future versions of this protocol or application profiles MAY define additional keys for capability negotiation (e.g., supported authentication types, maximum concurrent streams, or protocol extensions). This allows the connection handshake to evolve without breaking backward compatibility.</t>
</section>

<section anchor="version-negotiation"><name>Version Negotiation</name>
<t>The &quot;v&quot; parameter in the CONNECT message indicates the protocol version the client wishes to use.</t>

<ul spacing="compact">
<li>If the &quot;v&quot; parameter is omitted, the server MUST assume protocol version 1.</li>
<li>If the server supports the requested version, it MUST proceed with authentication and respond with OK or ERROR based on the credentials.</li>
<li>If the server does not support the requested version, it MUST respond with an ERROR message with status code 400 and SHOULD include the supported versions in the PAYLOAD. The server MUST then close the connection.</li>
</ul>
<t><strong>Version Mismatch Response Example:</strong></t>

<artwork><![CDATA[ERROR Message:
  STREAM_ID:  (mirrors CONNECT)
  PARAMETERS: 400
  PAYLOAD:    {"error": "Unsupported protocol version",
               "supported": [1]}
]]></artwork>
<t>The client MAY retry the connection using a version from the supported list.</t>
</section>

<section anchor="keepalive"><name>Keepalive</name>
<t>The keepalive mechanism verifies connection liveness for both sides. KEEP_ALIVE is a client-initiated message with an empty body (Body Size = 0). The server echoes each KEEP_ALIVE back, allowing the client to confirm reachability. The server determines client liveness by monitoring incoming traffic -- any message from the client (KEEP_ALIVE, STREAM_DATA, RUN, or any other type) resets the server's inactivity timer.</t>
<t>This asymmetric design simplifies server implementation: the server does not need to maintain per-connection keepalive send timers -- it only needs a single inactivity timeout per connection. This is the same model used by <xref target="MQTT"></xref> (PINGREQ/PINGRESP) and WebSocket (Ping/Pong).</t>
<t><strong>Client behavior:</strong></t>

<ul spacing="compact">
<li>The client MUST send a KEEP_ALIVE message if no other message has been sent to the server within the negotiated keepalive interval (default: 60 seconds). If the client sends other messages (RUN, STREAM_DATA, etc.) within the interval, it MAY skip the KEEP_ALIVE for that period, since the server treats any incoming message as proof of liveness.</li>
<li>If the client does not receive any message from the server (KEEP_ALIVE echo, STREAM_DATA, OK, RUN, or any other message type) within the keepalive interval plus a tolerance margin (RECOMMENDED: 15 seconds), it SHOULD consider the connection lost and initiate reconnection <xref target="reconnection"></xref>.</li>
</ul>
<t><strong>Server behavior:</strong></t>

<ul spacing="compact">
<li>Upon receiving a KEEP_ALIVE message, the server MUST respond with a KEEP_ALIVE message (echo). This enables the client to verify that the server is still reachable.</li>
<li>The server MUST NOT initiate KEEP_ALIVE messages. The server's role is limited to echoing client-initiated KEEP_ALIVE messages.</li>
<li>The server SHOULD consider a client disconnected if no message of any type is received within the keepalive interval plus a tolerance margin (RECOMMENDED: 15 seconds). The server MUST then close the connection and release all associated resources (streams, Stream IDs).</li>
</ul>
</section>

<section anchor="disconnection"><name>Disconnection</name>
<t>Either side MAY send a DISCONNECT message to initiate graceful shutdown. Upon receiving DISCONNECT, the peer SHOULD close the transport connection. No response is expected.</t>
<t><strong>DISCONNECT Fields:</strong></t>
<table>
<thead>
<tr>
<th>Field</th>
<th>Content</th>
</tr>
</thead>

<tbody>
<tr>
<td>PARAMETERS</td>
<td>(OPTIONAL) Status code (varint).</td>
</tr>

<tr>
<td>PAYLOAD</td>
<td>(OPTIONAL) Additional information (PSON-encoded).</td>
</tr>
</tbody>
</table><t>A DISCONNECT message with no fields indicates a normal graceful shutdown.</t>
</section>

<section anchor="server-redirect"><name>Server Redirect</name>
<t>A server MAY redirect a client to a different server in two scenarios:</t>
<t><strong>At connection time:</strong> The server responds to CONNECT with an ERROR message containing a redirect status code and the target server information in the PAYLOAD.</t>
<t><strong>During an active connection:</strong> The server sends a DISCONNECT message with a redirect status code and the target server in the PAYLOAD. The client SHOULD close the current connection and connect to the indicated server.</t>
<t><strong>Redirect Status Codes:</strong></t>
<table>
<thead>
<tr>
<th>Code</th>
<th>Meaning</th>
<th>Client Behavior</th>
</tr>
</thead>

<tbody>
<tr>
<td>301</td>
<td>Moved Permanently</td>
<td>Client MUST update its stored server address and connect to the new server for all future connections.</td>
</tr>

<tr>
<td>307</td>
<td>Temporary Redirect</td>
<td>Client SHOULD connect to the indicated server now, but MUST retain the original server address for future connections.</td>
</tr>
</tbody>
</table><t><strong>Redirect Payload:</strong></t>
<t>The PAYLOAD MUST be a PSON map containing the target server information:</t>
<table>
<thead>
<tr>
<th>Key</th>
<th>Type</th>
<th>Description</th>
<th>Required</th>
</tr>
</thead>

<tbody>
<tr>
<td>&quot;host&quot;</td>
<td>string</td>
<td>Hostname or IP address of the target.</td>
<td>Yes</td>
</tr>

<tr>
<td>&quot;port&quot;</td>
<td>uint</td>
<td>Port number of the target.</td>
<td>No</td>
</tr>
</tbody>
</table><t>If &quot;port&quot; is omitted, the client SHOULD use the same port as the current connection.</t>
<t><strong>Redirect Example (at connection time):</strong></t>

<artwork><![CDATA[Client -> Server:  CONNECT [credentials]
Server -> Client:  ERROR
                    STREAM_ID:  (mirrors CONNECT)
                    PARAMETERS: 307
                    PAYLOAD:    {"host": "server2.example.com",
                                "port": 25206}
]]></artwork>
<t><strong>Redirect Example (during active connection):</strong></t>

<artwork><![CDATA[Server -> Client:  DISCONNECT
                    PARAMETERS: 301
                    PAYLOAD:    {"host": "new-server.example.com"}
]]></artwork>
</section>

<section anchor="reconnection"><name>Reconnection</name>
<t>Clients SHOULD implement automatic reconnection with exponential backoff:</t>

<ul spacing="compact">
<li>Initial delay: 5 seconds.</li>
<li>Doubling on each failed attempt.</li>
<li>Maximum delay: 60 seconds.</li>
<li>Reset to initial delay upon successful authentication.</li>
</ul>
</section>
</section>

<section anchor="resource-model"><name>Resource Model</name>

<section anchor="overview"><name>Overview</name>
<t>IOTMP models capabilities as <strong>resources</strong> -- named endpoints that can be invoked by the peer. Both clients and servers MAY expose resources. Each resource has a defined I/O type that determines how data flows through it.</t>
</section>

<section anchor="i-o-types"><name>I/O Types</name>
<table>
<thead>
<tr>
<th>Type</th>
<th>Code</th>
<th>Data Flow</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td>none</td>
<td>0</td>
<td>--</td>
<td>No callback registered.</td>
</tr>

<tr>
<td>run</td>
<td>1</td>
<td>-&gt; (trigger)</td>
<td>Action with no data input or output.</td>
</tr>

<tr>
<td>input</td>
<td>2</td>
<td>Caller -&gt; Resource</td>
<td>Receives data (actuator, setter).</td>
</tr>

<tr>
<td>output</td>
<td>3</td>
<td>Resource -&gt; Caller</td>
<td>Produces data (sensor, getter).</td>
</tr>

<tr>
<td>input_output</td>
<td>4</td>
<td>Bidirectional</td>
<td>Accepts input and produces output (property).</td>
</tr>
</tbody>
</table></section>

<section anchor="resource-invocation-run-message"><name>Resource Invocation (RUN Message)</name>
<t>Either side MAY send a RUN message to invoke a resource on the peer:</t>

<ol spacing="compact">
<li>The receiver looks up the resource by the RESOURCE field value.</li>
<li>The receiver executes the resource callback, passing PAYLOAD as input and collecting output.</li>
<li>The receiver responds with OK (including any output in PAYLOAD) or ERROR.</li>
</ol>
<t><strong>RUN Fields:</strong></t>
<table>
<thead>
<tr>
<th>Field</th>
<th>Content</th>
</tr>
</thead>

<tbody>
<tr>
<td>STREAM_ID</td>
<td>Request correlation identifier.</td>
</tr>

<tr>
<td>RESOURCE</td>
<td>Resource name (string) or resource hash (unsigned integer, <xref target="resource-hashing"></xref>).</td>
</tr>

<tr>
<td>PAYLOAD</td>
<td>(OPTIONAL) Input data for the resource.</td>
</tr>
</tbody>
</table></section>

<section anchor="resource-description-describe-message"><name>Resource Description (DESCRIBE Message)</name>
<t>The DESCRIBE message enables API introspection. Either peer can request:</t>

<ol spacing="compact">
<li><strong>Full API:</strong> DESCRIBE with no RESOURCE field -&gt; the receiving peer responds with a map of all resource names and their I/O types.</li>
<li><strong>Single Resource:</strong> DESCRIBE with a RESOURCE field -&gt; the receiving peer responds with the resource's input/output data and optional schema information.</li>
</ol>

<section anchor="description-versioning"><name>Description Versioning</name>
<t>DESCRIBE responses MUST include a <tt>&quot;v&quot;</tt> (version) field at the root level to indicate the description format version. The current version is <strong>1</strong>.</t>
<table>
<thead>
<tr>
<th>Version</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td>1</td>
<td>Resource descriptions with optional <xref target="JSON-Schema"></xref> support for input/output validation and introspection.</td>
</tr>
</tbody>
</table><t>Receivers that encounter an unrecognized version SHOULD ignore unknown fields and interpret the response on a best-effort basis. The version field enables forward-compatible evolution of the description format as the protocol evolves (e.g., adding richer metadata, new keywords, or alternative schema languages in future versions).</t>
</section>

<section anchor="full-api-description"><name>Full API Description</name>
<t>A DESCRIBE with no RESOURCE field returns a map containing the description version and a <tt>&quot;res&quot;</tt> field with all resource names and their metadata.</t>

<sourcecode type="json"><![CDATA[{
  "v": 1,
  "res": {
    "temperature": {
      "fn": 3,
      "description": "Room temperature sensor"
    },
    "led": {"fn": 2, "description": "Status LED control"},
    "relay": {"fn": 4},
    "reboot": {"fn": 1}
  }
}
]]></sourcecode>
<t>The <tt>&quot;res&quot;</tt> field is a PSON map where each key is a resource name and each value is a resource descriptor. This separation ensures that protocol-level fields (such as <tt>&quot;v&quot;</tt>) do not collide with resource names.</t>
<t>Each resource entry MUST include a <tt>&quot;fn&quot;</tt> field with the I/O type code <xref target="i-o-types"></xref>. Each resource entry MAY include a <tt>&quot;description&quot;</tt> field with a human-readable string describing the resource's purpose.</t>
</section>

<section anchor="single-resource-description"><name>Single Resource Description</name>
<t>A DESCRIBE with a RESOURCE field returns the resource's input/output information. The response contains <tt>&quot;in&quot;</tt> and/or <tt>&quot;out&quot;</tt> fields (depending on the resource's I/O type), each structured as an object with the following fields:</t>
<table>
<thead>
<tr>
<th>Field</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>value</tt></td>
<td>YES</td>
<td>Current or sample data from the resource callback.</td>
</tr>

<tr>
<td><tt>schema</tt></td>
<td>NO</td>
<td>A <xref target="JSON-Schema"></xref> object describing the expected data structure.</td>
</tr>
</tbody>
</table><t><strong>Example -- Sensor with input and output (I/O type <tt>input_output</tt>):</strong></t>

<sourcecode type="json"><![CDATA[{
  "v": 1,
  "in": {
    "value": {"brightness": 128},
    "schema": {
      "type": "object",
      "properties": {
        "brightness": {
          "type": "integer",
          "minimum": 0,
          "maximum": 255,
          "description": "LED brightness level"
        }
      }
    }
  },
  "out": {
    "value": {"celsius": 22.5, "fahrenheit": 72.5},
    "schema": {
      "type": "object",
      "properties": {
        "celsius": {
          "type": "number",
          "minimum": -40,
          "maximum": 125,
          "description": "Temperature in Celsius"
        },
        "fahrenheit": {
          "type": "number",
          "minimum": -40,
          "maximum": 257,
          "description": "Temperature in Fahrenheit"
        }
      }
    }
  }
}
]]></sourcecode>
<t><strong>Example -- Output-only sensor (I/O type <tt>output</tt>):</strong></t>

<sourcecode type="json"><![CDATA[{
  "v": 1,
  "out": {
    "value": {
      "latitude": 40.4168,
      "longitude": -3.7038,
      "altitude": 650.0
    },
    "schema": {
      "type": "object",
      "properties": {
        "latitude": {
          "type": "number",
          "minimum": -90,
          "maximum": 90
        },
        "longitude": {
          "type": "number",
          "minimum": -180,
          "maximum": 180
        },
        "altitude": {
          "type": "number",
          "description": "Altitude in meters"
        }
      }
    }
  }
}
]]></sourcecode>
<t><strong>Example -- Input-only actuator (I/O type <tt>input</tt>):</strong></t>

<sourcecode type="json"><![CDATA[{
  "v": 1,
  "in": {
    "value": {"on": false},
    "schema": {
      "type": "object",
      "properties": {
        "on": {"type": "boolean", "description": "Relay state"}
      }
    }
  }
}
]]></sourcecode>
<t><strong>Example -- Minimal description without schema:</strong></t>

<sourcecode type="json"><![CDATA[{
  "v": 1,
  "out": {
    "value": {"celsius": 22.5, "fahrenheit": 72.5}
  }
}
]]></sourcecode>
<t>The <tt>&quot;schema&quot;</tt> field, when present, MUST be a valid <xref target="JSON-Schema"></xref> object. Implementations SHOULD use the following JSON Schema keywords where applicable:</t>
<table>
<thead>
<tr>
<th>Keyword</th>
<th>Purpose</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>type</tt></td>
<td>Data type (<tt>boolean</tt>, <tt>integer</tt>, <tt>number</tt>, <tt>string</tt>, <tt>object</tt>, <tt>array</tt>).</td>
</tr>

<tr>
<td><tt>properties</tt></td>
<td>Named fields within an object.</td>
</tr>

<tr>
<td><tt>items</tt></td>
<td>Element schema for arrays.</td>
</tr>

<tr>
<td><tt>description</tt></td>
<td>Human-readable field description.</td>
</tr>

<tr>
<td><tt>minimum</tt></td>
<td>Lower bound for numeric values.</td>
</tr>

<tr>
<td><tt>maximum</tt></td>
<td>Upper bound for numeric values.</td>
</tr>

<tr>
<td><tt>enum</tt></td>
<td>Array of allowed values.</td>
</tr>

<tr>
<td><tt>readOnly</tt></td>
<td>Field cannot be written.</td>
</tr>

<tr>
<td><tt>writeOnly</tt></td>
<td>Field cannot be read.</td>
</tr>

<tr>
<td><tt>default</tt></td>
<td>Default value.</td>
</tr>

<tr>
<td><tt>required</tt></td>
<td>Array of required property names.</td>
</tr>
</tbody>
</table><t>The <tt>&quot;schema&quot;</tt> field is OPTIONAL. When absent, receivers SHOULD infer data types from the sample values in <tt>&quot;value&quot;</tt>. Resources that only have output (e.g., sensors) include only <tt>&quot;out&quot;</tt>; resources that only have input (e.g., actuators) include only <tt>&quot;in&quot;</tt>; resources with no data (type <tt>run</tt>) MAY omit both.</t>
<t>Servers MAY use the schema information to validate incoming data before forwarding it to the device, generate user interfaces automatically, or produce API documentation compatible with standards such as OpenAPI.</t>
</section>
</section>

<section anchor="stream-echo"><name>Stream Echo</name>
<t>When a resource with one or more active streams receives input via a RUN message, the resource owner (typically the client) SHOULD immediately send a STREAM_DATA message with the updated resource state on <strong>each</strong> active stream for that resource. This enables real-time synchronization -- for example, when a server sets a property value via RUN, all dashboards subscribed to that resource's stream receive the updated state without waiting for the next periodic sample.</t>
<t><strong>Behavior:</strong></t>

<ul spacing="compact">
<li>Stream echo MUST occur <strong>after</strong> the resource callback has executed and the RUN response (OK or ERROR) has been sent. The echo reflects the resource state after the operation.</li>
<li>The STREAM_DATA message MUST use the Stream ID of the active stream, not the Stream ID of the RUN request.</li>
<li>If the resource has multiple active streams (from different server requests or stream configurations), the resource owner MUST send a separate STREAM_DATA on each active stream.</li>
<li>If compact mode <xref target="compact-encoding-mode"></xref> is active on a stream, the echo STREAM_DATA MUST follow the compact encoding rules (PSON array with positional values).</li>
<li>Stream echo is enabled by default. Implementations MAY allow disabling echo on a per-resource basis if the application does not require immediate synchronization.</li>
</ul>
<t><strong>Example:</strong></t>

<artwork><![CDATA[S -> C:  START_STREAM
           (Stream 0x01, resource: "relay", interval: 5000)
C -> S:  OK (Stream 0x01)
C -> S:  STREAM_DATA (Stream 0x01, {"on": false})
           <- initial state

S -> C:  RUN (Stream 0x03, resource: "relay",
           payload: {"on": true})
C -> S:  OK (Stream 0x03)
C -> S:  STREAM_DATA (Stream 0x01, {"on": true})
           <- echo (immediate)

  ... 5 seconds later ...
C -> S:  STREAM_DATA (Stream 0x01, {"on": true})
           <- periodic sample
]]></artwork>
<t>In this example, the server has an active stream (Stream ID 0x01) observing the &quot;relay&quot; resource. When the server changes the relay state via RUN (Stream ID 0x03), the client sends the RUN response (OK) first, then immediately echoes the updated state on the active stream (Stream ID 0x01). This ensures that any dashboard or application consuming the stream sees the change in real time, without waiting for the next periodic sample.</t>
</section>

<section anchor="resource-hashing"><name>Resource Hashing</name>
<t>As an optimization for high-frequency interactions, IOTMP supports identifying resources by a numeric hash of their name instead of the full string. This eliminates the need for prior DESCRIBE exchanges or state synchronization between peers.</t>
<t>The RESOURCE field in any message (RUN, DESCRIBE, START_STREAM, STOP_STREAM) accepts both:</t>

<ul spacing="compact">
<li>A <strong>PSON string</strong>: the resource name. Always works, self-documenting. Required for resources with dynamic path parameters (e.g., paths containing <tt>/</tt>).</li>
<li>A <strong>PSON unsigned integer</strong>: a 16-bit hash of the resource name. Compact (1-3 bytes in varint), requires no prior negotiation.</li>
</ul>
<t>The receiver MUST support both forms. When a PSON unsigned integer is received, the receiver computes the hash of each of its defined resources and matches the incoming value. When a PSON string is received, the receiver looks up the resource by name.</t>
<t><strong>Hash Function:</strong></t>
<t>Implementations MUST use FNV-1a (Fowler-Noll-Vo) truncated to 16 bits:</t>

<artwork><![CDATA[hash = 0x811C9DC5          // FNV offset basis (32-bit)
for each byte b in name:
    hash = hash XOR b
    hash = hash * 0x01000193  // FNV prime (32-bit)
return hash AND 0xFFFF      // truncate to 16 bits
]]></artwork>
<t>This function is simple to implement on constrained devices and provides good distribution across typical IoT resource names.</t>
<t><strong>Reference Implementation (C):</strong></t>

<sourcecode type="c"><![CDATA[uint16_t fnv1a_16(const char *name) {
    uint32_t hash = 0x811C9DC5;
    while (*name) {
        hash ^= (uint8_t)*name++;
        hash *= 0x01000193;
    }
    return (uint16_t)(hash & 0xFFFF);
}
]]></sourcecode>
<t><strong>Test Vectors:</strong></t>
<table>
<thead>
<tr>
<th>Resource Name</th>
<th>FNV-1a (32-bit)</th>
<th>Truncated (16-bit)</th>
<th>Hex</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>&quot;temperature&quot;</tt></td>
<td>0xE9F2A935</td>
<td>0xA935</td>
<td><tt>A935</tt></td>
</tr>

<tr>
<td><tt>&quot;humidity&quot;</tt></td>
<td>0x25C5B9A0</td>
<td>0xB9A0</td>
<td><tt>B9A0</tt></td>
</tr>

<tr>
<td><tt>&quot;led&quot;</tt></td>
<td>0x406AEACA</td>
<td>0xEACA</td>
<td><tt>EACA</tt></td>
</tr>

<tr>
<td><tt>&quot;relay&quot;</tt></td>
<td>0x16B481C2</td>
<td>0x81C2</td>
<td><tt>81C2</tt></td>
</tr>

<tr>
<td><tt>&quot;reboot&quot;</tt></td>
<td>0x87729FB8</td>
<td>0x9FB8</td>
<td><tt>9FB8</tt></td>
</tr>
</tbody>
</table><t><strong>Example:</strong></t>

<artwork><![CDATA[RUN with string:   RESOURCE = "temperature"  (12 bytes in PSON)
RUN with hash:     RESOURCE = 0xA935         (3 bytes in PSON)
]]></artwork>
<t>Both identify the same resource. The hash form saves 9 bytes per message.</t>
<t><strong>Applicability:</strong></t>
<t>Resource hashing is best suited for simple resource names without dynamic parameters. Resources with path-based parameters (e.g., <tt>fs/home/config.txt</tt>) SHOULD use the string form, since the path contains dynamic segments that cannot be pre-hashed.</t>
<t><strong>Collision Handling:</strong></t>
<t>With a 16-bit hash space (65,536 values), the probability of at least one collision follows the birthday problem. For typical IoT devices with small resource sets, the probability is very low:</t>
<table>
<thead>
<tr>
<th>Resources</th>
<th>P(collision)</th>
</tr>
</thead>

<tbody>
<tr>
<td>10</td>
<td>&lt; 0.1%</td>
</tr>

<tr>
<td>20</td>
<td><sub>0.3%</sub></td>
</tr>

<tr>
<td>30</td>
<td><sub>0.7%</sub></td>
</tr>

<tr>
<td>50</td>
<td><sub>1.9%</sub></td>
</tr>

<tr>
<td>100</td>
<td><sub>7.3%</sub></td>
</tr>
</tbody>
</table><t>For constrained devices with fewer than 30 resources, hash-based identification is safe for practical purposes. Implementations MUST verify at device startup that no two resource names produce the same hash. If a collision is detected, the implementation MUST fall back to string-based identification for the affected resources and SHOULD emit a diagnostic warning (e.g., via log output or assertion). If resources are added dynamically at runtime, the implementation MUST check for hash collisions against all existing resources before enabling hash-based identification for the new resource.</t>
<t><strong>Unmatched Hash Handling:</strong></t>
<t>If a receiver receives a PSON unsigned integer in the RESOURCE field and no defined resource matches the hash value, the receiver MUST respond with ERROR (404). This is consistent with the behavior for unrecognized string resource names <xref target="connection-lifecycle-conformance"></xref>.</t>
</section>
</section>

<section anchor="streaming"><name>Streaming</name>

<section anchor="stream-lifecycle"><name>Stream Lifecycle</name>

<section anchor="stream-state-machine"><name>Stream State Machine</name>
<t>Each stream is identified by a Stream ID and transitions through the following states:</t>

<artwork><![CDATA[                     +-----------+
                     |   IDLE    |
                     +-----+-----+
                           | Send/Receive START_STREAM
                     +-----v-----+
                     |  OPENING  |
                     +-----+-----+
               OK /         \ ERROR / Timeout
         +-------v---+   +---v---------+
         |  ACTIVE   |   |  FAILED     |
         +-------+---+   +---+---------+
                 |            | Stream ID released
                 |            +------> (IDLE)
                 |
                 | Send/Receive STOP_STREAM
            +----v-----+
            |  CLOSING  |
            +----+-----+
                 | OK / ERROR / Timeout
            +----v-----+
            |   IDLE   | Stream ID released
            +----------+
]]></artwork>
<t><strong>State Definitions:</strong></t>
<table>
<thead>
<tr>
<th>State</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td>IDLE</td>
<td>No active stream for this Stream ID. The ID is available for reuse.</td>
</tr>

<tr>
<td>OPENING</td>
<td>START_STREAM has been sent; awaiting OK or ERROR response.</td>
</tr>

<tr>
<td>ACTIVE</td>
<td>Stream is established. STREAM_DATA messages are being sent by the resource owner.</td>
</tr>

<tr>
<td>CLOSING</td>
<td>STOP_STREAM has been sent; awaiting OK or ERROR response.</td>
</tr>

<tr>
<td>FAILED</td>
<td>START_STREAM was rejected (ERROR) or timed out. Stream ID is released.</td>
</tr>
</tbody>
</table><t><strong>Transition Rules:</strong></t>

<ul spacing="compact">
<li><strong>IDLE -&gt; OPENING:</strong> A START_STREAM message is sent or received. The Stream ID is now active and MUST NOT be reused until released.</li>
<li><strong>OPENING -&gt; ACTIVE:</strong> An OK response is received for the START_STREAM. The resource owner MUST send an initial STREAM_DATA immediately <xref target="start-stream-message"></xref>.</li>
<li><strong>OPENING -&gt; FAILED -&gt; IDLE:</strong> An ERROR response is received, or the request times out <xref target="response-correlation"></xref>. The Stream ID is released.</li>
<li><strong>ACTIVE -&gt; CLOSING (initiator):</strong> The stream initiator sends a STOP_STREAM to terminate the stream.</li>
<li><strong>ACTIVE -&gt; CLOSING (resource owner):</strong> The resource owner MAY also send a STOP_STREAM with the stream's Stream ID to terminate a stream it did not initiate. This allows the resource owner to signal that the resource is no longer available (e.g., a terminal session has ended, a sensor has failed, or the device is running low on resources). The stream initiator MUST respond with OK and release the Stream ID.</li>
<li><strong>CLOSING -&gt; IDLE:</strong> An OK or ERROR response is received for the STOP_STREAM request. The Stream ID is released. If no response is received within the request timeout, the stream is considered closed and the Stream ID is released.</li>
<li><strong>Any state -&gt; IDLE (transport loss):</strong> If the transport connection is lost, all streams are immediately terminated and all Stream IDs are released <xref target="connection-lifecycle-conformance"></xref>.</li>
</ul>
<t><strong>Edge Cases:</strong></t>
<table>
<thead>
<tr>
<th>Condition</th>
<th>Required Behavior</th>
</tr>
</thead>

<tbody>
<tr>
<td>STREAM_DATA received while in OPENING state</td>
<td>Receiver SHOULD buffer or discard; sender MUST NOT send STREAM_DATA before receiving OK.</td>
</tr>

<tr>
<td>START_STREAM received for an already ACTIVE Stream ID</td>
<td>Receiver MUST respond with ERROR (409 Conflict).</td>
</tr>

<tr>
<td>STOP_STREAM received for a Stream ID not in ACTIVE state</td>
<td>Receiver MUST respond with ERROR (409 Conflict).</td>
</tr>

<tr>
<td>STOP_STREAM rejected with ERROR</td>
<td>Initiator MUST still consider the stream terminated and release the Stream ID.</td>
</tr>

<tr>
<td>RUN received targeting a resource with an ACTIVE stream</td>
<td>Resource executes normally. If stream echo is enabled <xref target="stream-echo"></xref>, the client MUST send a STREAM_DATA with the updated state on all ACTIVE streams for that resource.</td>
</tr>
</tbody>
</table></section>

<section anchor="interaction-diagram"><name>Interaction Diagram</name>

<artwork><![CDATA[Server                              Client
  |    START_STREAM [resource, interval]  |
  |-------------------------------------->|
  |              OK                       |
  |<--------------------------------------|
  |         STREAM_DATA [payload]         |
  |<--------------------------------------|  (initial state)
  |         STREAM_DATA [payload]         |
  |<--------------------------------------|  (after interval)
  |              ...                      |
  |         STOP_STREAM                   |
  |-------------------------------------->|
  |              OK                       |
  |<--------------------------------------|
]]></artwork>
</section>
</section>

<section anchor="start-stream-message"><name>START_STREAM Message</name>
<table>
<thead>
<tr>
<th>Field</th>
<th>Content</th>
</tr>
</thead>

<tbody>
<tr>
<td>STREAM_ID</td>
<td>Identifies this stream. Used in subsequent STREAM_DATA/STOP.</td>
</tr>

<tr>
<td>RESOURCE</td>
<td>Name of the resource to stream.</td>
</tr>

<tr>
<td>PARAMETERS</td>
<td>(OPTIONAL) Stream configuration. See below.</td>
</tr>
</tbody>
</table><t><strong>PARAMETERS Encoding:</strong></t>
<t>When PARAMETERS is a simple unsigned integer (varint), it represents the stream interval in milliseconds. A value of 0 indicates event-driven streaming (no periodic sampling).</t>
<t>When PARAMETERS is a PSON map, it MAY contain the following fields:</t>
<table>
<thead>
<tr>
<th>Key</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>

<tbody>
<tr>
<td>&quot;i&quot;</td>
<td>uint</td>
<td>Stream interval in milliseconds. 0 = event-driven.</td>
<td>0</td>
</tr>

<tr>
<td>&quot;cm&quot;</td>
<td>bool</td>
<td>Compact mode. Request compact encoding for STREAM_DATA.</td>
<td>false</td>
</tr>
</tbody>
</table><t>For backward compatibility, a server that only needs to set the interval MAY send PARAMETERS as a plain varint. A client MUST accept both encodings.</t>
<t>Upon receiving START_STREAM:</t>

<ol spacing="compact">
<li>The client registers the stream (Stream ID -&gt; resource + interval + encoding mode).</li>
<li>The client evaluates whether the resource supports compact mode (see <xref target="compact-encoding-mode"></xref>).</li>
<li>The client responds with OK. If compact mode was activated, the OK MUST include a PARAMETERS field as a PSON map with <tt>&quot;cm&quot;: true</tt>. If compact mode is not active, the <tt>&quot;cm&quot;</tt> field SHOULD be omitted.</li>
<li>The client immediately sends one STREAM_DATA with the current resource state.</li>
<li>If an interval is specified, the client sends STREAM_DATA at that interval.</li>
</ol>
</section>

<section anchor="stream-data-message"><name>STREAM_DATA Message</name>
<table>
<thead>
<tr>
<th>Field</th>
<th>Content</th>
</tr>
</thead>

<tbody>
<tr>
<td>STREAM_ID</td>
<td>The stream identifier from START_STREAM.</td>
</tr>

<tr>
<td>PAYLOAD</td>
<td>The resource output data, PSON-encoded.</td>
</tr>
</tbody>
</table><t>STREAM_DATA messages do NOT require a response.</t>
</section>

<section anchor="compact-encoding-mode"><name>Compact Encoding Mode</name>
<t>Compact encoding mode reduces per-message overhead for resources that consistently return PSON maps with the same set of keys. It eliminates the repetition of string keys in every STREAM_DATA message after the first.</t>

<section anchor="negotiation"><name>Negotiation</name>

<ol spacing="compact">
<li>The server requests compact mode by setting <tt>&quot;cm&quot;: true</tt> in the START_STREAM PARAMETERS. If <tt>&quot;cm&quot;</tt> is absent or false, the server does not want compact mode and the client MUST NOT activate it.</li>
<li>The client evaluates whether the resource output is suitable for compaction (i.e., it produces a PSON map with a stable set of keys).</li>
<li>If the server requested compact mode and the resource supports it, the client activates it for this stream and confirms by including <tt>&quot;cm&quot;: true</tt> in the OK response PARAMETERS. If the resource does not support compact mode, the client omits <tt>&quot;cm&quot;</tt> from the OK response and sends all STREAM_DATA messages as full PSON maps.</li>
<li>The server MUST check the OK response for <tt>&quot;cm&quot;: true</tt> before assuming compact encoding is active. If <tt>&quot;cm&quot;</tt> is absent from the OK response, the server MUST decode all STREAM_DATA messages as standard PSON values.</li>
</ol>
</section>

<section anchor="schema-establishment"><name>Schema Establishment</name>
<t>The <strong>first STREAM_DATA</strong> message on a compact stream MUST be a full PSON map with string keys. This message establishes the <strong>key order</strong> (schema) for all subsequent messages on this stream.</t>

<artwork><![CDATA[First STREAM_DATA:
  PAYLOAD: {"temperature": 23.5, "humidity": 60}
           (PSON map -- schema)
]]></artwork>
<t>Both the client and server MUST store the key order from this first map. The key order is determined by the iteration order of the PSON map fields.</t>
</section>

<section anchor="compact-stream-data"><name>Compact STREAM_DATA</name>
<t>All subsequent STREAM_DATA messages on this stream MUST encode the PAYLOAD as a <strong>PSON array</strong> with values in the same positional order as the keys in the schema message.</t>

<artwork><![CDATA[Subsequent STREAM_DATA:
  PAYLOAD: [23.6, 61]
           (PSON array -- values only)
]]></artwork>
<t>The server reconstructs the full map by matching each array position to the corresponding key from the schema.</t>
</section>

<section anchor="rules"><name>Rules</name>

<ul spacing="compact">
<li>The resource owner MUST NOT change the structure of the output (add, remove, or reorder keys at any nesting level) during the lifetime of a compact stream. If the output structure changes, the resource owner MUST terminate the stream with STOP_STREAM. The stream initiator MAY then open a new stream to renegotiate the schema.</li>
<li>If a value is absent or unavailable for a given key, the client MUST encode <tt>null</tt> at that position to maintain alignment.</li>
<li>The schema is valid for the lifetime of the stream. It is reset only when the stream is stopped (STOP_STREAM) and a new stream is started (START_STREAM).</li>
<li>If the resource output is not a PSON map (e.g., a single number, boolean, string, or binary), compact mode has no effect. The client SHOULD send the value as-is.</li>
<li>The PSON array MUST contain exactly the same number of elements as keys in the schema. If the array length does not match the schema length, the receiver SHOULD treat the STREAM_DATA as malformed and MAY close the stream with STOP_STREAM.</li>
</ul>
</section>

<section anchor="recursive-compaction"><name>Recursive Compaction</name>
<t>Compact mode applies recursively to nested PSON maps. When the schema message contains a map value nested within the top-level map, that nested map is also converted to a positional array in subsequent compact messages. This process applies at all nesting levels:</t>

<ul spacing="compact">
<li>A value that is a PSON map in the schema message becomes a PSON array in compact messages, with its own positional order derived from the schema.</li>
<li>A value that is a scalar (number, boolean, string, null) or a PSON array remains encoded as-is.</li>
<li>The <tt>null</tt> placeholder rule applies at every nesting level for absent values.</li>
</ul>
<t><strong>Distinguishing arrays from compacted maps:</strong></t>
<t>The receiver MUST store the type of each value from the schema message (the first STREAM_DATA). In subsequent compact messages, when the receiver encounters a PSON array at a given position, it checks the schema to determine the original type:</t>

<ul spacing="compact">
<li>If the schema value at that position was a <strong>PSON map</strong>, the array is a compacted map and MUST be expanded using the stored key order.</li>
<li>If the schema value at that position was a <strong>PSON array</strong>, the array is a real array and MUST be kept as-is. Real arrays MAY change in length between messages -- they are not subject to compaction or positional alignment.</li>
</ul>
<t>This distinction is unambiguous because the schema message provides the complete type information for every value at every nesting level.</t>
<t><strong>Example -- Mixed maps and arrays:</strong></t>

<artwork><![CDATA[Schema message (first STREAM_DATA):
  PAYLOAD: {"temperature": 23.5,
            "tags": ["indoor", "sensor"],
            "location": {"lat": 40.4168, "lon": -3.7038}}

Compact messages (subsequent STREAM_DATA):
  PAYLOAD: [23.6, ["indoor", "active", "new"], [40.4200, -3.7035]]
]]></artwork>
<t>The receiver reconstructs the full structure using the schema:</t>

<ul spacing="compact">
<li>Position 0: &quot;temperature&quot; -- scalar in schema, value is 23.6 (kept as-is).</li>
<li>Position 1: &quot;tags&quot; -- array in schema, value is <tt>[&quot;indoor&quot;, &quot;active&quot;, &quot;new&quot;]</tt> (kept as-is, length may vary).</li>
<li>Position 2: &quot;location&quot; -- map in schema, value is <tt>[40.4200, -3.7035]</tt> (expanded to <tt>{&quot;lat&quot;: 40.4200, &quot;lon&quot;: -3.7035}</tt> using stored key order).</li>
</ul>
</section>

<section anchor="example"><name>Example</name>

<artwork><![CDATA[Server -> Client: START_STREAM
  STREAM_ID: 0x00A1
  RESOURCE:  "environment"
  PARAMETERS: {"i": 5000, "cm": true}

Client -> Server: OK
  STREAM_ID: 0x00A1
  PARAMETERS: {"cm": true}

Client -> Server: STREAM_DATA (schema message)
  STREAM_ID: 0x00A1
  PAYLOAD:   {"temperature": 23.5, "humidity": 60, "pressure": 1013}
             (PSON map: 38 bytes)

Client -> Server: STREAM_DATA (compact)
  STREAM_ID: 0x00A1
  PAYLOAD:   [23.6, 61, 1013]
             (PSON array: 10 bytes -- 74% smaller)

Client -> Server: STREAM_DATA (compact)
  STREAM_ID: 0x00A1
  PAYLOAD:   [23.7, 62, 1014]
             (PSON array: 10 bytes)
]]></artwork>
</section>

<section anchor="wire-efficiency"><name>Wire Efficiency</name>
<t>Compact mode is most effective when:</t>

<ul spacing="compact">
<li>The resource returns a map with many keys (more string bytes eliminated per sample).</li>
<li>The stream has many samples (the schema overhead is amortized over all samples).</li>
<li>Keys are long (e.g., &quot;temperature&quot;, &quot;humidity&quot; vs. short keys like &quot;t&quot;, &quot;h&quot;).</li>
</ul>
<table>
<thead>
<tr>
<th>Scenario</th>
<th>Normal (bytes/sample)</th>
<th>Compact (bytes/sample)</th>
<th>Savings</th>
</tr>
</thead>

<tbody>
<tr>
<td>2 sensors (temp + humidity)</td>
<td>35</td>
<td>14</td>
<td>60%</td>
</tr>

<tr>
<td>8 sensors (environmental station)</td>
<td>83</td>
<td>27</td>
<td>67%</td>
</tr>
</tbody>
</table><t>For 100 samples of 2 sensors:</t>
<table>
<thead>
<tr>
<th>Mode</th>
<th>Total bytes</th>
<th>vs MQTT 3.1.1</th>
<th>vs MQTT v5 (alias)</th>
</tr>
</thead>

<tbody>
<tr>
<td>Normal</td>
<td><sub>3,816</sub></td>
<td>40% savings</td>
<td>11% savings</td>
</tr>

<tr>
<td>Compact</td>
<td><sub>1,421</sub></td>
<td>78% savings</td>
<td>67% savings</td>
</tr>
</tbody>
</table></section>

<section anchor="related-work"><name>Related Work</name>
<t>The problem of eliminating repeated keys in serialized data has been addressed by several existing standards and technologies. IOTMP's compact mode differs from these approaches in how schema negotiation is integrated into the streaming lifecycle.</t>
<t><strong>CBOR Packed</strong> <xref target="RFC9543"></xref> defines a mechanism for reducing repetition in CBOR by using shared value tables. Frequently occurring strings or values are stored in a table and referenced by index. This is a general-purpose approach that works on any CBOR data but requires an explicit packing/unpacking step and does not integrate with a streaming protocol.</t>
<t><strong>SenML</strong> <xref target="RFC8428"></xref> is a data format for sensor measurements that reduces repetition through base values and delta encoding (e.g., a base name shared across multiple records). SenML is widely used in CoAP and LwM2M but operates at the data model level, not the transport level, and each record still carries field identifiers.</t>
<t><strong>Protocol Buffers</strong> <xref target="ProtocolBuffers"></xref> and Apache Avro use pre-shared schema files (.proto, .avsc) to eliminate field names entirely from the wire format. This achieves excellent compactness but requires the schema to be agreed upon at build time, making it unsuitable for dynamic IoT resources whose structure may vary across firmware versions or device types.</t>
<t><strong>MQTT Sparkplug</strong> defines a metric registration mechanism over MQTT where metrics are registered with numeric aliases on first publish, and subsequent messages use aliases instead of full metric names. This is the closest existing approach to IOTMP's compact mode.</t>
<t>IOTMP's compact mode combines dynamic schema establishment (from the first STREAM_DATA message), recursive compaction (nested maps become nested arrays), and protocol-integrated negotiation (via START_STREAM parameters) into a single mechanism that requires no pre-shared schema files, no external registries, and no additional protocol messages beyond the standard streaming lifecycle.</t>
</section>
</section>

<section anchor="stop-stream-message"><name>STOP_STREAM Message</name>
<table>
<thead>
<tr>
<th>Field</th>
<th>Content</th>
</tr>
</thead>

<tbody>
<tr>
<td>STREAM_ID</td>
<td>The stream identifier to stop.</td>
</tr>
</tbody>
</table><t>Either side MAY send STOP_STREAM to terminate an active stream. The stream initiator sends STOP_STREAM when it no longer needs the data. The resource owner sends STOP_STREAM when the resource is no longer available (e.g., a terminal session has ended or the device cannot sustain the stream).</t>
<t>Upon receiving STOP_STREAM:</t>

<ol spacing="compact">
<li>The receiver removes the stream registration and compact schema state (if any).</li>
<li>The receiver clears the stream ID on the resource.</li>
<li>The receiver responds with OK.</li>
</ol>
</section>

<section anchor="multiple-concurrent-streams"><name>Multiple Concurrent Streams</name>
<t>A client MUST support multiple simultaneous streams. Different resources MAY have different stream intervals and encoding modes (normal or compact). The Stream ID space is partitioned between client and server <xref target="stream-id-partitioning"></xref>, providing 32,768 IDs per side -- ample for concurrent streams on a single connection.</t>
</section>

<section anchor="flow-control"><name>Flow Control</name>
<t>IOTMP does not define an application-level flow control mechanism (such as credit-based windows). Flow control is delegated to the transport layer: TCP's built-in backpressure naturally throttles the sender when the receiver cannot consume data fast enough.</t>
<t>If a device cannot accept a new stream due to resource constraints (memory, CPU, or maximum stream limit), it MUST respond to the START_STREAM request with ERROR (429). Similarly, if a device cannot execute a RUN request due to overload, it MAY respond with ERROR (429).</t>
<t>The server controls the data rate of each stream through the interval parameter in START_STREAM. Implementations SHOULD use appropriate intervals to avoid overwhelming constrained devices.</t>
<t>IOTMP does not define application-level delivery guarantees (such as QoS levels). Request-response exchanges (RUN, DESCRIBE, START_STREAM, STOP_STREAM) provide implicit delivery confirmation through OK/ERROR responses. Streaming data (STREAM_DATA) is ephemeral by nature -- each sample supersedes the previous one -- so retransmission of individual samples is unnecessary. Connection-level reliability is delegated to the transport layer (TCP/TLS).</t>
</section>
</section>

<section anchor="extensibility"><name>Extensibility</name>

<section anchor="symmetric-resource-model"><name>Symmetric Resource Model</name>
<t>IOTMP is a symmetric protocol: both clients and servers MAY expose resources. After authentication, either side can invoke resources on the peer using RUN, DESCRIBE, START_STREAM, and STOP_STREAM messages. The resource namespace is defined by each implementation.</t>
<t>This symmetry enables diverse use cases without protocol-level changes:</t>

<ul spacing="compact">
<li>A client can expose sensor resources that the server reads or streams.</li>
<li>A server can expose storage, notification, or inter-device communication resources that clients invoke via RUN.</li>
<li>Both sides discover available resources through DESCRIBE.</li>
</ul>
</section>

<section anchor="application-profiles"><name>Application Profiles</name>
<t>The specific resources exposed by a server or client are outside the scope of this specification. Implementations MAY define application profiles that specify well-known resource names, their expected I/O types, and PAYLOAD formats.</t>
</section>

<section anchor="bidirectional-stream-channels"><name>Bidirectional Stream Channels</name>
<t>While <xref target="streaming"></xref> describes streaming primarily in the context of periodic telemetry (resource owner sends STREAM_DATA at regular intervals), IOTMP streams are general-purpose bidirectional data channels. Once a stream is active, <strong>both sides</strong> MAY send STREAM_DATA messages on the same Stream ID simultaneously. This enables a wide range of application patterns beyond periodic sampling, using only existing protocol primitives.</t>
<t>This bidirectional capability has been validated in production implementations supporting remote terminals, file transfers of multiple gigabytes, TCP proxy tunneling, and firmware updates -- all over the same IOTMP connection used for sensor telemetry, with no protocol modifications.</t>

<section anchor="channel-establishment"><name>Channel Establishment</name>
<t>All bidirectional stream use cases follow the same lifecycle:</t>

<ol spacing="compact">
<li>The stream initiator sends START_STREAM with a resource name and application-specific parameters.</li>
<li>The resource owner responds with OK (optionally confirming negotiated parameters).</li>
<li>Both sides exchange STREAM_DATA messages on the Stream ID for the duration of the session.</li>
<li>Either side terminates the channel with STOP_STREAM.</li>
</ol>

<artwork><![CDATA[Initiator                                Resource Owner
  |  START_STREAM (resource, params)           |
  |------------------------------------------->|
  |  OK (negotiated params)                    |
  |<-------------------------------------------|
  |  STREAM_DATA <--------------------------   |  (both directions,
  |  ----------------------------> STREAM_DATA |  concurrent, on
  |  STREAM_DATA <--------------------------   |  the same Stream ID)
  |  ----------------------------> STREAM_DATA |
  |  ...                                       |
  |  STOP_STREAM                               |
  |------------------------------------------->|
  |  OK                                        |
  |<-------------------------------------------|
]]></artwork>
<t>The content and semantics of the STREAM_DATA messages are defined by the application profile for each resource. The protocol layer treats them identically regardless of the use case.</t>
</section>

<section anchor="use-case-interactive-sessions-terminal-proxy"><name>Use Case: Interactive Sessions (Terminal, Proxy)</name>
<t>For interactive sessions, STREAM_DATA carries raw binary data in both directions simultaneously. This includes:</t>

<ul spacing="compact">
<li><strong>Remote terminal:</strong> A PTY-backed shell session where user input flows as binary STREAM_DATA from initiator to resource owner, and terminal output flows back. START_STREAM parameters specify initial terminal dimensions (columns, rows).</li>
<li><strong>TCP proxy tunneling:</strong> A transparent relay where STREAM_DATA carries raw TCP bytes between the initiator and a local TCP endpoint on the device (e.g., a router admin panel, PLC interface, or database port). START_STREAM parameters specify the target host and port.</li>
</ul>
<t>In both cases, data flows continuously in both directions with no application-level acknowledgment -- TCP backpressure provides sufficient flow control for interactive traffic.</t>

<artwork><![CDATA[Initiator                                Resource Owner
  |  START_STREAM ("terminal", {"cols":80})    |
  |------------------------------------------->|  opens PTY
  |  OK                                        |
  |<-------------------------------------------|
  |  STREAM_DATA ("ls -la\n")                  |  user input
  |------------------------------------------->|
  |  STREAM_DATA ("total 42\ndrwx...")         |  terminal output
  |<-------------------------------------------|
  |  ...                                       |
]]></artwork>
<t><strong>Updating stream parameters at runtime:</strong></t>
<t>Some interactive sessions require runtime parameter updates without interrupting the data flow (e.g., resizing a terminal window). Since START_STREAM cannot be re-sent on an active Stream ID, the recommended application-level pattern is to expose a companion resource that accepts parameter updates via RUN:</t>

<artwork><![CDATA[Initiator                                Resource Owner
  |  START_STREAM ("terminal/main")          |
  |------------------------------------------->|  opens PTY (80x24)
  |  OK                                        |
  |<-------------------------------------------|
  |  STREAM_DATA <---------------------------> |  (bidirectional data)
  |  ...                                       |
  |  RUN ("terminal/main/params",              |
  |       {"size":{"cols":120,"rows":40}})     |  resize request
  |------------------------------------------->|  ioctl(TIOCSWINSZ)
  |  OK                                        |
  |<-------------------------------------------|
  |  STREAM_DATA <---------------------------> |  (stream continues)
]]></artwork>
<t>The companion resource (e.g., <tt>terminal/main/params</tt>) is a regular IOTMP resource with I/O type <tt>input</tt> that updates the session configuration. This pattern keeps the protocol simple -- no new message types or stream renegotiation mechanisms are needed -- while allowing applications to define whatever runtime parameters they require.</t>
</section>

<section anchor="use-case-bulk-data-transfer-files-firmware"><name>Use Case: Bulk Data Transfer (Files, Firmware)</name>
<t>For large data transfers (files, firmware images, logs), one side sends data chunks as binary STREAM_DATA and the other side sends acknowledgments as PSON maps in the opposite direction. This provides application-level flow control suitable for multi-gigabyte transfers.</t>
<t><strong>Data flow:</strong></t>

<ul spacing="compact">
<li>The <strong>sender</strong> transmits data as a sequence of STREAM_DATA messages, each carrying a PSON binary value within the negotiated message size limit.</li>
<li>The <strong>receiver</strong> periodically sends ACK messages as STREAM_DATA in the reverse direction. Each ACK is a PSON map: <tt>{&quot;ack&quot;: &lt;sequence_number&gt;, &quot;bytes&quot;: &lt;confirmed_bytes&gt;}</tt>.</li>
<li>The stream initiator sends STOP_STREAM when the transfer is complete.</li>
</ul>
<t><strong>Application-level flow control:</strong></t>
<t>The recommended approach uses a byte-based sliding window:</t>

<ul spacing="compact">
<li>The stream initiator specifies a <tt>window_size</tt> in the START_STREAM parameters -- the maximum number of unacknowledged bytes allowed in flight.</li>
<li>The sender tracks <tt>bytes_in_flight</tt> (bytes sent minus bytes acknowledged). When <tt>bytes_in_flight</tt> reaches <tt>window_size</tt>, the sender pauses until an ACK is received.</li>
<li>The ACK frequency is adaptive: implementations SHOULD send ACKs approximately every 1% of total transfer size, bounded between one chunk size and 1 MB.</li>
</ul>
<t><strong>Recommended defaults:</strong></t>
<table>
<thead>
<tr>
<th>Parameter</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>chunk_size</tt></td>
<td>64 KB</td>
<td>Size of each binary STREAM_DATA payload. Confirmed by resource owner in OK response.</td>
</tr>

<tr>
<td><tt>window_size</tt></td>
<td>2 MB</td>
<td>Maximum unacknowledged bytes in flight.</td>
</tr>

<tr>
<td><tt>ack_timeout</tt></td>
<td>10 s</td>
<td>Timeout waiting for an ACK before aborting the transfer.</td>
</tr>
</tbody>
</table><t><strong>Bandwidth limiting:</strong> Implementations MAY limit transfer rate by introducing controlled delays between chunks (sender-side) or between ACKs (receiver-side). Delaying ACKs naturally throttles the sender through the flow control window.</t>
<t><strong>Small data optimization:</strong> For data that fits within a single message (less than or equal to the negotiated maximum message size), implementations SHOULD use an inline RUN request/response instead of opening a stream, avoiding the START_STREAM/STOP_STREAM overhead.</t>
</section>

<section anchor="use-case-command-execution"><name>Use Case: Command Execution</name>
<t>For operations that are self-contained (short-lived, bounded output), a simple RUN request/response is sufficient. The client exposes a resource that accepts a command as input and returns the result (stdout, stderr, exit code) in the OK payload. No streaming is needed.</t>

<artwork><![CDATA[S -> C:  RUN ("cmd",
           {"exec":"uname","args":["-a"],"timeout":10})
C -> S:  OK  ({"stdout": "Linux device 5.15.0 ...",
           "stderr": "", "exit_code": 0})
]]></artwork>
</section>

<section anchor="design-principle"><name>Design Principle</name>
<t>These use cases illustrate a key IOTMP design property: the protocol provides a small set of general-purpose primitives (RUN for one-shot operations, START_STREAM/STREAM_DATA for persistent channels) that application profiles combine to address diverse requirements. No protocol-level changes are needed to support new use cases -- only the definition of new resource names and their data formats.</t>
</section>
</section>

<section anchor="example-client-invoking-a-server-resource"><name>Example: Client Invoking a Server Resource</name>

<artwork><![CDATA[RUN Message:
  STREAM_ID:  0x1234
  RESOURCE:   "storage/sensor_data" (PSON string)
  PAYLOAD:    {"temperature": 25.3, "humidity": 60} (PSON map)

Response:
  OK
  STREAM_ID: 0x1234
]]></artwork>
</section>

<section anchor="example-server-invoking-a-client-resource"><name>Example: Server Invoking a Client Resource</name>

<artwork><![CDATA[RUN Message:
  STREAM_ID:  0x5679
  RESOURCE:   "led" (PSON string)
  PAYLOAD:    true (PSON boolean)

Response:
  OK
  STREAM_ID: 0x5679
]]></artwork>
</section>
</section>

<section anchor="error-handling"><name>Error Handling</name>

<section anchor="error-message"><name>ERROR Message</name>
<t>An ERROR message is sent in response to a failed request. It carries the same Stream ID as the request.</t>
<table>
<thead>
<tr>
<th>Field</th>
<th>Content</th>
</tr>
</thead>

<tbody>
<tr>
<td>STREAM_ID</td>
<td>Mirrors the request Stream ID.</td>
</tr>

<tr>
<td>PARAMETERS</td>
<td>(OPTIONAL) Status code (varint).</td>
</tr>

<tr>
<td>PAYLOAD</td>
<td>(OPTIONAL) Error details (PSON-encoded).</td>
</tr>
</tbody>
</table></section>

<section anchor="status-codes"><name>Status Codes</name>
<t>The PARAMETERS field in OK and ERROR messages MAY carry a numeric status code. Implementations SHOULD use HTTP status codes as defined in <xref target="RFC9110"></xref>.</t>
<t><strong>Rationale:</strong> IOTMP reuses HTTP status codes rather than defining a separate code space for two reasons. First, IOTMP's resource-oriented model (named resources with read/write/invoke operations) maps naturally to HTTP semantics, making HTTP status codes directly applicable: a missing resource is 404, a malformed request is 400, a timeout is 408. Second, IOTMP is designed to support transparent HTTP bridging, where an HTTP gateway translates between HTTP requests and IOTMP messages. Reusing HTTP status codes enables this bridge to pass status codes through without a translation table, preserving the original error semantics end-to-end.</t>
<t>If the PARAMETERS field is absent or zero, a generic success (for OK) or generic error (for ERROR) is assumed.</t>
<t>The following status codes are commonly used:</t>
<table>
<thead>
<tr>
<th>Code</th>
<th>Name</th>
<th>Usage</th>
</tr>
</thead>

<tbody>
<tr>
<td>200</td>
<td>OK</td>
<td>Successful operation (implicit if omitted in OK).</td>
</tr>

<tr>
<td>301</td>
<td>Moved Permanently</td>
<td>Server redirect: client must update stored address <xref target="server-redirect"></xref>.</td>
</tr>

<tr>
<td>307</td>
<td>Temporary Redirect</td>
<td>Server redirect: client should connect to indicated server <xref target="server-redirect"></xref>.</td>
</tr>

<tr>
<td>400</td>
<td>Bad Request</td>
<td>Malformed request or invalid parameters.</td>
</tr>

<tr>
<td>401</td>
<td>Unauthorized</td>
<td>Authentication required or credentials invalid.</td>
</tr>

<tr>
<td>403</td>
<td>Forbidden</td>
<td>Insufficient permissions for the requested action.</td>
</tr>

<tr>
<td>404</td>
<td>Not Found</td>
<td>Resource does not exist.</td>
</tr>

<tr>
<td>408</td>
<td>Request Timeout</td>
<td>Operation timed out.</td>
</tr>

<tr>
<td>409</td>
<td>Conflict</td>
<td>Resource state conflict (e.g., already exists).</td>
</tr>

<tr>
<td>413</td>
<td>Content Too Large</td>
<td>Payload exceeds allowed size.</td>
</tr>

<tr>
<td>429</td>
<td>Too Many Requests</td>
<td>Rate limit exceeded.</td>
</tr>

<tr>
<td>500</td>
<td>Internal Server Error</td>
<td>Unexpected error during processing.</td>
</tr>
</tbody>
</table><t>Implementations MAY use other HTTP status codes as defined in <xref target="RFC9110"></xref>. Receivers that encounter an unrecognized status code SHOULD treat it according to the class of the code (2xx = success, 3xx = redirect, 4xx = client error, 5xx = server error).</t>
</section>

<section anchor="error-payload"><name>Error Payload</name>
<t>When the PAYLOAD field is present in an ERROR message, it SHOULD be a PSON map containing an <tt>&quot;error&quot;</tt> key with a human-readable error description:</t>

<artwork><![CDATA[ERROR Message:
  STREAM_ID:  0x1234
  PARAMETERS: 404
  PAYLOAD:    {"error": "Resource 'sensor' does not exist"}
]]></artwork>
<t>Implementations MAY include additional keys in the error payload for application-specific diagnostics.</t>
</section>

<section anchor="ok-message-with-status-code"><name>OK Message with Status Code</name>
<t>An OK message MAY also carry a status code in the PARAMETERS field to provide more specific success information. If omitted, a generic 200 (OK) is assumed.</t>
</section>

<section anchor="connection-level-errors"><name>Connection-Level Errors</name>

<ul spacing="compact">
<li><strong>Transport failure:</strong> If the transport connection is lost, all active streams are terminated. The client SHOULD attempt reconnection with exponential backoff.</li>
<li><strong>Message too large:</strong> If a received message exceeds the maximum size, the receiver MUST close the connection.</li>
<li><strong>Invalid message type:</strong> If a receiver encounters an unknown message type, it SHOULD ignore the message.</li>
<li><strong>Decode failure:</strong> If a message body cannot be decoded, the receiver MUST close the connection.</li>
</ul>
</section>
</section>

<section anchor="security-considerations"><name>Security Considerations</name>
<t>This section follows the guidelines of <xref target="RFC3552"></xref> for security considerations.</t>

<section anchor="threat-model"><name>Threat Model</name>
<t>IOTMP is designed for client-server communication between IoT devices and infrastructure servers (brokers). The following threat model identifies the primary attack surfaces and the protocol's defenses.</t>
<t><strong>Actors:</strong></t>

<ul spacing="compact">
<li><strong>Device (Client):</strong> A constrained IoT device connecting to a broker over a potentially untrusted network.</li>
<li><strong>Server (Broker):</strong> An infrastructure server managing device connections, authentication, and resource routing.</li>
<li><strong>External Attacker:</strong> An entity with network access who can observe, inject, or modify traffic between the device and the server.</li>
<li><strong>Compromised Device:</strong> A legitimate device whose credentials or firmware have been compromised.</li>
</ul>
<t><strong>Threat Analysis:</strong></t>
<table>
<thead>
<tr>
<th>Threat</th>
<th>Attack Vector</th>
<th>Impact</th>
<th>Mitigation</th>
</tr>
</thead>

<tbody>
<tr>
<td><strong>Eavesdropping</strong></td>
<td>Passive observation of unencrypted traffic</td>
<td>Credential theft, data leakage</td>
<td>TLS encryption <xref target="transport-security"></xref></td>
</tr>

<tr>
<td><strong>Credential Replay</strong></td>
<td>Attacker captures and replays a valid CONNECT message</td>
<td>Unauthorized access, device impersonation</td>
<td>TLS prevents replay; servers SHOULD reject duplicate CONNECT on same connection</td>
</tr>

<tr>
<td><strong>Man-in-the-Middle</strong></td>
<td>Attacker intercepts and modifies messages in transit</td>
<td>Data manipulation, command injection</td>
<td>TLS with server certificate validation; mTLS for mutual authentication</td>
</tr>

<tr>
<td><strong>Device Impersonation</strong></td>
<td>Attacker uses stolen or guessed credentials</td>
<td>Unauthorized data injection, control of resources</td>
<td>Strong unique credentials per device; rate limiting on authentication; credential rotation <xref target="authentication"></xref></td>
</tr>

<tr>
<td><strong>Message Injection</strong></td>
<td>Attacker injects crafted IOTMP messages into a connection</td>
<td>Unauthorized resource invocation, data corruption</td>
<td>TLS integrity protection; servers MUST validate Stream ID correlation</td>
</tr>

<tr>
<td><strong>Message Tampering</strong></td>
<td>Modification of messages in transit</td>
<td>Altered sensor data, modified commands</td>
<td>TLS message integrity</td>
</tr>

<tr>
<td><strong>Denial of Service (Connection Flood)</strong></td>
<td>Attacker opens many TCP connections without authenticating</td>
<td>Server resource exhaustion</td>
<td>CONNECT handshake timeout; per-IP connection limits <xref target="denial-of-service"></xref></td>
</tr>

<tr>
<td><strong>Denial of Service (Message Flood)</strong></td>
<td>Compromised device sends excessive messages</td>
<td>Server CPU/memory exhaustion</td>
<td>Rate limiting; maximum message size enforcement <xref target="denial-of-service"></xref></td>
</tr>

<tr>
<td><strong>Stream Exhaustion</strong></td>
<td>Client opens maximum number of streams without closing them</td>
<td>Server memory exhaustion</td>
<td>Per-device stream limits; idle stream timeout <xref target="resource-exhaustion"></xref></td>
</tr>

<tr>
<td><strong>Malformed Payload</strong></td>
<td>Crafted PSON data with extreme nesting, oversized lengths</td>
<td>Stack overflow, memory exhaustion on constrained devices</td>
<td>PSON validation; nesting depth limits; length bounds checking <xref target="resource-exhaustion"></xref></td>
</tr>

<tr>
<td><strong>Resource Enumeration</strong></td>
<td>Attacker uses DESCRIBE to discover all device capabilities</td>
<td>Information disclosure; aids targeted attacks</td>
<td>Authorization on DESCRIBE; TLS to prevent passive discovery <xref target="privacy"></xref></td>
</tr>

<tr>
<td><strong>Unauthorized Resource Access</strong></td>
<td>Authenticated device invokes resources beyond its scope</td>
<td>Privilege escalation, unauthorized control</td>
<td>Per-resource authorization; least privilege principle <xref target="authorization"></xref></td>
</tr>
</tbody>
</table></section>

<section anchor="transport-security"><name>Transport Security</name>
<t>IOTMP itself does not provide encryption or integrity protection at the application layer. Implementations MUST rely on TLS (version 1.2 or later, as specified in <xref target="RFC8446"></xref>) for:</t>

<ul spacing="compact">
<li><strong>Confidentiality:</strong> Encryption of credentials and application data.</li>
<li><strong>Server Authentication:</strong> Verification of the server's identity via X.509 certificates.</li>
<li><strong>Mutual Authentication:</strong> (OPTIONAL) Verification of the client's identity via client certificates (mTLS).</li>
<li><strong>Message Integrity:</strong> Protection against message tampering and replay.</li>
</ul>
<t>Unencrypted connections (port 25204) SHOULD only be used in isolated, trusted networks or during development. Production deployments MUST use TLS.</t>
<t>Implementations SHOULD support TLS 1.3 <xref target="RFC8446"></xref> for improved handshake performance (1-RTT vs 2-RTT) and stronger cipher suites. TLS 1.2 MAY be supported for compatibility with constrained devices that lack TLS 1.3 implementations.</t>
</section>

<section anchor="authentication"><name>Authentication</name>
<t>The CONNECT message transmits authentication data in the PAYLOAD field. The format of this data depends on the authentication type (&quot;at&quot; parameter, <xref target="connect-message"></xref>). For type 0 (Credentials), the payload contains namespace, device ID, and credential in cleartext. For type 1 (Token), the payload contains a bearer token. Without TLS, this data is vulnerable to eavesdropping.</t>
<t>Implementations MUST use TLS when transmitting over untrusted networks.</t>
<t>Implementations MUST:</t>

<ul spacing="compact">
<li>Reject CONNECT messages with empty, malformed, or missing authentication data.</li>
<li>Respond with ERROR (401) when authentication credentials are invalid, expired, or do not match any registered device.</li>
<li>Close the connection after a failed CONNECT response (ERROR). The client MUST NOT retry authentication on the same transport connection.</li>
<li>Respond with ERROR (400) if the &quot;at&quot; value is not supported by the server.</li>
</ul>
<t>Implementations SHOULD:</t>

<ul spacing="compact">
<li>Use strong, unique credentials per device (type 0) or short-lived tokens (type 1).</li>
<li>Store credentials securely on the device (e.g., in a secure element or encrypted storage).</li>
<li>Implement rate limiting on authentication attempts to mitigate brute-force attacks (RECOMMENDED: maximum 3 attempts per minute per source IP).</li>
<li>Support credential rotation without requiring device firmware updates.</li>
<li>Log failed authentication attempts for security monitoring.</li>
<li>Prefer certificate-based authentication (type 2) or token-based authentication (type 1) over plain credentials (type 0) in production deployments.</li>
</ul>
<t>The &quot;at&quot; parameter is extensible. Additional authentication types MAY be defined in separate specifications.</t>

<section anchor="certificate-authentication-security"><name>Certificate Authentication Security</name>
<t>When using authentication type 2 (Certificate), deployments may use either per-device certificates or fleet certificates (a single certificate shared across multiple devices within a namespace). Each approach has different security properties:</t>
<t><strong>Per-device certificates:</strong> The certificate's subject (CN or SAN) identifies both the namespace and the device. The server extracts the full device identity from the certificate. If a PAYLOAD is present, the server MUST verify that the declared namespace and device_id match the certificate identity and MUST reject the connection with ERROR (401) on mismatch. This is the RECOMMENDED approach for production deployments.</t>
<t><strong>Fleet certificates:</strong> The certificate identifies only the namespace (e.g., a shared CA per organizational scope). The device MUST send a PAYLOAD with <tt>[namespace, device_id]</tt> so the server can identify the specific device. In this model, any device holding the fleet certificate can claim any device_id within the namespace. Deployments using fleet certificates SHOULD be aware that compromise of a single device's certificate allows impersonation of any other device in the same namespace. To mitigate this risk, operators SHOULD:</t>

<ul spacing="compact">
<li>Limit the scope of fleet certificates to the smallest practical namespace.</li>
<li>Implement server-side device registries that reject unknown device_ids even when the certificate is valid.</li>
<li>Monitor for anomalous device_id claims (e.g., a single certificate claiming multiple device_ids in rapid succession).</li>
<li>Prefer per-device certificates when the PKI infrastructure supports it.</li>
</ul>
</section>
</section>

<section anchor="authorization"><name>Authorization</name>
<t>Authorization (which operations a device may perform) is outside the scope of this specification and is determined by the server implementation. However, implementations SHOULD:</t>

<ul spacing="compact">
<li>Follow the principle of least privilege: devices SHOULD only be authorized to access the resources they require.</li>
<li>Enforce authorization on all message types, including DESCRIBE, RUN, START_STREAM, and STOP_STREAM.</li>
<li>Support per-resource access control (e.g., device X may read resource Y but not write to it).</li>
<li>Apply authorization checks on both client-to-server and server-to-client operations in symmetric mode.</li>
</ul>
</section>

<section anchor="denial-of-service"><name>Denial of Service</name>
<t><strong>Connection-level mitigations:</strong></t>

<ul spacing="compact">
<li>Servers SHOULD enforce a maximum time for the CONNECT handshake (RECOMMENDED: 10 seconds). If the client does not send a valid CONNECT message within this time, the server MUST close the connection.</li>
<li>Servers SHOULD limit the number of concurrent connections per account or IP address (RECOMMENDED: maximum 100 connections per IP).</li>
<li>Servers SHOULD implement connection rate limiting to prevent SYN flood attacks (RECOMMENDED: maximum 10 new connections per second per IP).</li>
<li>Servers SHOULD close idle connections that have not sent any message within twice the negotiated keepalive interval.</li>
</ul>
<t><strong>Message-level mitigations:</strong></t>

<ul spacing="compact">
<li>Implementations MUST enforce the negotiated maximum message size. A receiver that encounters a message exceeding its maximum MUST close the connection.</li>
<li>Servers SHOULD implement per-device message rate limiting (RECOMMENDED: configurable, default 100 messages per second).</li>
<li>Implementations MUST validate the Message Type field. Unknown message types SHOULD be ignored; malformed framing (e.g., varint that does not terminate) MUST cause connection closure.</li>
</ul>
</section>

<section anchor="resource-exhaustion"><name>Resource Exhaustion</name>

<ul spacing="compact">
<li>Servers SHOULD limit the number of concurrent streams per device (RECOMMENDED: maximum 256 active streams).</li>
<li>Servers SHOULD implement idle stream detection and close streams that have not produced data within a configurable timeout.</li>
<li>Implementations SHOULD validate PSON data structures to prevent deeply nested or excessively large payloads from consuming excessive processing time or memory. A maximum nesting depth of 16 levels is RECOMMENDED for constrained devices.</li>
<li>Implementations MUST validate that varint encoding terminates within 4 bytes (representing values up to 2^28 - 1). A varint that exceeds this limit MUST be treated as a decode error and the connection MUST be closed.</li>
</ul>
</section>

<section anchor="secure-credential-storage"><name>Secure Credential Storage</name>
<t>Devices operating in physically accessible environments face the risk of credential extraction through hardware attacks (JTAG, flash dumping, side-channel analysis). Implementations SHOULD:</t>

<ul spacing="compact">
<li>Store credentials in hardware secure elements or trusted execution environments when available.</li>
<li>Use derived keys rather than storing master credentials on devices.</li>
<li>Support remote credential revocation so that compromised devices can be disabled without physical access.</li>
</ul>
</section>

<section anchor="privacy"><name>Privacy</name>

<ul spacing="compact">
<li>Device identifiers, resource names, and DESCRIBE responses are visible to any entity with access to the network traffic unless TLS is used. TLS MUST be used when privacy of device metadata is required.</li>
<li>Implementations SHOULD minimize the exposure of device metadata in resource names and DESCRIBE responses when operating over untrusted networks.</li>
<li>The DESCRIBE message can reveal the full API surface of a device, including resource names, I/O types, data schemas, and human-readable descriptions. Servers MUST enforce authorization on DESCRIBE requests and SHOULD allow operators to restrict which clients can perform API discovery.</li>
<li>Servers SHOULD NOT log secrets (credentials, tokens). For type 0 (Credentials), implementations SHOULD log only the namespace and device ID for audit purposes.</li>
</ul>
</section>
</section>

<section anchor="conformance-requirements"><name>Conformance Requirements</name>

<section anchor="client-conformance"><name>Client Conformance</name>
<t>A conformant IOTMP client implementation MUST:</t>

<ul spacing="compact">
<li>Support TCP and TLS transports.</li>
<li>Send a CONNECT message as the first message after transport establishment.</li>
<li>Support all message types defined in <xref target="message-types"></xref>.</li>
<li>Correctly encode and decode PSON values as defined in <xref target="PSON"></xref>.</li>
<li>Support at least one concurrent stream.</li>
<li>Implement keepalive as defined in <xref target="keepalive"></xref>.</li>
<li>Implement reconnection with exponential backoff as defined in <xref target="reconnection"></xref>.</li>
<li>Support messages up to at least 32,768 bytes (or the negotiated maximum).</li>
<li>Include a Stream ID in all request messages (CONNECT, RUN, DESCRIBE, START_STREAM, STOP_STREAM).</li>
<li>Use even Stream IDs for all client-initiated requests <xref target="stream-id-partitioning"></xref>.</li>
<li>Not reuse a Stream ID that is currently active.</li>
<li>Close the connection upon receiving an ERROR response to CONNECT.</li>
</ul>
<t>A conformant client SHOULD:</t>

<ul spacing="compact">
<li>Support WebSocket transport with the &quot;iotmp&quot; subprotocol.</li>
<li>Support multiple concurrent streams.</li>
<li>Support compact streaming mode <xref target="compact-encoding-mode"></xref>.</li>
<li>Support DESCRIBE with <xref target="JSON-Schema"></xref> <xref target="resource-description-describe-message"></xref>.</li>
</ul>
</section>

<section anchor="server-conformance"><name>Server Conformance</name>
<t>A conformant IOTMP server implementation MUST:</t>

<ul spacing="compact">
<li>Support TCP and TLS transports.</li>
<li>Process CONNECT messages and respond with OK or ERROR.</li>
<li>Support all message types defined in <xref target="message-types"></xref>.</li>
<li>Correctly encode and decode PSON values as defined in <xref target="PSON"></xref>.</li>
<li>Respect the client's declared maximum message size (&quot;ms&quot; parameter).</li>
<li>Use odd Stream IDs for all server-initiated requests <xref target="stream-id-partitioning"></xref>.</li>
<li>Validate that client-initiated requests use even Stream IDs.</li>
<li>Echo KEEP_ALIVE messages as defined in <xref target="keepalive"></xref>.</li>
<li>Implement keepalive timeout detection.</li>
<li>Enforce the CONNECT handshake timeout <xref target="denial-of-service"></xref>.</li>
<li>Enforce maximum message size <xref target="denial-of-service"></xref>.</li>
<li>Validate the Message Type field and close the connection on malformed framing.</li>
<li>Enforce authorization on all resource operations.</li>
</ul>
<t>A conformant server SHOULD:</t>

<ul spacing="compact">
<li>Support WebSocket transport with the &quot;iotmp&quot; subprotocol.</li>
<li>Support server redirect <xref target="server-redirect"></xref>.</li>
<li>Include status codes in OK and ERROR responses <xref target="status-codes"></xref>.</li>
<li>Support compact streaming mode negotiation <xref target="compact-encoding-mode"></xref>.</li>
<li>Validate incoming data against <xref target="JSON-Schema"></xref> when available from DESCRIBE <xref target="resource-description-describe-message"></xref>.</li>
</ul>
</section>

<section anchor="connection-lifecycle-conformance"><name>Connection Lifecycle Conformance</name>
<t>The following rules govern connection state transitions and edge cases:</t>
<t><strong>Stream Recovery on Reconnect:</strong></t>
<t>When a transport connection is lost, all active streams are terminated. Stream state (Stream IDs, compact mode schemas) is NOT preserved across connections. After reconnection:</t>

<ol spacing="compact">
<li>The client MUST send a new CONNECT message.</li>
<li>The server MUST re-establish any desired streams by sending new START_STREAM messages.</li>
<li>Both sides MUST NOT reference Stream IDs from the previous connection.</li>
</ol>
<t><strong>In-Flight Message Handling:</strong></t>

<ul spacing="compact">
<li>If a request message (RUN, DESCRIBE, START_STREAM, STOP_STREAM) has been sent but no response received when the connection drops, the request is considered failed. The client SHOULD NOT assume the operation was executed.</li>
<li>If a response (OK, ERROR) is lost due to connection failure, the server MAY have already executed the operation. The client MAY safely re-send the request after reconnection, provided the operation is idempotent.</li>
</ul>
<t><strong>Idempotency Guidance:</strong></t>
<t>IOTMP's request-response model (RUN -&gt; OK/ERROR) provides implicit delivery confirmation for the common case: if the client receives OK, the operation succeeded; if it receives ERROR, it failed. The only ambiguous scenario occurs when the connection drops after the server executes the operation but before the client receives the response.</t>
<t>IOTMP does not define protocol-level delivery guarantees (such as QoS levels) because its request-response model already covers the vast majority of cases. For the rare connection-loss scenario, the recommended approach is to design resource operations as <strong>absolute state transitions</strong> rather than relative or toggling operations:</t>
<table>
<thead>
<tr>
<th>Pattern</th>
<th>Example</th>
<th>Safe to retry?</th>
</tr>
</thead>

<tbody>
<tr>
<td>Absolute state (RECOMMENDED)</td>
<td><tt>{&quot;relay&quot;: true}</tt></td>
<td>Yes -- setting the same state twice has no additional effect.</td>
</tr>

<tr>
<td>Relative/toggle (AVOID)</td>
<td><tt>{&quot;action&quot;: &quot;toggle&quot;}</tt></td>
<td>No -- retrying may reverse the intended state.</td>
</tr>
</tbody>
</table><t>Resources that follow this pattern are inherently idempotent: re-executing the same RUN after reconnection produces the same result whether the original request was executed or not. This eliminates the need for protocol-level deduplication mechanisms.</t>
<t>Additionally, if the client needs to verify the outcome of an ambiguous request after reconnection, it can query the current resource state by sending a RUN (for output resources) or DESCRIBE to the resource. This allows the client to confirm whether the previous operation took effect before deciding whether to retry.</t>
<t>For the rare cases where non-idempotent operations are unavoidable, application profiles MAY implement their own deduplication (e.g., including a unique request identifier in the PAYLOAD that the receiver checks against recently processed requests).</t>
<t><strong>Invalid State Handling:</strong></t>
<table>
<thead>
<tr>
<th>Condition</th>
<th>Required Behavior</th>
</tr>
</thead>

<tbody>
<tr>
<td>Client sends a message before CONNECT</td>
<td>Server MUST close the connection.</td>
</tr>

<tr>
<td>Client sends CONNECT after authentication</td>
<td>Server MUST respond with ERROR (400) and close the connection.</td>
</tr>

<tr>
<td>Message received with unknown message type</td>
<td>Receiver SHOULD ignore the message.</td>
</tr>

<tr>
<td>Message received with unknown field number</td>
<td>Receiver MUST ignore the unknown field and process known fields.</td>
</tr>

<tr>
<td>STREAM_DATA received for unknown Stream ID</td>
<td>Receiver SHOULD ignore the message.</td>
</tr>

<tr>
<td>RUN/DESCRIBE received for non-existent resource</td>
<td>Receiver MUST respond with ERROR (404).</td>
</tr>

<tr>
<td>Message exceeds negotiated maximum size</td>
<td>Receiver MUST close the connection.</td>
</tr>

<tr>
<td>Varint does not terminate within 4 bytes</td>
<td>Receiver MUST close the connection.</td>
</tr>

<tr>
<td>Keepalive timeout exceeded</td>
<td>Server MUST close the connection.</td>
</tr>

<tr>
<td>Request with Stream ID from wrong partition <xref target="stream-id-partitioning"></xref></td>
<td>Receiver MUST respond with ERROR (400).</td>
</tr>

<tr>
<td>Request with a Stream ID that is already active</td>
<td>Receiver MUST respond with ERROR (409).</td>
</tr>
</tbody>
</table></section>

<section anchor="conformance-test-vectors"><name>Conformance Test Vectors</name>
<t>The following test vectors allow implementations to verify correct encoding and decoding. Each vector specifies the input, the expected wire encoding (hex), and the total byte count.</t>

<section anchor="keep-alive"><name>KEEP_ALIVE</name>
<t><strong>Input:</strong> KEEP_ALIVE message (empty body).</t>

<artwork><![CDATA[Expected encoding: 05 00
Total: 2 bytes
]]></artwork>

<ul spacing="compact">
<li><tt>05</tt>: Message type KEEP_ALIVE (varint 5).</li>
<li><tt>00</tt>: Body size 0 (varint 0).</li>
</ul>
</section>

<section anchor="connect"><name>CONNECT</name>
<t><strong>Input:</strong> CONNECT (auth type 0) with credentials <tt>[&quot;acme1&quot;, &quot;device1&quot;, &quot;secret123&quot;]</tt> (namespace, device_id, credential), Stream ID = 42 (client-initiated, even).</t>

<artwork><![CDATA[Expected encoding:
03 1C 08 2A 1A E3 85 61 63 6D 65 31 87 64 65 76
69 63 65 31 89 73 65 63 72 65 74 31 32 33
Total: 30 bytes
]]></artwork>
<t>Decoding verification:</t>

<ul spacing="compact">
<li><tt>03</tt>: Message type CONNECT.</li>
<li><tt>1C</tt>: Body size 28.</li>
<li><tt>08</tt>: Field tag (STREAM_ID, varint). <tt>2A</tt>: Stream ID = 42.</li>
<li><tt>1A</tt>: Field tag (PAYLOAD, pson).</li>
<li><tt>E3</tt>: PSON array, 3 elements.</li>
<li><tt>85 61 63 6D 65 31</tt>: PSON string &quot;acme1&quot; (namespace, 5 bytes).</li>
<li><tt>87 64 65 76 69 63 65 31</tt>: PSON string &quot;device1&quot; (device_id, 7 bytes).</li>
<li><tt>89 73 65 63 72 65 74 31 32 33</tt>: PSON string &quot;secret123&quot; (credential, 9 bytes).</li>
</ul>
</section>

<section anchor="ok-response"><name>OK Response</name>
<t><strong>Input:</strong> OK response to Stream ID 42, no status code, no payload.</t>

<artwork><![CDATA[Expected encoding: 01 02 08 2A
Total: 4 bytes
]]></artwork>

<ul spacing="compact">
<li><tt>01</tt>: Message type OK.</li>
<li><tt>02</tt>: Body size 2.</li>
<li><tt>08</tt>: Field tag (STREAM_ID, varint). <tt>2A</tt>: Stream ID = 42.</li>
</ul>
</section>

<section anchor="run-with-resource-name"><name>RUN with Resource Name</name>
<t><strong>Input:</strong> RUN resource &quot;led&quot; with payload <tt>{&quot;on&quot;: true}</tt>, Stream ID = 100 (client-initiated, even).</t>

<artwork><![CDATA[Expected encoding:
06 0D 08 64 22 83 6C 65 64 1A C1 82 6F 6E 61
Total: 15 bytes
]]></artwork>
<t>Decoding verification:</t>

<ul spacing="compact">
<li><tt>06</tt>: Message type RUN.</li>
<li><tt>0D</tt>: Body size 13.</li>
<li><tt>08 64</tt>: STREAM_ID = 100.</li>
<li><tt>22 83 6C 65 64</tt>: RESOURCE = PSON string &quot;led&quot; (3 bytes).</li>
<li><tt>1A C1 82 6F 6E 61</tt>: PAYLOAD = PSON map {1 entry: &quot;on&quot; -&gt; true}.</li>
</ul>
</section>

<section anchor="run-with-resource-hash"><name>RUN with Resource Hash</name>
<t><strong>Input:</strong> RUN resource hash 0x1A2B (FNV-1a hash of a resource name), no payload, Stream ID = 7 (server-initiated, odd).</t>

<artwork><![CDATA[Expected encoding:
06 05 08 07 20 AB 34
Total: 7 bytes
]]></artwork>
<t>Decoding verification:</t>

<ul spacing="compact">
<li><tt>06</tt>: Message type RUN.</li>
<li><tt>05</tt>: Body size 5.</li>
<li><tt>08 07</tt>: STREAM_ID = 7.</li>
<li><tt>20 AB 34</tt>: RESOURCE = varint 6699 (0x1A2B).</li>
</ul>
</section>

<section anchor="error-with-status-code-and-payload"><name>ERROR with Status Code and Payload</name>
<t><strong>Input:</strong> ERROR for Stream ID 42, status 404, payload <tt>{&quot;error&quot;: &quot;Not found&quot;}</tt>.</t>

<artwork><![CDATA[Expected encoding:
02 17 08 2A 10 94 03 1A C1 85 65 72 72 6F 72 89
4E 6F 74 20 66 6F 75 6E 64
Total: 25 bytes
]]></artwork>
<t>Decoding verification:</t>

<ul spacing="compact">
<li><tt>02</tt>: Message type ERROR.</li>
<li><tt>17</tt>: Body size 23.</li>
<li><tt>08 2A</tt>: STREAM_ID = 42.</li>
<li><tt>10 94 03</tt>: PARAMETERS (varint) = 404.</li>
<li><tt>1A C1 85 65 72 72 6F 72 89 4E 6F 74 20 66 6F 75 6E 64</tt>: PAYLOAD = PSON map {&quot;error&quot;: &quot;Not found&quot;}.</li>
</ul>
</section>

<section anchor="start-stream-with-compact-mode"><name>START_STREAM with Compact Mode</name>
<t><strong>Input:</strong> START_STREAM resource &quot;temperature&quot;, interval 5000ms, compact mode, Stream ID = 161 (server-initiated, odd).</t>

<artwork><![CDATA[Expected encoding:
08 1B 08 A1 01 12 C2 81 69 1F 88 27 82 63 6D 61
22 8B 74 65 6D 70 65 72 61 74 75 72 65
Total: 29 bytes
]]></artwork>
<t>Decoding verification:</t>

<ul spacing="compact">
<li><tt>08</tt>: Message type START_STREAM.</li>
<li><tt>1B</tt>: Body size 27.</li>
<li><tt>08 A1 01</tt>: STREAM_ID = 161 (varint).</li>
<li><tt>12 C2 81 69 1F 88 27 82 63 6D 61</tt>: PARAMETERS (pson map) = {&quot;i&quot;: 5000, &quot;cm&quot;: true}.</li>
<li><tt>22 8B 74 65 6D 70 65 72 61 74 75 72 65</tt>: RESOURCE = PSON string &quot;temperature&quot; (11 bytes).</li>
</ul>
</section>
</section>
</section>

<section anchor="iana-considerations"><name>IANA Considerations</name>

<section anchor="port-number-registration"><name>Port Number Registration</name>
<t>This specification requests the assignment of the following TCP port numbers from IANA's Service Name and Transport Protocol Port Number Registry:</t>
<table>
<thead>
<tr>
<th>Service Name</th>
<th>Port</th>
<th>Transport</th>
<th>Description</th>
<th>Reference</th>
</tr>
</thead>

<tbody>
<tr>
<td>iotmp</td>
<td>25204</td>
<td>TCP</td>
<td>IOTMP over TCP (unencrypted)</td>
<td>This document</td>
</tr>

<tr>
<td>iotmps</td>
<td>25206</td>
<td>TCP</td>
<td>IOTMP over TLS</td>
<td>This document</td>
</tr>
</tbody>
</table><t><strong>Registration details:</strong></t>

<ul spacing="compact">
<li><strong>Service Name:</strong> iotmp / iotmps</li>
<li><strong>Transport Protocol:</strong> TCP</li>
<li><strong>Assignee:</strong> Alvaro Luis Bustamante / Internet of Thinger SL</li>
<li><strong>Contact:</strong> alvaro@thinger.io</li>
<li><strong>Description:</strong> Internet of Things Message Protocol -- a binary application-layer protocol for bidirectional communication between IoT devices and servers.</li>
<li><strong>Reference:</strong> This document</li>
<li><strong>Assignment Notes:</strong> Port 25204 is for unencrypted connections (development and trusted networks only). Port 25206 is for TLS-encrypted connections (production use). Implementations SHOULD default to port 25206 (TLS).</li>
</ul>
</section>

<section anchor="websocket-subprotocol-registration"><name>WebSocket Subprotocol Registration</name>
<t>This specification requests registration of the following entry in the IANA WebSocket Subprotocol Name Registry, as defined in <xref target="RFC6455"></xref>, Section 11.5:</t>

<ul spacing="compact">
<li><strong>Subprotocol Identifier:</strong> iotmp</li>
<li><strong>Subprotocol Common Name:</strong> Internet of Things Message Protocol</li>
<li><strong>Subprotocol Definition:</strong> This document</li>
<li><strong>Reference:</strong> This document</li>
</ul>
<t>When operating over WebSocket, the client MUST include <tt>&quot;iotmp&quot;</tt> in the <tt>Sec-WebSocket-Protocol</tt> header during the handshake. The server MUST confirm the subprotocol in the response. All WebSocket messages MUST use binary opcode (0x02) and each frame MUST contain exactly one complete IOTMP message <xref target="websocket-transport"></xref>.</t>
</section>

<section anchor="message-type-registry"><name>Message Type Registry</name>
<t>IANA is requested to create a new registry entitled &quot;IOTMP Message Types&quot; in a new &quot;Internet of Things Message Protocol (IOTMP)&quot; registry group. The registry contains the following columns: Value, Name, and Reference.</t>
<t>New registrations in the range 0x0B-0xFF require Standards Action <xref target="RFC8126"></xref>.</t>
<t>Initial values:</t>
<table>
<thead>
<tr>
<th>Value</th>
<th>Name</th>
<th>Reference</th>
</tr>
</thead>

<tbody>
<tr>
<td>0x00</td>
<td>RESERVED</td>
<td>This document</td>
</tr>

<tr>
<td>0x01</td>
<td>OK</td>
<td>This document</td>
</tr>

<tr>
<td>0x02</td>
<td>ERROR</td>
<td>This document</td>
</tr>

<tr>
<td>0x03</td>
<td>CONNECT</td>
<td>This document</td>
</tr>

<tr>
<td>0x04</td>
<td>DISCONNECT</td>
<td>This document</td>
</tr>

<tr>
<td>0x05</td>
<td>KEEP_ALIVE</td>
<td>This document</td>
</tr>

<tr>
<td>0x06</td>
<td>RUN</td>
<td>This document</td>
</tr>

<tr>
<td>0x07</td>
<td>DESCRIBE</td>
<td>This document</td>
</tr>

<tr>
<td>0x08</td>
<td>START_STREAM</td>
<td>This document</td>
</tr>

<tr>
<td>0x09</td>
<td>STOP_STREAM</td>
<td>This document</td>
</tr>

<tr>
<td>0x0A</td>
<td>STREAM_DATA</td>
<td>This document</td>
</tr>
</tbody>
</table></section>

<section anchor="authentication-type-registry"><name>Authentication Type Registry</name>
<t>IANA is requested to create a new registry entitled &quot;IOTMP Authentication Types&quot; in the &quot;Internet of Things Message Protocol (IOTMP)&quot; registry group. The registry contains the following columns: Code, Name, PAYLOAD Format, and Reference.</t>
<t>New registrations in the range 3-255 require Specification Required <xref target="RFC8126"></xref>.</t>
<t>Initial values:</t>
<table>
<thead>
<tr>
<th>Code</th>
<th>Name</th>
<th>PAYLOAD Format</th>
<th>Reference</th>
</tr>
</thead>

<tbody>
<tr>
<td>0</td>
<td>Credentials</td>
<td>PSON array: [namespace, device_id, credential]</td>
<td>This document</td>
</tr>

<tr>
<td>1</td>
<td>Token</td>
<td>PSON string (bearer token)</td>
<td>This document</td>
</tr>

<tr>
<td>2</td>
<td>Certificate</td>
<td>Optional PSON array: [namespace, device_id] or absent</td>
<td>This document</td>
</tr>
</tbody>
</table></section>

<section anchor="field-number-registry"><name>Field Number Registry</name>
<t>IANA is requested to create a new registry entitled &quot;IOTMP Field Numbers&quot; in the &quot;Internet of Things Message Protocol (IOTMP)&quot; registry group. The registry contains the following columns: Number, Name, Allowed Wire Types, and Reference.</t>
<t>New registrations in the range 0x05-0x07 require Standards Action <xref target="RFC8126"></xref>.</t>
<t>Initial values:</t>
<table>
<thead>
<tr>
<th>Number</th>
<th>Name</th>
<th>Allowed Wire Types</th>
<th>Reference</th>
</tr>
</thead>

<tbody>
<tr>
<td>0x00</td>
<td>RESERVED</td>
<td>--</td>
<td>This document</td>
</tr>

<tr>
<td>0x01</td>
<td>STREAM_ID</td>
<td>varint</td>
<td>This document</td>
</tr>

<tr>
<td>0x02</td>
<td>PARAMETERS</td>
<td>varint, pson</td>
<td>This document</td>
</tr>

<tr>
<td>0x03</td>
<td>PAYLOAD</td>
<td>pson, bytes</td>
<td>This document</td>
</tr>

<tr>
<td>0x04</td>
<td>RESOURCE</td>
<td>varint, pson</td>
<td>This document</td>
</tr>
</tbody>
</table></section>

<section anchor="wire-type-registry"><name>Wire Type Registry</name>
<t>IANA is requested to create a new registry entitled &quot;IOTMP Wire Types&quot; in the &quot;Internet of Things Message Protocol (IOTMP)&quot; registry group. The registry contains the following columns: Value, Name, Description, and Reference.</t>
<t>New registrations in the range 0x03-0x07 require Standards Action <xref target="RFC8126"></xref>.</t>
<t>Initial values:</t>
<table>
<thead>
<tr>
<th>Value</th>
<th>Name</th>
<th>Description</th>
<th>Reference</th>
</tr>
</thead>

<tbody>
<tr>
<td>0x00</td>
<td>varint</td>
<td>Variable-length unsigned integer</td>
<td>This document</td>
</tr>

<tr>
<td>0x01</td>
<td>bytes</td>
<td>Length-prefixed raw bytes</td>
<td>This document</td>
</tr>

<tr>
<td>0x02</td>
<td>pson</td>
<td>PSON-encoded value</td>
<td>This document</td>
</tr>
</tbody>
</table></section>

<section anchor="media-type"><name>Media Type</name>
<t>The <tt>application/pson</tt> media type is defined in <xref target="PSON"></xref>.</t>
</section>
</section>

</middle>

<back>
<references><name>References</name>
<references><name>Normative References</name>
<reference anchor="IEEE754" target="">
  <front>
    <title>IEEE Standard for Floating-Point Arithmetic</title>
    <author>
      <organization>IEEE</organization>
    </author>
    <date year="2019"></date>
  </front>
  <seriesInfo name="IEEE" value="754-2019"></seriesInfo>
</reference>
<reference anchor="JSON-Schema" target="https://json-schema.org/specification">
  <front>
    <title>JSON Schema: A Media Type for Describing JSON Documents</title>
    <author fullname="Austin Wright" initials="A." surname="Wright"></author>
    <author fullname="Henry Andrews" initials="H." surname="Andrews"></author>
    <author fullname="Ben Hutton" initials="B." surname="Hutton"></author>
    <author fullname="Greg Dennis" initials="G." surname="Dennis"></author>
    <date year="2022" month="June"></date>
  </front>
  <seriesInfo name="Internet-Draft" value="draft-bhutton-json-schema-01"></seriesInfo>
</reference>
<reference anchor="PSON" target="">
  <front>
    <title>PSON: Packed Sensor Object Notation</title>
    <author fullname="Alvaro Luis Bustamante" initials="A.L." surname="Bustamante"></author>
    <date year="2026" month="March"></date>
  </front>
  <seriesInfo name="Internet-Draft" value="draft-bustamante-pson-00"></seriesInfo>
</reference>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.2119.xml"/>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.6455.xml"/>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.8174.xml"/>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.8446.xml"/>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.9110.xml"/>
</references>
<references><name>Informative References</name>
<reference anchor="LwM2M" target="">
  <front>
    <title>Lightweight Machine to Machine Technical Specification</title>
    <author>
      <organization>Open Mobile Alliance</organization>
    </author>
  </front>
  <seriesInfo name="OMA" value="TS-LightweightM2M_Core-V1_2"></seriesInfo>
</reference>
<reference anchor="MQTT" target="https://docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.html">
  <front>
    <title>MQTT Version 5.0</title>
    <author>
      <organization>OASIS</organization>
    </author>
    <date year="2019" month="March"></date>
  </front>
  <seriesInfo name="OASIS" value="Standard"></seriesInfo>
</reference>
<reference anchor="ProtocolBuffers" target="https://protobuf.dev/programming-guides/encoding/">
  <front>
    <title>Protocol Buffers Encoding</title>
    <author>
      <organization>Google</organization>
    </author>
  </front>
</reference>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.3552.xml"/>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.7252.xml"/>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.8126.xml"/>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.8428.xml"/>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.8610.xml"/>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.8949.xml"/>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.9543.xml"/>
</references>
</references>

<section anchor="wire-format-examples"><name>Wire Format Examples</name>

<section anchor="keep-alive-message"><name>KEEP_ALIVE Message</name>

<artwork><![CDATA[05 00
|  +- Body Size: 0 (varint)
+---- Message Type: 0x05 = KEEP_ALIVE (varint)
]]></artwork>
<t>Total: <strong>2 bytes</strong>.</t>
</section>

<section anchor="connect-message-1"><name>CONNECT Message</name>
<t>For auth type 0 credentials <tt>[&quot;acme1&quot;, &quot;device1&quot;, &quot;secret123&quot;]</tt> (namespace, device_id, credential) with Stream ID 42:</t>

<artwork><![CDATA[03                          # Message Type: CONNECT (varint)
1C                          # Body Size: 28 (varint)
08                          # STREAM_ID (field=1, wire=varint)
2A                          # Stream ID: 42 (varint)
1A                          # PAYLOAD (field=3, wire=pson)
E3                          # PSON: array of 3 elements
  85 61 63 6D 65 31         # string "acme1" (namespace, len=5)
  87 64 65 76 69 63 65 31   # string "device1" (device_id, len=7)
                            # PSON: string "secret123"
  89 73 65 63 72 65 74 31 32 33  # (credential, len=9)
]]></artwork>
<t>Total: <strong>30 bytes</strong>.</t>
</section>

<section anchor="run-message-resource-invocation"><name>RUN Message (Resource Invocation)</name>
<t>Peer asks for resource &quot;temperature&quot;:</t>

<artwork><![CDATA[06                          # Message Type: RUN (varint)
0F                          # Body Size: 15 (varint)
08                          # STREAM_ID (field=1, wire=varint)
2A                          # Stream ID: 42
22                          # RESOURCE (field=4, wire=pson)
                            # PSON: string "temperature" (len=11)
8B 74 65 6D 70 65 72 61 74 75 72 65
]]></artwork>
<t>Total: <strong>17 bytes</strong>.</t>
</section>

<section anchor="ok-response-with-payload"><name>OK Response with Payload</name>
<t>Response with <tt>{&quot;temperature&quot;: 25.3}</tt>:</t>

<artwork><![CDATA[01                          # Message Type: OK (varint)
15                          # Body Size: 21 (varint)
08                          # STREAM_ID (field=1, wire=varint)
2A                          # Stream ID: 42
1A                          # PAYLOAD (field=3, wire=pson)
C1                          # PSON: map with 1 entry
                            # key: "temperature" (len=11)
  8B 74 65 6D 70 65 72 61 74 75 72 65
                            # value: float 25.3 (IEEE 754, LE)
  40 66 66 CA 41
]]></artwork>
<t>Total: <strong>23 bytes</strong>.</t>
</section>

<section anchor="error-response-with-status-code"><name>ERROR Response with Status Code</name>
<t>Response with 404 status and error message:</t>

<artwork><![CDATA[02                          # Message Type: ERROR (varint)
20                          # Body Size: 32 (varint)
08                          # STREAM_ID (field=1, wire=varint)
2A                          # Stream ID: 42
10                          # PARAMETERS (field=2, wire=varint)
94 03                       # Status code: 404 (varint)
1A                          # PAYLOAD (field=3, wire=pson)
C1                          # PSON: map with 1 entry
  85 65 72 72 6F 72         # key: "error" (len=5)
                            # value: "Resource not found" (len=18)
  92 52 65 73 6F 75 72 63 65 20 6E 6F 74 20
  66 6F 75 6E 64
]]></artwork>
<t>Total: <strong>34 bytes</strong>.</t>
</section>
</section>

<section anchor="formal-message-grammar-cddl"><name>Formal Message Grammar (CDDL)</name>
<t>This appendix provides a formal definition of IOTMP messages using CDDL (Concise Data Definition Language, <xref target="RFC8610"></xref>). This grammar is normative and defines the <strong>logical structure</strong> of all IOTMP messages -- the required fields, their types, and their constraints.</t>
<t><strong>Note on scope:</strong> CDDL is designed for CBOR-based protocols, but is used here as a structural description language. The CDDL definitions describe the semantic structure of each message (which fields exist, their types, and cardinality), NOT the wire encoding. The actual wire format uses IOTMP's own framing <xref target="message-framing"></xref>, field tag encoding <xref target="message-fields"></xref>, and PSON data encoding <xref target="data-encoding-pson"></xref>, which differ from CBOR. Implementations MUST follow the wire format defined in <xref target="message-framing"></xref>, <xref target="message-fields"></xref>, and <xref target="data-encoding-pson"></xref>; the CDDL grammar serves as a complementary formal reference for message structure validation.</t>

<section anchor="message-frame"><name>Message Frame</name>

<artwork><![CDATA[+-------------------------------------------------------------+
|                     IOTMP Message Frame                      |
+--------------+--------------+--------------------------------+
| Message Type |  Body Size   |            Body                |
|  (varint)    |  (varint)    |       (Body Size bytes)        |
|  1-2 bytes   |  1-4 bytes   |       0-N bytes                |
+--------------+--------------+--------------------------------+
]]></artwork>
</section>

<section anchor="field-tag-encoding-1"><name>Field Tag Encoding</name>

<artwork><![CDATA[+---------------------------------------+
|          Field Tag (1 byte)           |
+------------------+--------------------+
|  Field Number    |   Wire Type        |
|  (bits 7-3)      |   (bits 2-0)       |
|  5 bits (0-31)   |   3 bits (0-7)     |
+------------------+--------------------+

Wire Type values:
  0x00 = varint   (variable-length unsigned integer)
  0x01 = bytes    (length-prefixed raw bytes)
  0x02 = pson     (PSON-encoded value)

Defined Field Tags:
  0x08 = STREAM_ID   (field 1, wire type varint)
  0x10 = PARAMETERS  (field 2, wire type varint)
  0x12 = PARAMETERS  (field 2, wire type pson)
  0x1A = PAYLOAD     (field 3, wire type pson)
  0x19 = PAYLOAD     (field 3, wire type bytes)
  0x20 = RESOURCE    (field 4, wire type varint -- resource hash)
  0x22 = RESOURCE    (field 4, wire type pson -- resource name or hash)
]]></artwork>
</section>

<section anchor="varint-encoding-1"><name>Varint Encoding</name>

<artwork><![CDATA[Single-byte varint (values 0-127):
+---+-----------+
| 0 |  value    |
|   | (7 bits)  |
+---+-----------+

Multi-byte varint (values >= 128):
+---+-----------+ +---+-----------+     +---+-----------+
| 1 | bits 0-6  | | 1 | bits 7-13 | ... | 0 | bits N-N+6|
+---+-----------+ +---+-----------+     +---+-----------+
 MSB = 1: more      MSB = 1: more        MSB = 0: last
 bytes follow        bytes follow         byte
]]></artwork>
</section>

<section anchor="cddl-definitions"><name>CDDL Definitions</name>

<sourcecode type="cddl"><![CDATA[; ===========================================================
; IOTMP Message Grammar -- CDDL (RFC 8610)
; ===========================================================

; --- Top-level frame ---

iotmp-frame = (
  message-type: message-type-id,
  body-size:    uint,
  body:         bstr .size body-size,
)

message-type-id = &(
  RESERVED:     0x00,
  OK:           0x01,
  ERROR:        0x02,
  CONNECT:      0x03,
  DISCONNECT:   0x04,
  KEEP_ALIVE:   0x05,
  RUN:          0x06,
  DESCRIBE:     0x07,
  START_STREAM: 0x08,
  STOP_STREAM:  0x09,
  STREAM_DATA:  0x0A,
)

; --- Field definitions ---

stream-id   = uint .size 2     ; 0-65535 (even=client, odd=server)
status-code = uint              ; HTTP status code (RFC 9110)
resource-id = tstr / uint .size 2
                                ; string name or 16-bit FNV-1a hash

; --- Per-message field requirements ---

; CONNECT (0x03): Client -> Server
connect-body = {
  stream_id:   stream-id,
  ? payload:   connect-payload,
  ? parameters: {
    ? "v":  uint .default 1,         ; protocol version
    ? "ka": uint .le 1800 .default 60, ; keepalive (seconds)
    ? "at": auth-type .default 0,    ; authentication type
    ? "ms": uint .ge 1024 .default 32768,  ; max message size (bytes)
  },
}

auth-type = &(
  credentials: 0,  ; PAYLOAD = [ns, device_id, cred]
  token:       1,  ; PAYLOAD = bearer token string
  certificate: 2,  ; PAYLOAD optional: [ns, device_id]
                   ; or absent
)

connect-payload = credentials-payload
                / token-payload
                / certificate-payload

credentials-payload = [
  namespace: tstr, device_id: tstr, credential: tstr
]
token-payload       = tstr      ; bearer token (JWT, API key, etc.)
certificate-payload = [namespace: tstr, device_id: tstr]
                    / nil / empty

; OK (0x01): Response to any request
ok-body = {
  stream_id:    stream-id,
  ? parameters: status-code / pson-value,
                ; status code or structured data
  ? payload:    pson-value,
}

; ERROR (0x02): Error response to any request
error-body = {
  stream_id:    stream-id,
  ? parameters: status-code,
  ? payload: { "error": tstr, * tstr => any },
             ; error message + optional diagnostics
}

; DISCONNECT (0x04)
disconnect-body = {
  ? parameters: status-code,    ; redirect code (301, 307)
  ? payload:    pson-value,          ; redirect target or reason
}

; KEEP_ALIVE (0x05)
keep-alive-body = empty              ; body MUST be empty (size = 0)

; RUN (0x06): Execute a resource
run-body = {
  stream_id:  stream-id,
  resource:   resource-id,
  ? payload:  pson-value,            ; input data for the resource
}

; DESCRIBE (0x07): Request resource metadata
describe-body = {
  stream_id:  stream-id,
  ? resource: resource-id,
              ; absent = full API, present = single resource
}

; START_STREAM (0x08): Begin streaming
start-stream-body = {
  stream_id:    stream-id,
  resource:     resource-id,
  ? parameters: uint / {        ; interval or structured params
    ? "i":  uint,              ; interval in ms (0 = event-driven)
    ? "cm": bool .default false,     ; compact mode request
  },
}

; STOP_STREAM (0x09): End streaming
stop-stream-body = {
  stream_id: stream-id,
}

; STREAM_DATA (0x0A): Stream payload
stream-data-body = {
  stream_id: stream-id,
  payload:   pson-value,   ; resource data (map or compact arr)
}

; --- DESCRIBE response structures ---

; Full API DESCRIBE response (no RESOURCE in request)
full-api-describe-response = {
  "v": uint,                         ; description format version
  "res": {               ; resource map (not protocol fields)
    * resource-name => {
      "fn": io-type-code,            ; I/O type code
      ? "description": tstr,         ; human-readable description
    },
  },
}

resource-name = tstr
io-type-code = &(
  none:         0,
  run:          1,
  input:        2,
  output:       3,
  input_output: 4,
)

; Single Resource DESCRIBE response (RESOURCE present in request)
single-resource-describe-response = {
  "v":   uint,
  ? "in":  io-descriptor,
  ? "out": io-descriptor,
}

io-descriptor = {
  "value":   pson-value,             ; current/sample data
  ? "schema": json-schema,     ; JSON Schema definition (optional)
}

json-schema = {                      ; subset of JSON Schema keywords
  ? "type":        schema-type,
  ? "properties":  { * tstr => json-schema },
  ? "items":       json-schema,
  ? "description": tstr,
  ? "minimum":     number,
  ? "maximum":     number,
  ? "enum":        [+ any],
  ? "readOnly":    bool,
  ? "writeOnly":   bool,
  ? "default":     any,
  ? "required":    [+ tstr],
}

schema-type = "boolean" / "integer" / "number"
            / "string" / "object" / "array"

; --- Generic types ---

pson-value = any          ; any PSON-encoded value (see PSON)
empty = nil                          ; zero-length body
]]></sourcecode>
</section>

<section anchor="field-requirements-per-message-type"><name>Field Requirements per Message Type</name>
<t>The following table summarizes which fields are required (R), optional (O), conditional (C), or not used (--) for each message type:</t>
<table>
<thead>
<tr>
<th>Message Type</th>
<th>STREAM_ID</th>
<th>PARAMETERS</th>
<th>PAYLOAD</th>
<th>RESOURCE</th>
</tr>
</thead>

<tbody>
<tr>
<td>OK</td>
<td>R</td>
<td>O</td>
<td>O</td>
<td>--</td>
</tr>

<tr>
<td>ERROR</td>
<td>R</td>
<td>O</td>
<td>O</td>
<td>--</td>
</tr>

<tr>
<td>CONNECT</td>
<td>R</td>
<td>O</td>
<td>C</td>
<td>--</td>
</tr>

<tr>
<td>DISCONNECT</td>
<td>--</td>
<td>O</td>
<td>O</td>
<td>--</td>
</tr>

<tr>
<td>KEEP_ALIVE</td>
<td>--</td>
<td>--</td>
<td>--</td>
<td>--</td>
</tr>

<tr>
<td>RUN</td>
<td>R</td>
<td>--</td>
<td>O</td>
<td>R</td>
</tr>

<tr>
<td>DESCRIBE</td>
<td>R</td>
<td>--</td>
<td>--</td>
<td>O</td>
</tr>

<tr>
<td>START_STREAM</td>
<td>R</td>
<td>O</td>
<td>--</td>
<td>R</td>
</tr>

<tr>
<td>STOP_STREAM</td>
<td>R</td>
<td>--</td>
<td>--</td>
<td>--</td>
</tr>

<tr>
<td>STREAM_DATA</td>
<td>R</td>
<td>--</td>
<td>R</td>
<td>--</td>
</tr>
</tbody>
</table><t>C = Conditional: CONNECT PAYLOAD is required for authentication types 0 (Credentials) and 1 (Token). For type 2 (Certificate), PAYLOAD is optional: required when the certificate identifies only the namespace, and may be absent when the certificate identifies both namespace and device.</t>
</section>
</section>

<section anchor="pson-vs-json-size-comparison"><name>PSON vs JSON Size Comparison</name>
<t>For a detailed size comparison between PSON and JSON for typical IoT payloads, see Section 10 of <xref target="PSON"></xref>. In summary, PSON achieves 40-75% size reduction for typical IoT data patterns.</t>
</section>

<section anchor="protocol-comparisons"><name>Protocol Comparisons</name>
<t>Detailed protocol comparisons between IOTMP and other IoT protocols <xref target="MQTT"></xref>, <xref target="RFC7252"></xref>, <xref target="LwM2M"></xref> are available as separate companion documents.</t>
</section>

<section anchor="revision-history"><name>Revision History</name>
<table>
<thead>
<tr>
<th>Version</th>
<th>Date</th>
<th>Changes</th>
</tr>
</thead>

<tbody>
<tr>
<td>0.1</td>
<td>2026-03-30</td>
<td>Initial public draft.</td>
</tr>
</tbody>
</table></section>

</back>

</rfc>
