Modbus server
The station's own Modbus TCP server: its row in the Communication column, turning it on and off, its port and the clients connected, the register map that serves tags, Logic variables and alarm states, what a client reads and writes, a worked example, and what it does not do.
The Modbus server lets a PLC, an HMI or a building system that speaks only Modbus TCP read the station's tags, Logic variables and alarm states, and write the tags and variables you allow. It is the second way data leaves the station, beside the embedded OPC UA server, and it is not a driver: a driver reads equipment, while the Modbus server is read by equipment, waiting for a PLC to connect and never connecting to anything. Its panel says so under the title: "Answers the PLCs, HMIs and building systems that read and write over Modbus TCP. It receives connections and never makes one."
Modbus carries no user name and no password: whoever reaches the port reads every register the map serves. So the server starts turned off, every row of its map starts read only, and writing is allowed row by row, by someone who chose it. View connector reads the page; Configure connector changes it.
The row in the column
The Modbus server row sits under the Servers folder of the Communication tree, below the OPC UA server's row, with a server icon where the drivers show a plug. It is always there: a station has exactly one, and it is never created or deleted. The search box finds it by its name, and the state chips file it by its state: Off while it is turned off, Issues while its port is in use or it could not listen, OK otherwise. Beside the name it says what the server is doing, and a dot sums it up:
| The row says | Dot | When |
|---|---|---|
| "Turned off" | grey | The server is off, which is how every station starts. |
| "Listening on port 502", then how many clients are connected: "No client connected", "1 client connected", "3 clients connected" | green | The server is on and the runtime runs: the port is open. |
| "Port 502 is in use" | red | The server is on, but another program holds the port. |
| "Could not listen on port 502" | red | The server is on, but Windows refused to open the port for another reason. |
| "Runtime stopped" | grey | The server is on and the runtime is stopped: the port stays closed until the runtime runs again. |
The dot is the server's alone: it takes no part in the Drivers folder's roll-up or in the status bar, which are about the equipment the station reads. Selecting the row opens the server's panel; its header repeats the state beside the same dot. The commands are on the command bar; the row carries no menu of its own.
Turning it on and off
| Command | What it does | Greyed when (situation) | Not drawn when (role) |
|---|---|---|---|
| Enable | Turns the server on, as its tooltip says: "Turn the server on: it listens on port 502 while the runtime runs." The port opens at once when the runtime runs, and the feed says "Modbus server turned on." | The server is already on ("The server is already on."), a run holds the station, or the runtime is stopped; the tooltip carries the reason. | Without Configure connector. |
| Disable | Turns the server off: "Turn the server off: it closes its port and drops every client." The feed says "Modbus server turned off." | The server is already off ("The server is already off."), a run holds the station, or the runtime is stopped. | Without Configure connector. |
Being on is a setting the station keeps, and listening follows the runtime:
| Moment | What the server does |
|---|---|
| The station starts | A server left on opens its port once the runtime is up and the runs it resumes are back; a server left off stays off. |
| The port cannot be opened | The row reads "Port 502 is in use" or "Could not listen on port 502", and the Port and clients card says it under the port field: "Port 502 is in use on this machine. Close what is holding it, or pick another port.", or the same failure followed by the reason Windows gave. The station runs on without it. The server does not try again by itself: once the port is free, turn it off and on again, or move it to another port. |
| The runtime is stopped | The port closes and every client is dropped; a write still waiting for its answer stops waiting. The row reads "Runtime stopped", and restarting the runtime opens the port again. |
| The application closes | The port closes with everything else. |
The Port and clients card
The first card of the panel is Port and clients.
| Field | What it does | Values and default |
|---|---|---|
| TCP port | The port the server listens on, on every address of this computer, IPv4 and IPv6. Changing it while the server listens closes the old port and opens the new one, which drops every client; the feed says "Modbus server moved to port 5020." | 1 to 65535. Default 502, the Modbus standard. |
| Connected clients | How many clients are connected now: "No client connected", "1 client connected", "3 clients connected". While anyone is connected, a table lists each of them with Client address (its IP address and port, such as 192.168.0.20:51234) and Connected at (the date and time, in the station's format). |
Read only. |
A port the server cannot move to is refused before anything is stored: the reason is said under the field, the field goes back to the port in force, and the server stays where it was. Six entries are refused: a number outside the range ("Port must be between 1 and 65535."); the port of the web server, set on Remote access ("Port N is already the web server. Pick another port, or move the web server first."); the port agent access is set to on the Agent page, whether agent access is on or off ("Port N is already agent access. Pick another port, or move agent access first."); the port of the OPC UA server on Configuration, running or stopped ("Port N is already the OPC UA server. Pick another port, or move the server first."); the port of the MQTT broker on the Connector page, on or off ("Port N is already the MQTT broker. Pick another port, or move the MQTT broker first."); and a port something else on this machine holds right now ("Port N is in use on this machine. Close what is holding it, or pick another port."). The four other port fields refuse this server's port in turn. The reason stays under the field until the next port the field accepts, or until the row is selected again.
Under the card a note says: "Answers any unit id. Other machines reach this server only if Windows Firewall lets TCP port 502 in: allowing it is your decision, and this station never does it on its own."
The server takes at most 16 clients at once: a seventeenth connection is closed as soon as it opens, and the journal says so. A client that disappears without closing its connection (a PLC switched off, a cable pulled) is found by TCP keep-alive, and its place is freed within about a minute and a half.
Windows Firewall
Under the note, a dot and a sentence state whether the station's own inbound rule lets this port in, as the Client access card states it for the OPC UA port, with one action when it does not.
| State | Text |
|---|---|
| Allowed (green) | "Inbound Modbus connections are allowed on port N for private and domain networks." |
| Not configured (amber) | "Network clients may be blocked because no app-owned rule allows port N." The Allow local network button is drawn. |
| Unsupported (grey) | "This launch cannot manage Windows Firewall. Configure inbound access in the operating system if needed." A development, portable or demo launch, or any run that is not the installed application. |
| Unknown (grey) | "Windows Firewall status could not be determined." |
| Command | What it does | Greyed when (situation) | Not drawn when (role) |
|---|---|---|---|
| Allow local network | Starts the app's own helper elevated, so Windows asks for administrator approval, and adds the rule for this port, scoped to the Private and Domain profiles, the local subnet and this program. Reads "Applying…" meanwhile; the feed says "Requesting Windows Firewall access for Modbus port N…", then "Windows Firewall now allows Modbus connections on port N." or the state text as an error. A cancelled approval prompt leaves the state Not configured. | In a browser on another device ("The firewall rule can only be applied from the station's desktop app."), while a run holds the station or the runtime is stopped, and while the rule is being applied ("The firewall rule is being applied."). | Without Configure connector. |
The state is read when the card opens and again when the port moves, so a new port shows Not configured until you allow it. The station never adds the rule on its own.
The register map
The second card, Register map, is what the server serves: each row puts one tag, one Logic variable or one state of an alarm at a register, or at a coil, that a client addresses. A register no row covers does not exist for a client. The card says: "Each row serves one tag, Logic variable or alarm state at a register. A row stays read only until you allow writing, and a row that serves an alarm always does. A client's write then takes the same path as every other write and is recorded in the journal with the client's address."
The table
| Column | What it shows |
|---|---|
| Register class | Holding register (4xxxx), Input register (3xxxx), Coil (0xxxx) or Discrete input (1xxxx), each with the first digit a manual's address carries for it. |
| Registers | The register the row starts at, 0-based, and the last one it takes: 0 for a row of one register, 0-1 for a Float. |
| In the manual | The same place in the notation a client's manual or PLC tool writes: the class's first digit, then the register counted from 1. 40001 for holding register 0, 40001 to 40002 for a Float there, six digits past 9999 (410000 for holding register 9999). |
| Data type | Boolean, Int16, UInt16, Int32, Float or String. |
| Access | Read only, or Read and write. |
| Source | The source, as the picker names it. |
| Value now | What a client reads there right now, or a dash when a client reading the row is answered with exception 04. |
The rows are listed by class (holding registers, input registers, coils, discrete inputs), then by register. With none: "No register is served yet. Add one with New row." Clicking a row selects it and opens its fields under the table; which row is selected belongs to the window you are in.
What grows the table and what ends a row stand at the right of the card's title:
| Command | What it does | Greyed when (situation) | Not drawn when (role) |
|---|---|---|---|
| New row | Adds a row at the first holding register no row takes: Float, both order boxes on, read only, no source yet. The row is selected and the feed says "Row added at Holding register (4xxxx) 40001 to 40002." When no holding register is left: "No holding register is left free for a new row." | A run holds the station, or the runtime is stopped. | Without Configure connector. |
| Delete row (danger ink) | Deletes the row the table has selected, at once and with no question; its tooltip names it ("Delete the row at Holding register (4xxxx) 40001 to 40002") and the feed says "Row at Holding register (4xxxx) 40001 to 40002 deleted." The tag or the variable it served is not touched. A client reading those registers is answered with exception 02 from then on. | No row is selected ("Select a row of the register map to delete it."), a run holds the station, or the runtime is stopped. | Without Configure connector. |
A row's fields
A row has exactly the fields and the rules of a Modbus tag: a point accepted at one end of the wire is accepted at the other. Each field is stored the moment it changes (a number when you leave the field or press Enter, a pick when you pick, a box when you tick it), and a client is served the new row from its next request, without being dropped.
| Field | What it does | Values and default |
|---|---|---|
| Register class | Which Modbus table the row answers in. Picking a coil or a discrete input makes the type Boolean; picking an input register or a discrete input makes the row read only. | Holding register (4xxxx), Input register (3xxxx), Coil (0xxxx), Discrete input (1xxxx). Holding register for a new row. |
| Register number (0-based) | Where the row starts, the number as it travels in the request: holding register 0 is the one the classic notation calls 40001. Beside it, Address in the manual reads the row in that notation, as the In the manual column does. | 0 to 65535. The first free holding register for a new row. |
| Data type | How the value is laid out, as a Modbus tag lays it out (Value types and quantities): Boolean in one register (1 or 0) or as one bit, Int16 and UInt16 in one register, Int32 and Float in two, String as UTF-8 with two bytes per register. A coil and a discrete input carry a Boolean alone. Choosing Boolean on a register row moves it to a discrete input (read only) or a coil (read and write); choosing a number on a coil or a discrete input moves it to an input register or a holding register, as a Modbus tag's panel does. | Boolean, Int16, UInt16, Int32, Float, String. Float for a new row. |
| Length (registers) | Drawn for a String only: how many registers the text takes. | Blank means 4. 1 to 65535. |
| Most significant byte first (MSB first, big-endian) | Which byte of each register travels first; the words in brackets are the ones manuals print for it. Not drawn for a String, nor for a coil or a discrete input. | On. |
| Most significant word first (high word first) | Which register of the pair carries the high half. Drawn for Int32 and Float only. With both boxes on, a Float travels as AB CD, the specification's order (Byte and word order); Byte order of the value, under the boxes, names the layout they make (ABCD, CDAB, BADC or DCBA). |
On. |
| Access | Whether a client may write the row. On an input register or a discrete input, Read and write stays listed and greyed, its tooltip saying "Only a holding register or a coil takes writes." On a row that serves an alarm it is greyed too: "A row that serves an alarm is read only." | Read only or Read and write. Read only for a new row. |
| Source | What the row serves. The list opens with "(none)", then every enabled tag of an enabled device, named after its driver, its device and itself ("Tag · Simulated · Oven · Temperature"), then every Logic variable that holds a value (Boolean, Integer, Decimal, Text or Date and time) by its name ("Variable · Setpoint") or, filed in a folder, by its folders and its name ("Variable · Oven / Setpoint"), so two of the same name read apart. Then every alarm, named the same way ("Alarm · Oven / High temperature"); a row that serves one is always read only, and serves the state Alarm state served names. The row holds its source by identity, so renaming the tag or the variable, or filing its device in a folder, leaves the row on the same point. A source the list no longer offers stays selected and named: a disabled tag as "Tag · Simulated · Oven · Temperature (disabled)", a tag of a disabled device as "Tag · Simulated · Oven · Temperature (device disabled)", and only a source that no longer exists as Missing Tag or Missing Variable with its identity. A tag's Used by card lists the rows that serve it, and its Replace all moves them to the tag it picks. | "(none)" for a new row. |
| Alarm state served | Drawn for a row that serves an alarm: which of its states a client reads. Active (Boolean) is 1 while the condition holds, Acknowledged (Boolean) is 1 while nothing waits for an acknowledgement, and State (number) is the state as a number. | Active (Boolean) for a new alarm row. |
Under the fields, three lines read the row back: where it sits ("Takes register 0." or "Takes registers 0 to 1."), what a client may do with it ("A client may read this row and not write it." or "A client may read and write this row: each write is recorded in the journal with its address.", or "A client reads this alarm state and never writes it. As a number, 0 is normal, 1 active and unacknowledged, 2 active and acknowledged, 3 cleared and unacknowledged." for a row that serves an alarm), and what a client reads there now ("Value now: 21.5"). When a client reading the row would get no value, the reason follows:
| The panel says | When |
|---|---|
| "This row serves nothing yet: pick its source." | No source is chosen. |
| "The source of this row no longer exists. A client reading it is answered with exception 04." | The tag or the variable was deleted. |
| "The source has no good value now. A client reading this row is answered with exception 04 until it has one." | The tag has no good value (its device is not answering and the tag presents no stand-in, the tag or its device is disabled, a limit fault made its quality bad), or the variable is a gap. |
| "The source's value does not fit this row's type. A client reading it is answered with exception 04." | A value no row of this type can hold: a NaN in an integer row, or a text longer than the row's registers. |
What a row refuses
A change the server's rules refuse is not stored: the reason is said in red above the row's fields, the change stays on the screen so you can correct it, and the row keeps serving what it served until a change is accepted. The other rows are not affected.
| The panel says | When |
|---|---|
| "The register number must be between 0 and 65535." | A register outside the protocol's range. |
| "A Modbus point carries Boolean, Int16, UInt16, Int32, Float, String." | A type outside those six, which only an agent can ask for. |
| "A coil or a discrete input carries a Boolean." | A number in a coil or a discrete input, which only an agent can ask for. |
| "A text takes 1 to 65535 registers." | A String length outside the range. |
| "Registers 65535 to 65536 run past register 65535." | A row that would end past the last register: here a Float at 65535. |
| "Registers 40001 to 40002 overlap the row at Holding register (4xxxx) 40002." | Two rows of the same class sharing a register. |
| "An input register or a discrete input is read only for a client: only a holding register or a coil takes writes." | Writing allowed on an input class, which only an agent can ask for. |
| "A row that takes writes needs the tag or the variable they go to." | Read and write chosen before a source, or the source set back to "(none)" on a row that allows writing. Pick the source first. |
| "The source cannot be written: pick a tag with write access or a memory variable, or keep the row read only." | Writing allowed on a row whose source is a read-only tag, a tag of a driver that never writes, an expression variable, or a source that no longer exists. |
| "The source holds text or a date, which only a String row carries." | A text or date tag, or a Text or Date and time variable, in a row of any other type. |
| "A row that serves an alarm is read only: a client reads the alarm's state and never writes it." | Read and write asked for on a row that serves an alarm, which only an agent can ask for. |
| "This row of the register map no longer exists. Reload and retry." | The row was deleted elsewhere, in another window or by an agent, while you were editing it. |
While a run holds the station or the runtime is stopped, a change is refused the way every change of the page is, with the reason in the action feed ("Configuration is locked while a Unit is running.", "Runtime stopped"), and nothing is stored.
What a client reads and writes
The server answers any unit id, and answers with the unit id it was asked with: on TCP a station is addressed by its IP address. Register numbers are 0-based as they travel. A PLC tool that counts from 1, or that writes the classic notation, adds the offset itself:
| Register class | Read with function | Write with function | Register 0 in the classic notation |
|---|---|---|---|
| Holding register (4xxxx) | 3 | 6 (one register) or 16 (several) | 40001 |
| Input register (3xxxx) | 4 | none: read only | 30001 |
| Coil (0xxxx) | 1 | 5 (one coil) or 15 (several) | 00001 |
| Discrete input (1xxxx) | 2 | none: read only | 10001 |
Any other function code is answered with exception 01. One request reads at most 125 registers or 2000 coils or inputs, and writes at most 123 registers or 1968 coils.
A read is answered whole or not at all. Each value is read from the station's current state, never evaluated for the client: a tag serves its engineering value, the one the station shows after the value pipeline, a variable serves its value, and an alarm the state its row names. The value is converted to the row's type by the station's one conversion: a real is rounded to the nearest whole number for an integer row (halves away from zero) and held at the type's limits, so an Int16 row serves 32767 for 40000. A read may ask for part of a two-register row: it gets the registers it asked for, as the encoded value holds them. A row whose source has no good value is never served a last value the server kept: a register carries no quality, and a PLC that read an old number would act on a value the station knows is not current. The client is answered with exception 04 instead, and its own communication-fault handling decides; the price is that one row without a value fails a block read that spans it, which the map's Value now column points at. A tag that presents a stand-in of its own (the last good value held, or a declared number, set on its Invalid reading stage) keeps a good value, and the stand-in is served like any value.
A client's write crosses the same single write funnel as every other write (see Logic): a tag through its device's delivery queue, its engineering value reversed through the tag's conversion, and a memory variable on the Logic queue. The source locks apply: a source that a running Process recipe or interlock holds is refused. The funnel journals each value as old to new under the source Modbus client 192.168.0.20:51234, and the answer waits for each value's receipt, which for a tag is the equipment's own answer.
- A write covers whole rows: writing one register of a Float row is refused, so a Float is written with function 16 and a count of 2.
- Every row the write reaches is checked against the map before any value leaves: a register in no row, or a row that is read only, refuses the whole request, and nothing is written.
- The values then go to their sources in register order. A refusal the funnel gives one of them is what the client is answered with, although the other rows of the same request were handed over too.
| Exception | When |
|---|---|
| 01 (illegal function) | A function code other than 1, 2, 3, 4, 5, 6, 15 and 16. |
| 02 (illegal data address) | A register in no row; a write to a read-only row or to part of a row; a range past 65535; a source the write funnel says cannot be written. |
| 03 (illegal data value) | A count out of range or a malformed request; a coil written with anything but on or off; a value the source cannot take. |
| 04 (server device failure) | A source with no good value, a source that no longer exists, or a value the row cannot carry; a write the equipment refused or did not answer, or a device that is not connected. |
| 06 (server device busy) | A source a running Process holds, or the runtime stopping. |
A worked example
A PLC reads an oven's temperature and its door switch from the station, and writes the oven's setpoint, a memory variable Variables.Oven.Setpoint. On the panel:
- Select the Modbus server row and press New row. Set Register class to Input register (3xxxx) (the register stays 0) and Source to "Tag · Simulated · Oven · Temperature". The row reads "Takes registers 0 to 1.", Address in the manual reads 30001 to 30002, and the Value now column shows the temperature.
- Press New row again, set Register class to Discrete input (1xxxx) (the type becomes Boolean and the register stays 0), and pick the door switch, "Tag · Simulated · Oven · DoorOpen".
- Press New row a third time. It lands at holding register 0, Float. Pick "Variable · Oven / Setpoint" first, then set Access to Read and write: the row reads "A client may read and write this row: each write is recorded in the journal with its address."
- Press Enable on the command bar: the row reads "Listening on port 502". For a PLC on another machine, press Allow local network on the Port and clients card.
What the PLC asks for, with both order boxes on (a Float travels big-endian, most significant word first, ABCD):
| Value | Function | Address | Count | Classic notation |
|---|---|---|---|---|
| Temperature (read) | 4 | 0 | 2 | 30001-30002 |
| Door switch (read) | 2 | 0 | 1 | 10001 |
| Setpoint (write) | 16 | 0 | 2 | 40001-40002 |
| Setpoint (read back) | 3 | 0 | 2 | 40001-40002 |
The same with an agent, through the four tools of the Connector areas:
modbus_map_upsert(source: "Connector.Simulated.Oven.Temperature", registerClass: "InputRegister", register: 0, type: "Float")
modbus_map_upsert(source: "Connector.Simulated.Oven.DoorOpen", registerClass: "DiscreteInput", register: 0)
modbus_map_upsert(source: "Variables.Oven.Setpoint", registerClass: "HoldingRegister", register: 0, type: "Float", access: "ReadAndWrite")
modbus_server_configure(enabled: true)
A row created by an agent is read only unless access says otherwise, and a coil or a discrete input is Boolean when no type is given. modbus_server_status reads the whole server back: its state, its clients, and every row with what a client reads there now. The tools are described on The tools, area by area.
Permissions and locks
| Permission | What it opens here |
|---|---|
| View connector | The row, the panel and the register map, read only. The cards are held inert under the note "Changing the Connector requires the “Configure connector” permission.", and no verb is drawn: no Enable, no Disable, no New row, no Delete row. |
| Configure connector | Everything on the page: turning the server on and off, its port, and every row of the map. |
While a run holds the station or the runtime is stopped, the verbs stay drawn and greyed with the reason, and every change is refused, as everywhere on the Connector page. Reading the panel, and the clients reading the server, go on. An agent changes the server only through the Connector configure area of agent access, and every change it makes is journaled as the agent's.
The journal
The server writes on the Events console under the Server area, component Modbus server: when it starts and stops listening, a port it could not open, a connection it closed because 16 clients were already served, and every request it refused with its exception code. A refusal that repeats, as a PLC polling a register that is not in the map repeats it every cycle, is written the first time, summarized every few minutes while it lasts, and closed with one line when the client is answered again. Connections and disconnections are detail, kept for the detailed capture. Each change of the server's settings or of its map is journaled too, whether it came from the page or from an agent. What a client's write did is the write funnel's own line, under its source Modbus client <address>:<port>.
What the server does not do
- It serves Modbus TCP only, with the MBAP header. There is no Modbus RTU or ASCII server on a serial line, and no RTU framing over TCP.
- It never opens Windows Firewall on its own (only Allow local network does, when you press it), and it cannot listen on one network card alone: it listens on every address of the computer, on one port.
- It answers any unit id with the same one map: it cannot host several units with different maps.
- It has no user, no password and no encryption, because Modbus has none: anyone who reaches the port reads every row and writes every row that allows writing. Keep rows read only unless a client must write them, and keep the port closed to networks that have no business with it.
- It never connects to anything. To read a Modbus device, add it under the Modbus TCP or Modbus RTU driver.
- It keeps no registers of its own: a register no row covers does not exist, and everything a client writes lands in a tag or a variable.
- It never keeps a value of its own to serve in place of a missing one, and it carries none of the other function codes (Read/Write Multiple Registers, Mask Write Register, device identification): they are answered with exception 01.
- The map has no import or export of its own: it travels with the station's configuration in the backups.