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
nullis 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 itscomment. 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/sentinelstable, so that a change to one register is a one-line diff. $schemais 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.
| Key | JSON type | Default | Meaning |
|---|---|---|---|
mbmap | integer 2 | Format version (§3.1, §8) | |
vendor | non-empty string | Manufacturer | |
model | non-empty string | Model or model family, as shown to people | |
models | array of non-empty strings | [model] | Every model name or code the map applies to (§3.2) |
vendor_website | string: a domain or an http(s):// URL | Manufacturer's website, e.g. se.com or https://www.se.com | |
description | string | Short description | |
revision | integer ≥ 1 | Revision of this map's content (§3.1) | |
revised | string YYYY-MM-DD | Date of that revision | |
firmware | string | Free text about the firmware the map applies to | |
firmware_min | version string (§3.2) | Oldest firmware the map applies to | |
firmware_max | version string (§3.2) | Newest firmware the map applies to | |
source | string | Free-text summary of where the map comes from | |
sources | array of source objects | Documents the map was built or checked from (§3.3) | |
notes | array of strings | Free notes about the file | |
protocol | "tcp", "rtu", "tcp+rtu" | "tcp+rtu" | Transports the device supports |
unit_id | integer 0–255 | 1 | Default unit / slave id |
port | integer 1–65535 | 502 | Default TCP port |
baudrate | integer | 9600 | Default RTU baud rate |
parity | "N", "E", "O" | "E" | Default RTU parity |
databits | 7, 8 | 8 | Default RTU data bits |
stopbits | 1, 2 | 1 | Default 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_base | 0, 1 | 0 | Whether address is 0-based or 1-based (§5) |
max_read_registers | integer 1–125 | 125 | Largest block the device accepts per read (FC03/04) |
max_read_bits | integer 1–2000 | 2000 | Largest block the device accepts per read (FC01/02) |
enums | object: name → table | Named translation tables (§4.1). Names use a–z, 0–9 and _ | |
sentinels | object: type → table | Reserved raw values per integer type (§4.2) | |
identify | object | How to read the device identity with Report Slave ID (FC17) or Read Device Identification (FC43) (§4.8) | |
registers | array of register objects | The 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 at1and 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).revisedgives 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"includes2.1,2.1.4and3.0, but not2.0.9;firmware_max"2"includes every2.xand2.x.y, but not3.0;firmware_max"2.3"includes2.3.7but not2.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:
| Key | JSON type | Meaning |
|---|---|---|
title | non-empty string, required | Document title, e.g. "EM530/EM540 Modbus communication protocol" |
version | string | Edition or revision of the document, as printed on it |
date | string YYYY, YYYY-MM or YYYY-MM-DD | Publication date of that edition |
url | http(s):// URL | Where the document can be found |
note | string | Which 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.
| Key | JSON type | Default | Meaning |
|---|---|---|---|
name | non-empty string | Variable name. SHOULD be unique in the file | |
group | string | Free grouping for display (e.g. Measurements) | |
area | "coil", "discrete", "input", "holding" | Modbus data area (§5) | |
address | integer (§2.1) | First register / bit address (§5) | |
type | see §6 | Data type | |
length | integer ≥ 1 | Number of registers. Required for string, ignored otherwise | |
bit | integer 0–15 | For bool in input/holding: which bit of the register, 0 = least significant. For a uint16 bit field: its lowest bit | |
bits | integer 1–16 | For uint16 in input/holding: width of a bit field starting at bit (§4.3) | |
scale | number | 1 | Multiplier |
scale_factor | string: a register name | Register whose integer value is a power-of-ten exponent for this one (§4.4) | |
offset | number | 0 | Added after scaling |
unit | string | Engineering unit (V, kWh, °C…) | |
access | "R", "W", "RW" | "R" | Allowed operations |
byte_order | "big", "little" | file byte_order | Overrides the file default for this register (§6) |
word_order | "big", "little" | file word_order | Overrides the file default for this register (§6) |
write_min | number | Smallest value accepted when writing, in engineering units (§4.5) | |
write_max | number | Largest value accepted when writing, in engineering units (§4.5) | |
enum | string (a name in enums) or table | Labels for integer or bool values (§4.1) | |
reference | see §4.6 | Raw value this register is expected to hold on a healthy device | |
firmware_min | version string (§3.2) | Oldest firmware that has this register | |
firmware_max | version string (§3.2) | Newest firmware that has this register | |
role | "model", "firmware", "serial" | This register identifies the device (§4.7) | |
read_alone | boolean | false | The register is read in a request of its own (§4.9) |
read_group | non-empty string | The register is read only together with registers of the same group (§4.9) | |
description | string | Free text | |
comment | string | Free 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
sentinelsareint16,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
scaleandoffset. - 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
enumlabel 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 nobits, and MUST NOT itself have ascale_factor. If several registers share the name, the first one counts. - Only numeric types can have a
scale_factor(notboolorstring). - 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,scaleandoffsetare applied. A limit of0.5on a register withscale0.1means 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_minMUST NOT be greater thanwrite_max. The limits are inclusive.- A tool MUST NOT send a write whose engineering value is below
write_minor abovewrite_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
boolandstringregisters 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,scaleandoffset, in the JSON type of that value: an integer (§2.1) for the integer types and for bit fields, a number forfloat32andfloat64,true/falseforbool, a string forstring. It MUST fit the type (and the width of a bit field, or2 × lengthcharacters for a string). - The check is the register itself: a tool reads its
areaandaddress(already adjusted foraddress_base) and decodes itstypewith 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
float32reference 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
referenceon 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 itsenumlabel when it has one, is compared withmodels. 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"gives2.01). A number gives its engineering value (102withscale0.01gives1.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": 8and"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"}
]
}
| Key | JSON type | Default | Meaning |
|---|---|---|---|
function | 17 or 43 | 17: Report Slave ID. 43: Read Device Identification (encapsulated interface transport, MEI type 0x0E) | |
fields | non-empty array of field objects | The values to take from the answer |
Each field is an object. name is always required. role and the keys below depend on function.
| Key | JSON type | Default | Meaning |
|---|---|---|---|
name | non-empty string | Name shown to people | |
role | "model", "firmware", "serial" | Meaning of the value, as for registers (§4.7). At most one "model" and one "serial" | |
offset | integer ≥ 0 | FC17 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 | |
length | integer ≥ 1 | FC17 only. Length in bytes. Required for string | |
enum | table name or inline table | FC17 only. Translates a number into a label (§4.1). A model code is best given a label, which is then compared with models | |
object | integer 0–255 | FC43 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_basedoes 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:
identifyand registers with arolecan both be present. When both give a value for the same role, a tool SHOULD use the registers and MAY useidentifyfor 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:truemeans 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 withoutread_groupare 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
area | Unit | Read | Write | Allowed access |
|---|---|---|---|---|
coil | bit | FC01 | FC05 / FC15 | R, W, RW |
discrete | bit | FC02 | — | R |
input | 16-bit register | FC04 | — | R |
holding | 16-bit register | FC03 | FC06 / FC16 | R, 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
type | Registers | Notes |
|---|---|---|
bool | — | In coil/discrete: one bit. In input/holding: one bit of a register, so bit is required |
int16, uint16 | 1 | |
int32, uint32, float32 | 2 | Floats are IEEE 754 |
int64, uint64, float64 | 4 | |
string | length | 2 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).
| Type | byte_order | word_order |
|---|---|---|
bool | not used | not used |
int16, uint16 | swaps the two bytes of the register | not used (one register) |
int32, uint32, float32 | swaps the two bytes of each register | decides which of the 2 registers is the high word |
int64, uint64, float64 | swaps 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 character | not 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_order | word_order | Bytes on the wire | Common name |
|---|---|---|---|
big | big | 0A 0B 0C 0D | Modbus standard |
big | little | 0C 0D 0A 0B | "word swap", used by many meters and PLCs |
little | big | 0B 0A 0D 0C | "byte swap" (rare) |
little | little | 0D 0C 0B 0A | little-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.
| Code | Severity | When |
|---|---|---|
not-utf8 | error | The file is not valid UTF-8 |
not-json | error | The 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-type | error | A known key holds the wrong JSON type (a string where a number is expected, null, …) |
missing-key | error | mbmap, 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-value | error | A top-level key has a value outside its allowed set or range, including mbmap other than 2 |
bad-area | error | Unknown area, or not written in lowercase |
bad-type | error | Unknown type, or not written in lowercase |
type-area | error | A type other than bool in coil or discrete |
bad-address | error | address is not an integer |
address-range | error | The address is below address_base, or the value goes beyond address 65535 |
missing-bit | error | bool in input/holding without bit |
bad-bit | error | bit outside 0–15 |
missing-length | error | string without length |
bad-length | error | length is not an integer ≥ 1 |
bad-access | error | Unknown access (or not in uppercase), or not allowed for the area |
bad-byte-order | error | A register's byte_order or word_order is not "big" or "little" |
bad-enum | error | An 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-sentinel | error | sentinels names an unsupported type, or a table is not valid (§2.2) or has a value that does not fit the type |
bad-bits | error | bits 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-enum | error | enum names a table that enums does not define |
bad-write-limit | error | write_min is greater than write_max |
bad-reference | error | reference 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-factor | error | scale_factor names no register of the file (registers with errors do not count) |
bad-scale-factor | error | scale_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-version | error | firmware_min or firmware_max, in the file or a register, is not a version string (§3.2) |
bad-firmware-range | error | firmware_min is after firmware_max, in the file or a register |
bad-source | error | An 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-role | error | role 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-identify | error | identify 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-rows | warning | registers is empty |
duplicate-name | warning | A name is used more than once by registers whose firmware ranges overlap (§3.2) |
overlap | warning | A 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-map | warning | A register's firmware range has no version in common with the file's, so it never applies |
bad-read | error | read_group is an empty string |
unknown-key | warning | A register has a key that is not in this spec and does not start with x_ |
unused-write-limit | warning | write_min or write_max is set on a register where it has no effect (bool, string, or access "R") |
unused-bit | warning | bit 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. mbmapis the major version. A reader MUST refuse a file whosembmapit 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:
| Outcome | Model | Firmware |
|---|---|---|
| match | The 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 |
| mismatch | It is not | It is outside the range |
| unknown | The map has no "model" register, it could not be read, or it gave no value | The 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).