MBMap Registry

Specification

.mbmap — Modbus Map format, version 2

A .mbmap.json file describes the Modbus register map of one device model: who makes it, how to talk to it, and what every register means. It is a JSON document, so any language can read it with its standard JSON parser, and a JSON Schema describes its structure.

The key words MUST, SHOULD and MAY are used as in RFC 2119.

1. Example

{
  "$schema": "https://raw.githubusercontent.com/nobrega8/mbmap-library/main/schema/mbmap-2.schema.json",
  "mbmap": 2,
  "vendor": "Example Corp",
  "model": "EM-100",
  "description": "Single-phase energy meter",
  "protocol": "rtu",
  "unit_id": 1,
  "baudrate": 9600,
  "parity": "E",
  "stopbits": 1,
  "byte_order": "big",
  "word_order": "big",
  "address_base": 0,
  "enums": {
    "relay": {"0": "Open", "1": "Closed"}
  },
  "registers": [
    {"name": "Voltage", "group": "Measurements", "area": "input", "address": 0, "type": "float32", "unit": "V", "description": "RMS voltage"},
    {"name": "Current", "group": "Measurements", "area": "input", "address": 6, "type": "float32", "unit": "A", "description": "RMS current"},
    {"name": "Active energy", "group": "Energy", "area": "holding", "address": 256, "type": "uint32", "scale": 0.01, "unit": "kWh", "word_order": "little"},
    {"name": "Relay state", "group": "Status", "area": "holding", "address": 512, "type": "bool", "bit": 3, "enum": "relay", "description": "Bit 3 of the status word"},
    {"name": "Reset energy", "group": "Commands", "area": "coil", "address": 0, "type": "bool", "access": "W", "description": "Write 1 to reset counters"},
    {"name": "Serial number", "group": "Info", "area": "holding", "address": 1024, "type": "string", "length": 8, "description": "16 ASCII characters"}
  ]
}

A complete example is in examples/example-corp/em-100.mbmap.json.

2. File

  • A map is one JSON object (RFC 8259), encoded as UTF-8. Readers MUST accept and ignore a leading BOM. Writers SHOULD NOT write one.
  • The file name ends in .mbmap.json.
  • An object MUST NOT repeat a key. JSON parsers handle duplicate keys differently, so a file with one is invalid.
  • Strings are used as written: there is no trimming, and enumerated values are case-sensitive ("holding", not "Holding").
  • JSON null is not a valid value for any key; leave the key out instead.
  • JSON has no comments. Notes about the whole file go in notes, a note about one register in its comment. Both are free text that tools MAY show and MUST NOT interpret.
  • Writers SHOULD use the layout of §1: two-space indentation, and one line per register and per enums/sentinels table, so that a change to one register is a one-line diff.
  • $schema is optional and points editors to the JSON Schema. Readers MUST ignore it.

2.1 Integers

Where a value is an integer, it MAY be written as a JSON integer (1040) or as a string holding a decimal integer or 0x hex ("0x0410"). Strings keep the hex spelling that manuals use for addresses and identifiers, and carry 64-bit values that not every JSON parser reads exactly as numbers (beyond 2^53). Keys of enums and sentinels tables are always strings, in the same two forms.

2.2 Tables

A table maps raw values to labels: an object whose keys are integers written as strings (§2.1) and whose values are non-empty labels, e.g. {"0": "Stopped", "1": "Idle", "0xFFFF": "Unimplemented"}. A table has at least one entry, and two keys MUST NOT stand for the same value ("1" and "0x1").

3. Top-level keys

mbmap, vendor, model and registers are required. Every other key is optional; when it is missing, the default applies.

KeyJSON typeDefaultMeaning
mbmapinteger 2Format version (§3.1, §8)
vendornon-empty stringManufacturer
modelnon-empty stringModel or model family, as shown to people
modelsarray of non-empty strings[model]Every model name or code the map applies to (§3.2)
vendor_websitestring: a domain or an http(s):// URLManufacturer's website, e.g. se.com or https://www.se.com
descriptionstringShort description
revisioninteger ≥ 1Revision of this map's content (§3.1)
revisedstring YYYY-MM-DDDate of that revision
firmwarestringFree text about the firmware the map applies to
firmware_minversion string (§3.2)Oldest firmware the map applies to
firmware_maxversion string (§3.2)Newest firmware the map applies to
sourcestringFree-text summary of where the map comes from
sourcesarray of source objectsDocuments the map was built or checked from (§3.3)
notesarray of stringsFree notes about the file
protocol"tcp", "rtu", "tcp+rtu""tcp+rtu"Transports the device supports
unit_idinteger 0–2551Default unit / slave id
portinteger 1–65535502Default TCP port
baudrateinteger9600Default RTU baud rate
parity"N", "E", "O""E"Default RTU parity
databits7, 88Default RTU data bits
stopbits1, 21Default RTU stop bits
byte_order"big", "little""big"Default byte order inside each register (§6)
word_order"big", "little""big"Default order of the registers of a multi-register value (§6)
address_base0, 10Whether address is 0-based or 1-based (§5)
max_read_registersinteger 1–125125Largest block the device accepts per read (FC03/04)
max_read_bitsinteger 1–20002000Largest block the device accepts per read (FC01/02)
enumsobject: name → tableNamed translation tables (§4.1). Names use a–z, 0–9 and _
sentinelsobject: type → tableReserved raw values per integer type (§4.2)
identifyobjectHow to read the device identity with Report Slave ID (FC17) or Read Device Identification (FC43) (§4.8)
registersarray of register objectsThe registers (§4)

Connection values are defaults: the user can still change them when connecting.

3.1 Format version and map revision

Two numbers describe a map, and they change for different reasons:

  • mbmap, the format version, says which version of this specification the file follows, so a reader knows whether it can read it (§8). It is the same for every map written to this specification and changes only when the format changes.
  • revision, the map revision, says which edition of this map's content the file is. It starts at 1 and SHOULD increase by one with every published change that affects how a device is read or written: a register, a connection default, a table, a firmware range or a model. It MAY stay the same for changes to free text only (description, notes, comment, source). revised gives the date of that change.

A tool that has two copies of the same map (the same vendor and model) SHOULD treat the higher revision as the newer one, and MAY offer the newer one to the user. A map without revision cannot be compared this way. The revision is not the firmware version of the device: firmware belongs in §3.2.

3.2 Models and firmware

A map applies to the models in models, or to model alone when models is missing. model is the name shown to people ("EKMBPP1 / EKMBPP1A"); models lists each name or code a device can report, so a tool can compare it with the connected device (§9):

"model": "EKMBPP1 / EKMBPP1A",
"models": ["EKMBPP1", "EKMBPP1A"]

firmware_min and firmware_max say which firmware versions the map applies to. Both are optional: without them the map applies to every firmware, which is also the right choice when the version is unknown. firmware stays available for anything a range cannot say ("2.x, except 2.3 beta").

A version string is one or more whole numbers separated by dots: "2", "2.1", "1.02.7". Components are compared as numbers from the left ("1.10" is newer than "1.9", and "1.02" is the same as "1.2"), so write versions the way the device reports them. A bound covers every version that starts with it:

  • firmware_min "2.1" includes 2.1, 2.1.4 and 3.0, but not 2.0.9;
  • firmware_max "2" includes every 2.x and 2.x.y, but not 3.0; firmware_max "2.3" includes 2.3.7 but not 2.4.

So "firmware_min": "1", "firmware_max": "2" reads "firmware 1.x and 2.x". firmware_min MUST NOT be after firmware_max.

Registers that depend on the firmware. A register can carry its own firmware_min and firmware_max, with the same meaning, when it exists only in some firmware versions. This keeps one file for firmware revisions that differ in a few registers. A register without them applies wherever the map applies. Two registers may share a name (and an address) when their firmware ranges do not overlap: the tool uses the one that matches the device.

Example: firmware 2.1 moved the energy counter and added a power factor register:

"firmware_min": "1",
"firmware_max": "2",
"registers": [
  {"name": "Firmware version", "area": "holding", "address": 0, "type": "string", "length": 4, "role": "firmware"},
  {"name": "Voltage", "area": "input", "address": 10, "type": "float32", "unit": "V"},
  {"name": "Active energy", "area": "input", "address": 20, "type": "uint32", "unit": "Wh", "firmware_max": "2.0"},
  {"name": "Active energy", "area": "input", "address": 40, "type": "uint64", "unit": "Wh", "firmware_min": "2.1"},
  {"name": "Power factor", "area": "input", "address": 30, "type": "int16", "scale": 0.001, "firmware_min": "2.1"}
]

When the differences are large (most addresses change, or the connection settings do), separate files are clearer: one map per firmware range, each with its own firmware_min and firmware_max.

3.3 Sources

sources records the documents a map was built from or checked against, so a reader can find them and tell which edition was used. Each item is an object:

KeyJSON typeMeaning
titlenon-empty string, requiredDocument title, e.g. "EM530/EM540 Modbus communication protocol"
versionstringEdition or revision of the document, as printed on it
datestring YYYY, YYYY-MM or YYYY-MM-DDPublication date of that edition
urlhttp(s):// URLWhere the document can be found
notestringWhich part was used, or how the map was checked (e.g. "tables 4.1-4.24", "tested on a device with firmware 2.1")
"sources": [
  {"title": "EM530/EM540 Modbus communication protocol", "version": "1.3", "date": "2024-02-13", "url": "https://www.gavazziautomation.com/...", "note": "tables 4.1-4.24"}
]

source remains a one-line, free-text summary for people and for maps whose sources are not known in this detail. A map MAY have both.

4. Registers

Each item of registers is an object. name, area, address and type are required; every other key is optional, and the default applies when it is missing.

KeyJSON typeDefaultMeaning
namenon-empty stringVariable name. SHOULD be unique in the file
groupstringFree grouping for display (e.g. Measurements)
area"coil", "discrete", "input", "holding"Modbus data area (§5)
addressinteger (§2.1)First register / bit address (§5)
typesee §6Data type
lengthinteger ≥ 1Number of registers. Required for string, ignored otherwise
bitinteger 0–15For bool in input/holding: which bit of the register, 0 = least significant. For a uint16 bit field: its lowest bit
bitsinteger 1–16For uint16 in input/holding: width of a bit field starting at bit (§4.3)
scalenumber1Multiplier
scale_factorstring: a register nameRegister whose integer value is a power-of-ten exponent for this one (§4.4)
offsetnumber0Added after scaling
unitstringEngineering unit (V, kWh, °C…)
access"R", "W", "RW""R"Allowed operations
byte_order"big", "little"file byte_orderOverrides the file default for this register (§6)
word_order"big", "little"file word_orderOverrides the file default for this register (§6)
write_minnumberSmallest value accepted when writing, in engineering units (§4.5)
write_maxnumberLargest value accepted when writing, in engineering units (§4.5)
enumstring (a name in enums) or tableLabels for integer or bool values (§4.1)
referencesee §4.6Raw value this register is expected to hold on a healthy device
firmware_minversion string (§3.2)Oldest firmware that has this register
firmware_maxversion string (§3.2)Newest firmware that has this register
role"model", "firmware", "serial"This register identifies the device (§4.7)
read_alonebooleanfalseThe register is read in a request of its own (§4.9)
read_groupnon-empty stringThe register is read only together with registers of the same group (§4.9)
descriptionstringFree text
commentstringFree note about the register, for people editing the map

Engineering value = raw × 10^scale_factor × scale + offset (without a scale_factor: raw × scale + offset). When writing, a tool applies the inverse and rounds to the nearest integer for integer types.

4.1 Translation tables

Many registers hold a number that stands for a state: 0 is stopped, 1 is idle, 2 is starting, and so on. enum turns those numbers into labels. It is either:

  • a table written in the register: "enum": {"0": "Stopped", "1": "Idle", "2": "Starting"};
  • the name of a table in the top-level enums, so the same list is written once for every register that uses it:
"enums": {
  "run_state": {"0": "Stopped", "1": "Idle", "2": "Starting", "3": "Running", "4": "Fault"}
},
"registers": [
  {"name": "Pump 1 state", "area": "holding", "address": 100, "type": "uint16", "enum": "run_state"},
  {"name": "Pump 2 state", "area": "holding", "address": 101, "type": "uint16", "enum": "run_state"}
]

Values are compared with the raw value, before scale and offset. A value that is not in the table is shown as a plain number. Tools SHOULD offer the labels when writing.

4.2 Sentinel values

Some devices answer with a reserved raw value instead of a reading: one value means "not implemented", another "over range", another "sensor fault". sentinels declares them for every register of one type:

"sentinels": {
  "uint16": {"0xFFFF": "Unimplemented", "0xFFFE": "Over range", "0xFFFD": "Under range"},
  "int16": {"0x7FFF": "Unimplemented", "0x7FFE": "Over range", "0x7FFD": "Under range"},
  "uint32": {"0xFFFFFFFF": "Unimplemented"}
}
  • The keys of sentinels are int16, uint16, int32, uint32, int64, uint64. A table applies to every register of that type.
  • Values MUST fit the type, and are compared with the raw value, before scale and offset.
  • When a reading matches, tools SHOULD show the label instead of a number and SHOULD NOT treat it as a measurement (in charts, limits or averages).
  • An enum label for the same raw value takes precedence over the sentinel.

4.3 Bit fields

Some devices pack several small values into one register, for example four 4-bit alarm codes. A uint16 register with bit and bits reads one of them: raw value = (register >> bit) & (2^bits − 1). bit + bits MUST NOT exceed 16.

{"name": "Alarm A", "area": "holding", "address": 100, "type": "uint16", "bit": 12, "bits": 4, "enum": "alarm_condition"},
{"name": "Alarm B", "area": "holding", "address": 100, "type": "uint16", "bit": 8, "bits": 4, "enum": "alarm_condition"}

Registers that read different fields of the same register do not overlap. Writing a field SHOULD read the register, change only the field's bits and write it back.

4.4 Scale factors

Some devices, notably SunSpec inverters and meters, send a separate exponent register for a value instead of a fixed scale: the value is raw × 10^exponent, and the exponent can change with the device's range. scale_factor names that exponent register:

{"name": "AC power", "area": "holding", "address": 40083, "type": "int16", "unit": "W", "scale_factor": "W_SF"},
{"name": "W_SF", "area": "holding", "address": 40084, "type": "int16"}
  • The named register MUST be a register of the same file with an integer type (int16 … uint64) and no bits, and MUST NOT itself have a scale_factor. If several registers share the name, the first one counts.
  • Only numeric types can have a scale_factor (not bool or string).
  • A tool reads the exponent register together with the value, also when the user chose not to show it. If the exponent cannot be read, or is a sentinel (§4.2, e.g. SunSpec's -32768 = not implemented), the value is not available.
  • When writing, a tool reads the exponent first and applies the inverse.

4.5 Write limits

write_min and write_max declare which values a register accepts when written, so a tool can reject a bad value before it reaches the device.

  • Both are numbers in engineering units: the value the user sees, after scale_factor, scale and offset are applied. A limit of 0.5 on a register with scale 0.1 means a raw value of 5.
  • Either may be left out: a missing limit leaves that side open. With both missing, a register has no declared limit (the device decides).
  • write_min MUST NOT be greater than write_max. The limits are inclusive.
  • A tool MUST NOT send a write whose engineering value is below write_min or above write_max, and SHOULD tell the user which limit was exceeded.
  • They apply to the value the user asks for, before it is converted to a raw value and rounded. A value inside the limits that still does not fit the register's type is refused by the normal type checks.
  • They have no meaning for bool and string registers or for registers that cannot be written (access "R"), and are ignored there.
{"name": "Setpoint", "area": "holding", "address": 100, "type": "uint16", "scale": 0.1, "unit": "°C", "access": "RW", "write_min": 5, "write_max": 30}

4.6 Reference values

reference gives a value that a healthy device is known to hold in this register (a model number, a fixed protocol identifier, a manufacturer code). A tool can read the register and compare it with the reference to detect a wrong address, base, byte order or data type before the user trusts the other values. Not every device has such a register, so most registers have no reference.

  • It is the raw value, before scale_factor, scale and offset, in the JSON type of that value: an integer (§2.1) for the integer types and for bit fields, a number for float32 and float64, true / false for bool, a string for string. It MUST fit the type (and the width of a bit field, or 2 × length characters for a string).
  • The check is the register itself: a tool reads its area and address (already adjusted for address_base) and decodes its type with its byte and word order.
  • A map can have any number of registers with a reference.
  • Outcome of each check: match (the decoded raw value equals the reference), mismatch (it differs), failed (the read returned an error or timed out, so nothing is known), not checked (the register was not read). A tool SHOULD report them separately: a mismatch points to a wrong map or settings, a failed read to a connection or addressing problem.
  • Floats are compared at the type's own precision (a float32 reference is compared as a 32-bit float). Strings are compared after trimming trailing NUL and space characters.
  • A reference only supports a diagnosis. Several byte or word orders can decode the same bytes to the same value (a small number or a palindrome reads the same in more than one order), so a match does not prove the whole map is correct. Maps SHOULD use a distinctive value and SHOULD give more than one where the device has them.
  • A reference on a register that cannot be read (access "W") is an error.
{"name": "Manufacturer code", "area": "holding", "address": 768, "type": "uint16", "reference": "0x016A"},
{"name": "Serial prefix", "area": "holding", "address": 770, "type": "uint32", "word_order": "little", "reference": "0x00010002"}

4.7 Identity registers

role marks the registers that tell which device is connected, so a tool can check the map against it (§9):

  • "model": the device's model name or code. Its value, or its enum label when it has one, is compared with models. A numeric model code is best given a label: "enum": {"0x016A": "EM-100"}.
  • "firmware": the firmware version. A string register is read as text, and the first version string in it is used ("V2.01b" gives 2.01). A number gives its engineering value (102 with scale 0.01 gives 1.02). When several registers have this role, their values are joined with dots in the order of the file, which fits devices that keep the major and minor versions apart, for example as two bit fields of one register ("bit": 8, "bits": 8 and "bit": 0, "bits": 8).
  • "serial": the serial number. Tools MAY show it; it is never compared.

At most one register MAY have the role "model" and at most one "serial". A register with a role MUST be readable. Registers with a role are ordinary registers otherwise: they are read and shown like the others, and can also have a reference.

4.8 Identity from the device (FC17, FC43)

Some devices report what they are with a function of their own and have no identity register. identify says which function to send and where the identity is in the answer, so a tool can check the map as it does with the registers of §4.7 (§9). A map has at most one identify.

"enums": {"type_codes": {"0x41": "DMG610", "0x57": "DMG620"}},
"identify": {
  "function": 17,
  "fields": [
    {"name": "Type", "offset": 0, "type": "uint8", "role": "model", "enum": "type_codes"},
    {"name": "Software revision", "offset": 1, "type": "uint8", "role": "firmware"},
    {"name": "Hardware revision", "offset": 2, "type": "uint8"}
  ]
}
"identify": {
  "function": 43,
  "fields": [
    {"name": "Vendor name", "object": 0},
    {"name": "Product code", "object": 1, "role": "model"},
    {"name": "Revision", "object": 2, "role": "firmware"}
  ]
}
KeyJSON typeDefaultMeaning
function17 or 4317: Report Slave ID. 43: Read Device Identification (encapsulated interface transport, MEI type 0x0E)
fieldsnon-empty array of field objectsThe values to take from the answer

Each field is an object. name is always required. role and the keys below depend on function.

KeyJSON typeDefaultMeaning
namenon-empty stringName shown to people
role"model", "firmware", "serial"Meaning of the value, as for registers (§4.7). At most one "model" and one "serial"
offsetinteger ≥ 0FC17 only, required. Position of the field, in bytes, in the data of the answer. Offset 0 is the first byte after the byte count, so the data are the bytes a device lists after "byte count" in its manual
type"uint8", "uint16", "string"FC17 only, required. uint16 is big-endian. string is ASCII, with trailing NUL and space characters trimmed
lengthinteger ≥ 1FC17 only. Length in bytes. Required for string
enumtable name or inline tableFC17 only. Translates a number into a label (§4.1). A model code is best given a label, which is then compared with models
objectinteger 0–255FC43 only, required. Object id: 0x00 VendorName, 0x01 ProductCode, 0x02 MajorMinorRevision, 0x03 VendorUrl, 0x04 ProductName, 0x05 ModelName, 0x06 UserApplicationName, 0x80–0xFF defined by the device
  • FC43: the value of an object is an ASCII string, with trailing NUL and space characters trimmed. A tool asks for the access level (basic, regular or extended) that includes every object the fields name, and follows the "more follows" flag until it has them. For "firmware" the first version string in the text is used, as for a string register (§4.7).
  • Both functions: a device answers on a unit id, not on a register address, so address_base does not apply.
  • No value: an answer that is shorter than a field needs, an exception answer, a missing object, or a device that does not support the function gives no value. The check is unknown (§9), not a mismatch.
  • Several sources: identify and registers with a role can both be present. When both give a value for the same role, a tool SHOULD use the registers and MAY use identify for the roles that no register gives.

4.9 Reading in separate requests

A reader normally joins neighbouring registers into one request. Some devices refuse a request that runs across the border between two internal tables, even though every register in it exists. Two optional keys limit how registers are joined:

  • read_alone: true means the register is read in a request that holds no other register. Bit fields and other registers that occupy the same words count as the same register.
  • read_group: a name. Registers with the same name MAY be read in one request, and a request MUST NOT hold registers of different groups or a register without a group. Registers without read_group are joined as before.
{"name": "Digital input status", "area": "holding", "address": 768, "type": "uint16", "read_group": "status"},
{"name": "Active tariff", "area": "holding", "address": 769, "type": "uint16", "read_group": "status"},
{"name": "Firmware", "area": "holding", "address": 770, "type": "uint16", "read_alone": true}

When both keys are present, read_alone wins. The keys only limit joining: a reader is free to make more requests than needed, and max_read_registers and max_read_bits still apply. They affect only how the registers are requested, not what they mean, so a reader that does not know them reads the same registers, joined as it always did. That is why they need no new mbmap version (§8): on a device that refuses the joined request, an older reader fails exactly as it would without the keys.

5. Areas and addresses

areaUnitReadWriteAllowed access
coilbitFC01FC05 / FC15R, W, RW
discretebitFC02—R
input16-bit registerFC04—R
holding16-bit registerFC03FC06 / FC16R, W, RW

With address_base 0, address is the address sent on the wire (0–65535). With address_base 1, the address on the wire is address − 1 (1–65536), which matches manuals that number registers from 1.

The 5- and 6-digit "Modicon" notation (40001, 300010) is not valid in address. Write it as an area plus a number: 40001 becomes "area": "holding", "address": 1, with "address_base": 1.

6. Types, byte order and word order

typeRegistersNotes
bool—In coil/discrete: one bit. In input/holding: one bit of a register, so bit is required
int16, uint161
int32, uint32, float322Floats are IEEE 754
int64, uint64, float644
stringlength2 ASCII characters per register. Trailing NUL and space characters are trimmed when reading

Two independent settings say how the bytes of a value arrive:

  • byte_order, inside each 16-bit register: "big" sends the high byte first (the Modbus standard), "little" the low byte first.
  • word_order, across the registers of a 32-bit or 64-bit value: "big" puts the most significant register first (at the lowest address), "little" the least significant register first.

The top-level keys set the default for the file; a register's byte_order and word_order each override it on their own (a register that sets only "word_order": "little" keeps the file's byte order).

Typebyte_orderword_order
boolnot usednot used
int16, uint16swaps the two bytes of the registernot used (one register)
int32, uint32, float32swaps the two bytes of each registerdecides which of the 2 registers is the high word
int64, uint64, float64swaps the two bytes of each register"little" reverses the 4 registers as a whole
string"big": the first byte received is the first character; "little": the second byte received is the first characternot used (registers are read in address order)

For 64-bit values, little word order reverses the whole sequence of four registers; it does not swap two 32-bit halves separately.

Wire order of the 32-bit value 0x0A0B0C0D:

byte_orderword_orderBytes on the wireCommon name
bigbig0A 0B 0C 0DModbus standard
biglittle0C 0D 0A 0B"word swap", used by many meters and PLCs
littlebig0B 0A 0D 0C"byte swap" (rare)
littlelittle0D 0C 0B 0Alittle-endian

7. Validation

Readers SHOULD report every problem with one of the codes below and a JSON Pointer to the value (/registers/3/address, /enums/relay), so tools agree on what is wrong. Problems that belong to the whole file have no pointer. An error makes the file invalid. A warning is reported, but the file is still usable.

CodeSeverityWhen
not-utf8errorThe file is not valid UTF-8
not-jsonerrorThe file is not one valid JSON object, or repeats a key in an object. Reported for the whole file: parsers place syntax errors at different positions, so the position goes in the message
bad-json-typeerrorA known key holds the wrong JSON type (a string where a number is expected, null, …)
missing-keyerrormbmap, vendor, model or registers is missing (for the whole file), or a register lacks name, area, address or type, or has it empty (at the register)
bad-meta-valueerrorA top-level key has a value outside its allowed set or range, including mbmap other than 2
bad-areaerrorUnknown area, or not written in lowercase
bad-typeerrorUnknown type, or not written in lowercase
type-areaerrorA type other than bool in coil or discrete
bad-addresserroraddress is not an integer
address-rangeerrorThe address is below address_base, or the value goes beyond address 65535
missing-biterrorbool in input/holding without bit
bad-biterrorbit outside 0–15
missing-lengtherrorstring without length
bad-lengtherrorlength is not an integer ≥ 1
bad-accesserrorUnknown access (or not in uppercase), or not allowed for the area
bad-byte-ordererrorA register's byte_order or word_order is not "big" or "little"
bad-enumerrorAn enum table (§2.2) has a key that is not an integer, an empty label, a repeated value or no entries, or an enums name is not a–z, 0–9, _
bad-sentinelerrorsentinels names an unsupported type, or a table is not valid (§2.2) or has a value that does not fit the type
bad-bitserrorbits is not an integer from 1 to 16, is used on a type other than uint16 or outside input/holding, has no bit, or bit + bits exceeds 16
unknown-enumerrorenum names a table that enums does not define
bad-write-limiterrorwrite_min is greater than write_max
bad-referenceerrorreference is not a valid raw value for the register's type (not an integer, outside the type's range, wider than the bit field, not a boolean for bool, not a finite number for floats, longer than the string), or is set on a register that cannot be read
unknown-scale-factorerrorscale_factor names no register of the file (registers with errors do not count)
bad-scale-factorerrorscale_factor is set on a bool or string, or names a register that is not a whole integer (a float, bool, string or bit field) or that has a scale_factor itself
bad-versionerrorfirmware_min or firmware_max, in the file or a register, is not a version string (§3.2)
bad-firmware-rangeerrorfirmware_min is after firmware_max, in the file or a register
bad-sourceerrorAn item of sources is not an object with a non-empty title, or has a version, date, url or note of the wrong form
bad-roleerrorrole is not "model", "firmware" or "serial", is set on a register that cannot be read, or "model" or "serial" is used more than once
bad-identifyerroridentify is not valid (§4.8): function is not 17 or 43, fields is empty, or a field lacks a name, has a role that is unknown or repeated for "model" or "serial", or breaks the rules of its function (FC17: offset missing or negative, type missing or unknown, string without length, an enum that names no table or is not a valid table, an object; FC43: object missing or outside 0–255, an offset, type, length or enum)
no-rowswarningregisters is empty
duplicate-namewarningA name is used more than once by registers whose firmware ranges overlap (§3.2)
overlapwarningA value starts inside another value's registers in the same area, which usually means a wrong address or type. Registers that start at the same address are alternative readings of the same data (for example a code read as a number and as text) and never count as overlap; neither do bits and bit fields of one register (bool + bit, uint16 + bit + bits), nor registers whose firmware ranges do not overlap
firmware-outside-mapwarningA register's firmware range has no version in common with the file's, so it never applies
bad-readerrorread_group is an empty string
unknown-keywarningA register has a key that is not in this spec and does not start with x_
unused-write-limitwarningwrite_min or write_max is set on a register where it has no effect (bool, string, or access "R")
unused-bitwarningbit is set on a register where it has no effect (any type but bool, unless bits is also set on a uint16)

A register with an error in name, area, address or type is not checked further. A register with any error is left out of the overlap check.

The JSON Schema checks the structure, the JSON types and the value ranges; the rules that relate several values (unknown-enum, scale factors, overlap, address-range, bad-reference by type, duplicate keys) need code. The tests/ folder holds sample files with the diagnostics each one must produce, so any implementation can check itself against them.

8. Compatibility and versioning

  • Readers MUST ignore keys they do not know, at the top level and in registers. This lets newer files open in older readers.
  • Vendor- or tool-specific keys SHOULD start with x_ ("x_modbench_icon", "x_alarm_high") so they never clash with future versions of this spec. Their values MAY be of any JSON type.
  • mbmap is the major version. A reader MUST refuse a file whose mbmap it does not support, and SHOULD say which version it found.
  • New optional keys that older readers can safely ignore are added without changing mbmap. A key whose meaning an older reader would get wrong if it ignored it needs a new major version.
  • The schema file is named after the major version (mbmap-2.schema.json) and only gains optional keys within it.

9. Checking a map against the connected device

A tool that reads the identity registers (§4.7), or the identity answer described by identify (§4.8), of the connected device SHOULD compare them with the map, so the user does not trust values from a map written for another model or firmware. It can also let the user state the model or firmware by hand when the device does not report them.

Each check has one of three outcomes:

OutcomeModelFirmware
matchThe device's model, compared without case and surrounding spaces, is one of models (or model)The device's version is within firmware_min … firmware_max
mismatchIt is notIt is outside the range
unknownThe map has no "model" register, it could not be read, or it gave no valueThe map has no "firmware" register, it could not be read, no version string could be found in it, or the map declares no range

What a tool does with each:

  • match: nothing to report; the tool MAY show that the device was recognised.
  • mismatch: the tool SHOULD warn clearly, saying what the map expects and what the device reported, and SHOULD ask for confirmation before the first write to the device. It MUST NOT stop reading because of it: reading is harmless, and the user may know better (a rebranded device, a newer firmware that kept the same registers).
  • unknown: not a problem. Most devices do not report a model or firmware, and most maps apply to every firmware. The tool MUST NOT treat it as a mismatch, and MAY mention that the map declares a firmware range that could not be checked.

Registers with their own firmware range (§3.2):

  • When the firmware is known, a tool SHOULD use only the registers whose range includes it: the others do not exist on this device, and a register name shared by several firmware ranges then means one register.
  • When the firmware is unknown, a tool SHOULD read every register and SHOULD show which ones depend on the firmware (for example "firmware 2.1 and later"), and MAY let the user choose the version.

A map without models, firmware_min, firmware_max and identity registers is checked against nothing, which is correct for a map that applies to every device of its model.

10. Older files

Version 1 of the format, based on CSV, is deprecated. New readers do not need to support it; files in that format can be converted with tools/migrate/migrate.py (see legacy/v1).