Chapter 15 — SNIP (Simple Node Information Protocol)

The Simple Node Information Protocol provides a lightweight way to identify a node without reading its full CDI. A SNIP reply packs manufacturer identification and user-configurable name/description into a single addressed message (up to 256 bytes).

Source files: src/openlcb/protocol_snip.c, src/openlcb/protocol_snip.h

15.1 Request / Reply Exchange

sequenceDiagram participant Requester participant Node Requester->>Node: Simple Node Info Request (MTI 0x0DE8) Note over Node: Build 8-field reply Node-->>Requester: Simple Node Info Reply (MTI 0x0A08)

The handler ProtocolSnip_handle_simple_node_info_request() builds the reply and marks it valid. The incoming reply handler ProtocolSnip_handle_simple_node_info_reply() is a no-op (no automatic response is generated).

15.2 Reply Payload Format (8 Fields)

The SNIP reply contains exactly 8 fields assembled sequentially. Each string field is null-terminated. The total maximum payload is LEN_MESSAGE_BYTES_SNIP (256 bytes).

#FieldTypeMax SizeSource
1Manufacturer Version1 byte1snip.mfg_version (node params)
2Manufacturer Namestring + null41snip.name (node params)
3Modelstring + null41snip.model (node params)
4Hardware Versionstring + null21snip.hardware_version (node params)
5Software Versionstring + null21snip.software_version (node params)
6User Version1 byte1snip.user_version (node params)
7User Namestring + null63Configuration memory (ACDI User space)
8User Descriptionstring + null64Configuration memory (ACDI User space)
Total structure size: LEN_SNIP_STRUCTURE = 264 bytes (includes all max lengths). The wire payload is variable-length depending on actual string lengths.

15.3 Manufacturer vs. User Fields

Fields 1-5 (manufacturer section) come from the node's parameters->snip structure, which is typically stored in flash/ROM. These are read-only and set at compile time:

typedef struct {
    uint8_t mfg_version;                                     // Always 1
    char name[LEN_SNIP_NAME_BUFFER];                         // 41 bytes
    char model[LEN_SNIP_MODEL_BUFFER];                       // 41 bytes
    char hardware_version[LEN_SNIP_HARDWARE_VERSION_BUFFER]; // 21 bytes
    char software_version[LEN_SNIP_SOFTWARE_VERSION_BUFFER]; // 21 bytes
    uint8_t user_version;                                    // Always 1
} user_snip_struct_t;

Fields 7-8 (user section) are read at runtime from configuration memory via the config_memory_read callback. The addresses are defined by the application:

If the node's address_space_config_memory.low_address_valid flag is set, the low address offset is added to the base address, allowing per-node offsets in a shared configuration space.

15.4 Field Loader Functions

Each of the 8 fields has a dedicated public loader function. All share the same signature pattern:

uint16_t ProtocolSnip_load_<field>(
    openlcb_node_t *openlcb_node,
    openlcb_msg_t  *outgoing_msg,
    uint16_t        offset,           // current payload offset
    uint16_t        requested_bytes   // max bytes to write
);
// Returns: updated offset after writing
Loader FunctionField
ProtocolSnip_load_manufacturer_version_id()Mfg version byte
ProtocolSnip_load_name()Manufacturer name
ProtocolSnip_load_model()Model name
ProtocolSnip_load_hardware_version()Hardware version
ProtocolSnip_load_software_version()Software version
ProtocolSnip_load_user_version_id()User version byte
ProtocolSnip_load_user_name()User name (from config memory)
ProtocolSnip_load_user_description()User description (from config memory)

The internal helper _process_snip_string() handles string copying with truncation. It clamps the string length to the buffer maximum, copies the characters, and always appends a null terminator.

15.5 Reply Assembly Flow

flowchart TD A["SNIP Request received"] --> B["Load outgoing header\n(source, dest, MTI 0x0A08)"] B --> C["Load mfg_version (1 byte)"] C --> D["Load name (string + null)"] D --> E["Load model (string + null)"] E --> F["Load hardware_version (string + null)"] F --> G["Load software_version (string + null)"] G --> H["Load user_version (1 byte)"] H --> I["Load user_name\n(config memory read + null)"] I --> J["Load user_description\n(config memory read + null)"] J --> K["Mark outgoing valid"] style A fill:#4a90d9,color:#fff style K fill:#27ae60,color:#fff

Each loader function advances the payload_offset and increments outgoing_msg->payload_count. The loaders are called sequentially so the payload is built in wire order.

15.6 Validation: Exactly 6 Null Terminators

ProtocolSnip_validate_snip_reply() verifies an incoming SNIP reply is well-formed. The three checks are:

  1. Length check: payload_count must not exceed LEN_MESSAGE_BYTES_SNIP (256).
  2. MTI check: must be MTI_SIMPLE_NODE_INFO_REPLY (0x0A08).
  3. Null count: the payload must contain exactly 6 null bytes (0x00).

The 6 null bytes correspond to the 6 null terminators expected from the string fields (fields 2-5 and 7-8). The two version bytes (fields 1 and 6) are not null-terminated. The null count is computed by OpenLcbUtilities_count_nulls_in_openlcb_payload().

Why exactly 6? The SNIP reply has 6 string fields, each with a null terminator. If a string is empty, it contributes a single null byte. The version bytes are not counted. Any other null count indicates a malformed or truncated reply.

15.7 Callback Interface

The SNIP handler requires a single callback:

typedef struct {
    uint16_t (*config_memory_read)(
        openlcb_node_t *openlcb_node,
        uint32_t address,
        uint16_t count,
        configuration_memory_buffer_t *buffer
    );  // REQUIRED
} interface_openlcb_protocol_snip_t;

The config_memory_read callback is used to fetch user name and user description from configuration memory. If this callback is NULL, the user fields are written as empty strings (single null byte each).