Modbus Function Code 43: Read Device Identification (FC43 / MEI 14)

The function code that lets a master ask a slave 'what are you?' — the FC43 PDU, the four Read Device ID access modes, conformity levels, and the standard identification objects byte by byte.

OrangeHorse Engineering Team 8 min read

Modbus Function Code 43: Read Device Identification (FC43 / MEI 14)

Every other Modbus function code reads or writes data — coils, registers, bits. Function code 43 does something different: it asks the device "what are you?" The response is a set of ASCII strings — the vendor's name, the product code, the firmware revision — which is why FC43 is the tool a master reaches for when it has to auto-configure against hardware it has never seen before.

FC43 is an encapsulated interface: instead of being one operation, it is a container for a family of operations, each identified by a sub-code called the MEI type. The one worth knowing is MEI type 14, Read Device Identification. This article decodes that one operation byte by byte.

Why the encapsulated interface exists

Function codes are a byte, so there are at most 255 of them, and the common data functions already consume the low range. Rather than burn a dozen function codes on "read this specific metadata," the spec gave FC43 (0x2B) a two-byte inner header: a MEI type that selects which encapsulated operation follows. MEI type 14 is Read Device Identification. (MEI type 13, CANopen General Reference, is the only other defined value you will meet.)

So the mental model is three layers:

  1. Function code 0x2B — "this is an encapsulated operation."
  2. MEI type 0x0E — "specifically, read device identification."
  3. Read Device ID codehow much identification to read: basic, regular, extended, or one named object.

The request PDU

A Read Device Identification request is exactly four bytes:

ByteFieldValue
0Function code0x2B
1MEI type0x0E
2Read Device ID code0x010x04
3Object ID0x00 for stream access, or the object to fetch

The Read Device ID code is the interesting byte. It is not arbitrary — it names one of four access modes:

  • 0x01 Basic — the mandatory minimum every device must answer: VendorName, ProductCode, MajorMinorRevision.
  • 0x02 Regular — everything basic, plus the remaining standard objects the device implements.
  • 0x03 Extended — every identification object the device supports, vendor-defined ones included.
  • 0x04 Individual — fetch one named object, whose ID goes in the Object ID field.

Modes 01–03 are stream access: the request names no specific object, and the device streams back however many objects fit, possibly across several transactions. Mode 04 is individual access: you ask for exactly one object by ID.

The response PDU, and the objects it carries

The response repeats the function code, MEI type, and Read Device ID code, then adds a fixed block followed by the objects themselves:

FieldSizeMeaning
Function code10x2B
MEI type10x0E
Read Device ID code1echoes the request
Conformity level1what the device supports (see below)
More Follows10xFF if more data follows, 0x00 if this is the end
Next Object ID1the object ID to request next (0x00 when More Follows is 0x00)
Number of Objects1how many objects are in this response
Objectsvariable(Object ID, Length, Value) repeated

Each object is a length-prefixed ASCII string: an Object ID byte, a Length byte, then that many bytes of text. The standard object IDs are fixed by the spec:

Object IDName
0x00VendorName
0x01ProductCode
0x02MajorMinorRevision
0x03VendorUrl
0x04ProductName
0x05ModelName
0x06UserApplicationName
0x800xFFvendor-defined

The conformity level encodes two facts in one byte. The low three bits say which access modes the device supports (0x01 basic only, 0x02 regular, 0x03 extended). Setting bit 7 (0x80) says the device also supports individual access. So a device reporting 0x83 supports extended stream access and individual object access; a device reporting 0x01 answers only the basic stream and nothing else. A master uses this to avoid asking for individual objects the device can't serve.

Stream vs individual, and the multi-transaction dance

Stream access has a subtlety worth naming: the object list can be longer than one response frame fits. When that happens the device sets More Follows to 0xFF and places the next object ID in the Next Object ID field. The master issues the request again with Object ID set to that value, and keeps going until More Follows comes back 0x00. In practice, basic identification (three short strings) always fits in one frame, so you only meet the dance on the extended mode of a chatty device.

Individual access skips the dance entirely: you name the object, you get exactly that object, done.

A byte-level decode

The demo below is pure standard-library Rust, because FC43 is a protocol feature — it builds a request for each access mode and parses a mock basic-identification response into named objects:

/// Read Device Identification (FC43) — Encapsulated Interface, MEI type 14.
const FC_READ_DEVICE_ID: u8 = 0x2B;
const MEI_READ_DEVICE_ID: u8 = 0x0E;

/// The four Read Device ID access modes.
#[derive(Debug, Clone, Copy)]
enum Access {
    Basic,      // 0x01 — mandatory minimum: vendor, product, revision
    Regular,    // 0x02 — adds product name, etc.
    Extended,   // 0x03 — every object the device supports
    Individual, // 0x04 — fetch one named object by ID
}

impl Access {
    fn code(self) -> u8 {
        match self {
            Access::Basic => 0x01,
            Access::Regular => 0x02,
            Access::Extended => 0x03,
            Access::Individual => 0x04,
        }
    }
}

/// Standard identification objects, by object ID.
const OBJECT_NAMES: [(u8, &str); 7] = [
    (0x00, "VendorName"),
    (0x01, "ProductCode"),
    (0x02, "MajorMinorRevision"),
    (0x03, "VendorUrl"),
    (0x04, "ProductName"),
    (0x05, "ModelName"),
    (0x06, "UserApplicationName"),
];

fn object_name(id: u8) -> String {
    OBJECT_NAMES
        .iter()
        .find(|(i, _)| *i == id)
        .map(|(_, n)| n.to_string())
        .unwrap_or_else(|| format!("object 0x{id:02X}"))
}

/// Build a Read Device Identification request PDU.
/// Stream access (01–03) uses object ID 0 for the first transaction;
/// individual access (04) names the object directly.
fn build_request(access: Access, object_id: u8) -> [u8; 4] {
    [FC_READ_DEVICE_ID, MEI_READ_DEVICE_ID, access.code(), object_id]
}

/// Parse a response PDU into (object ID, value) pairs.
fn parse_response(pdu: &[u8]) -> Vec<(u8, String)> {
    // pdu: FC | MEI | read code | conformity | more follows | next obj | count
    let count = pdu[6] as usize;
    let mut objects = Vec::with_capacity(count);
    let mut i = 7;
    for _ in 0..count {
        let id = pdu[i];
        let len = pdu[i + 1] as usize;
        let value = String::from_utf8_lossy(&pdu[i + 2..i + 2 + len]).to_string();
        objects.push((id, value));
        i += 2 + len;
    }
    objects
}

fn main() {
    // Each access mode differs only in the Read Device ID code byte.
    for access in [Access::Basic, Access::Regular, Access::Extended, Access::Individual] {
        let request = build_request(access, 0);
        println!("request PDU ({access:?}): {:02X?}", request);
    }
    println!();

    // A simulated basic-identification response from a device.
    let response: &[u8] = &[
        0x2B, 0x0E, // FC43 + MEI 14
        0x01,       // Read Device ID code: basic
        0x01,       // conformity level: basic
        0x00, 0x00, // more follows: no; next object id: 0
        0x03,       // number of objects
        0x00, 0x0B, b'O', b'r', b'a', b'n', b'g', b'e', b'H', b'o', b'r', b's', b'e',
        0x01, 0x08, b'O', b'H', b'T', b'S', b'1', b'0', b'2', b'0',
        0x02, 0x03, b'1', b'.', b'2',
    ];

    println!("response objects:");
    for (id, value) in parse_response(response) {
        println!("  {} = {value}", object_name(id));
    }
}

Run it:

request PDU (Basic): [2B, 0E, 01, 00]
request PDU (Regular): [2B, 0E, 02, 00]
request PDU (Extended): [2B, 0E, 03, 00]
request PDU (Individual): [2B, 0E, 04, 00]

response objects:
  VendorName = OrangeHorse
  ProductCode = OHTS1020
  MajorMinorRevision = 1.2

Read the response bytes against the table and every field lines up: 2B 0E is the FC43/MEI-14 header, 01 echoes the basic access code, 01 is the conformity level (basic only, no individual), 00 00 is "no more follows, no next object," 03 is three objects — then (00, 0B, "OrangeHorse"), (01, 08, "OHTS1020"), and (02, 03, "1.2"). The off-by-zero trap that trips people on register addressing does not appear here: object IDs are literal, not 1-based.

Does oms-modbus support FC43?

Objectively: not in v0.2.0. oms-modbus implements the eleven workhorse function codes — FC01–06, FC08, FC15–16, and FC22–23 — because those cover reading and writing coils and registers over TCP, RTU, and ASCII. FC43 is not among them, so there is no read_device_identification helper and no FC43 codec in the library today.

That is a meaningful gap if your master must auto-configure against unknown hardware, and it is exactly the kind of thing to weigh when comparing Modbus Rust libraries. The request and response formats above are the stable, spec-defined shape, so the work to add FC43 is a self-contained codec plus a method on ModbusClient — not a redesign. But as of this writing, a master that needs FC43 must assemble and parse the PDU itself, as the demo above does.

What to remember

  • FC43 (0x2B) is an encapsulated interface; MEI type 0x0E is Read Device Identification.
  • The Read Device ID code selects the mode: 0x01 basic, 0x02 regular, 0x03 extended, 0x04 individual.
  • Stream access (01–03) returns (Object ID, Length, Value) triplets; individual access (04) fetches one named object.
  • The conformity level encodes both supported access modes and (bit 7) individual-access support.
  • More Follows = 0xFF means the stream is not done — re-request with the Next Object ID.
  • oms-modbus v0.2.0 does not implement FC43; a master that needs it must build and parse the PDU itself.

This article is part of the oms-modbus tutorial series. oms-modbus is a transport-generic Modbus library in Rust — TCP, RTU, and ASCII through one API — used at OrangeHorse to build diagnostic tools for our Modbus-enabled sensors. MIT/Apache-2.0, on crates.io.

Read next: Modbus Register Addressing — the 40001 puzzle and the off-by-one trap. Modbus Function Codes Reference — the eleven codes oms-modbus does implement.

modbus modbus function code 43 modbus read device identification modbus fc43 modbus device id modbus vendor name rust iiot