ModbusLens

Documentation

Getting Started

ModbusLens is a Modbus TCP and RTU client built for testing, commissioning, and troubleshooting industrial devices - PLCs, drives, meters, and gateways. It combines the things you'd normally reach for several separate tools to do: reading and writing coils/registers, monitoring named tags on several devices at once, graphing values over time, simulating a slave device, running repeatable test scripts, and scanning a network for Modbus devices.

What you need before you start

  • Each device's Modbus TCP address/port, or its Modbus RTU serial settings (COM port, baud rate, parity) if it's a serial device.
  • Each device's Unit ID (also called Slave ID or Station Address) - commonly 1, but check the device's manual or DIP switches/configuration. Several devices behind one gateway or on one RS-485 line share the connection and differ only by Unit ID.
  • Ideally, the device's register map - which addresses hold which values, and in what format. If you don't have one, the Raw (Hex) column and the Address Table are good tools for reverse-engineering it safely (read-only first).

Basic workflow

  1. Open the Overview tab (the first tab). A new window starts with one device, "Device 1". Set its connection with Device Settings (top right), or its card's ⋮ menu > Edit Device... > Connection Settings...: IP address, port (usually 502) and Unit ID - or switch to Modbus Serial and enter the serial parameters instead.
  2. Add any further devices with Add Device, Add from Profile... (a device plus a saved profile's tags) or Find Unit IDs... (asks each Unit ID over a connected device's link).
  3. Click Connect All (top right), or Connect on one device's card. Each device's pill in the top bar and its card show its status - green when connected/online.
  4. Use the tabs to work with the devices: Address Table for quick reads/writes, Profiles for reusable device templates, Tags for named live monitoring, Raw Data for the untouched bytes behind every transaction, Trend for graphing, Server to act as a slave device yourself, Script to automate a test sequence, and Scanner to find which addresses respond. With two or more devices, the single-device tools (Address Table, Script, Scanner, Diagnostic Functions) each have their own Device dropdown; Tags and Trend pens carry their own device.

The status bar at the bottom shows "N of M device(s) connected" and the version.

Menu bar

See the Menus & Toolbars topic on the left for a full, step-by-step list of every menu and option and exactly where to find it.

See the topics on the left for details on each part of the app, and check Troubleshooting if something isn't behaving the way you expect.

Connecting to a Device

Every device has its own connection and Unit ID. Open a device's connection from its Overview card (⋮ > Edit Device... > Connection Settings...), from the Add Device dialog, or with Device Settings on the top bar (Tools > Device Settings... does the same). Choose Modbus TCP or Modbus Serial (RTU/ASCII) under Connection Type, fill in the fields for that mode, and click Save Settings. A connected device can't be edited - disconnect it first.

Adding a device

Add Device (Overview, or the Tags tab) asks for a Name (required, unique - case doesn't matter), its Connection (starts as a copy of the first device's; change it with Connection Settings...) and its Unit ID (0-255, suggested as the next free one on that connection). Two devices on the same connection need different Unit IDs; the same Unit ID on two different gateways is fine. Devices with identical connection settings automatically share one link - see Overview. A serial port can only carry one set of line settings, so a second device on a COM port that's already in use with different baud/parity/framing is refused.

Modbus TCP (Target Device (TCP))

  • IP Address - the target device, PLC, or gateway's Modbus TCP address.
  • Port - usually 502 for standard Modbus TCP (ModbusLens's own Server tab defaults to 5020).
  • Unit ID - see below.
  • Network Interface - Auto (let OS choose route) is the default: it fills in a local interface's IP as a convenience when Target IP is blank (e.g. testing against ModbusLens's own Server tab), but otherwise leaves routing entirely to the OS. Picking a specific interface instead actually binds the outgoing TCP socket to it - useful on a multi-homed machine (VPN + Ethernet + Wi-Fi all up at once) where the OS's default route isn't the NIC you actually want the connection to go out of. If you need the full picture - every adapter's IP and subnet mask - Tools > IP Configuration shows all of them at once, like running ipconfig.
  • Fast LAN Mode - a short (200ms) timeout and no retries, for a local network where a timeout means the device is actually gone rather than just momentarily slow. It also changes how Tags monitoring reacts to a failed poll - see Tags Monitoring. It's part of the device's connection, so two devices at the same IP:port but with different Fast LAN, framing or interface settings use two separate TCP connections.
  • Framing - Modbus TCP (standard) is the default MBAP framing almost every device uses. Switch to RTU over TCP only for a transparent serial-to-Ethernet gateway (e.g. a Waveshare RS485-TO-ETH running in TCP Server/passthrough mode) that tunnels raw RTU frames (with CRC16) over a plain TCP socket instead of translating them to real Modbus-TCP framing. Many of these gateways forward every reply to every connected client, so if another master (a SCADA, Node-RED, ...) polls the same gateway, its replies reach ModbusLens too - they're recognised and skipped, since a reply has to match the request's function code and register count (or write echo). A reply from the same unit with the same function and count can't be told apart in RTU, which carries no transaction ID, so keep polling slow on a shared bus.
  • Find Devices... - closes the dialog and opens Diagnostics > Find Devices with the TCP network scan selected.

Modbus Serial (Target Device (Serial))

  • COM Port - the port the device is connected to (via USB-RS485/RS232 adapter or a native serial port); detected ports are listed, and you can type one too.
  • Baud Rate (1200-115200, or type another), Parity (None/Even/Odd), Stop Bits (1/2), Byte Size (7/8) - must match the device's configuration exactly, or communication will fail or return garbage.
  • Framing - RTU (binary) (the default and far more common) or ASCII (hex-encoded, human-readable on the wire, framed with a leading : and trailing CR/LF). The two are incompatible - a device speaking one will not respond correctly to the other, so this has to match the device exactly, the same as baud rate and parity.
  • Find Devices... - opens Diagnostics > Find Devices with the serial parameter sweep selected.

A brand-new device starts at 127.0.0.1:502, or COM1 at 19200 8N1 RTU on the serial side.

Unit ID and Recent Connections

  • Unit ID - the Modbus slave/unit identifier (1 is common; some TCP gateways ignore it, but RTU devices and gateways with several units behind them always need the right one). It belongs to the device: changing it here changes the device's Unit ID. Values above 255 are clamped to 255 with a warning.
  • Recent Connections - picking an entry fills the form with a connection you've used before (the last 10); it doesn't connect by itself. A connection is added to the list when you save settings or connect.

Connecting and disconnecting

Click Connect / Disconnect on a device's Overview card, or Connect All / Disconnect All on the top bar. Each device's pill and card show its status: Disconnected, Connection failed, Reconnecting..., Connected - and once Tags monitoring is polling it, Online, Device exception, Partly failing, No response, Paused, Waiting for first poll or No tags. If a card's Connect fails, a "Connection Failed" message names the device and target, the error, and a short checklist; Connect All lists every device that failed in one message instead. See Troubleshooting for the details.

Finding your own IP (IP Configuration)

Tools > IP Configuration opens a small window listing every network adapter on this machine - the same information ipconfig gives you at a command prompt, without leaving the app. Each row shows the adapter name, its IPv4 address, subnet mask, and whether the adapter is Up or Down.

This is mainly useful for two things: figuring out which of your adapters is on the same network/subnet as the target device before you connect, and getting the right address to hand out when you're the target - e.g. telling a colleague's PLC or SCADA system which of your IPs to connect to when testing against ModbusLens's own Server tab. The Network Interface dropdown in Connection Settings (above) covers the common case of "fill in my own IP"; IP Configuration is for when you need to see every adapter and its subnet mask at once, such as confirming two adapters aren't sharing a conflicting subnet.

Auto-reconnect

Once connected, ModbusLens watches every link in the background. If one drops - a cable pulled, a device rebooting, a network blip - the devices on it show Reconnecting... and it keeps retrying automatically, waiting a little longer between each attempt (2s, 4s, 8s... capped at 30s) so it doesn't hammer a device that's still coming back up. Other links keep working meanwhile. If Tags monitoring stopped because every tag failed for several polls in a row (read as: the connection itself was the problem, not one bad tag), it resumes automatically the moment the connection recovers - you don't have to click Start Monitoring again. This only applies to an unexpected drop; disconnecting yourself never triggers a reconnect attempt.

0-based vs 1-based addressing

Modbus devices are documented two different ways: some vendors say "register 40001" meaning protocol offset 0 (1-based/traditional), others say "register 0" meaning the same offset (0-based/raw). The 0-Based Addressing checkbox on the Address Table and Tags tabs controls which convention the address field uses:

  • Unchecked (default): 1-based. Entering address 1 reads protocol offset 0, matching the classic 40001-style convention.
  • Checked: 0-based. Entering address 0 reads protocol offset 0 directly.

If a value looks off by one compared to what you expect, this is almost always the cause - toggle the checkbox and compare. See Troubleshooting for more on diagnosing this.

Address Table

A quick read/write grid for a contiguous range of one Modbus data type, similar to classic tools like ModScan. This is usually the fastest way to sanity-check a connection or probe an unfamiliar register map before setting up named Tags.

Device

With two or more devices, a Device dropdown ("Name (Unit N)") sits above the range settings - the table reads and writes that device only, independently of what the other tabs are pointed at. Changing it stops Live Monitoring and logs which device the table now talks to. The controls are only enabled while that device is connected.

Creating a table

  1. Pick a Function (Read Coils, Read Discrete Inputs, Read Holding/Input Registers, Write Single/Multiple Coil(s), Write Single/Multiple Register(s)).
  2. Set Start Address and Count (Count is fixed at 1 for the Write Single functions). A read can cover at most 125 registers or 2000 coils/inputs, and the range can't go past address 65535.
  3. Click Create Table. Changing the Function afterwards rebuilds the table.

Each row shows the Modbus reference address (0xxxx coils, 1xxxx discrete inputs, 3xxxx input registers, 4xxxx holding registers), the current value, and the same value in hex. For a Write function, double-click the Value cell to edit and send it immediately (coil rows show a checkbox instead).

  • 0-Based Addressing - the same convention switch as on the Tags tab (1-65536 when unchecked, 0-65535 when checked). See Connecting to a Device.
  • Display - how register values are shown: U16, S16, HEX, or the 32-bit U32/S32/F32, which pair adjacent rows and show the value on the first row of each pair. It doesn't apply to coils/discrete inputs and re-renders immediately.

Write bounds

For Write Single/Multiple Register functions, two extra columns appear: Min and Max. Setting both on a row rejects any write to that register outside the range - a typo like an extra zero gets refused instead of sent to the field device. Bounds belong to the device picked in the Device dropdown - another device on the same gateway keeps its own - and are enforced for every write to that register on that device, whether it comes from this table, the Tags tab or a Script. They're saved with the device, so they're still there after reconnecting or restarting, and the Min/Max cells show them when you build the table again. Bounds cover registers. Leave both blank for no limit. A rejected write shows up as a failed write with a message like "Write rejected: value 10000 at address 99 is outside the configured write bound [0, 100]".

Live Monitoring

For Read functions, check Enable Live Monitoring and set an interval (100-10000 ms, default 1000) to keep polling the whole range automatically. It runs alongside Tags monitoring and Trend - each request waits its turn on the connection - so you can watch one device's range here while Tags keeps polling every device, and both show up in the Raw Data tab. It keeps running while you look at other tabs.

Status Log

The panel on the right shows what the table is doing - reads, writes, and any errors, each with a timestamp and color-coded so the right lines stand out: writes in blue, connection events in green, errors in red, everything else in the default color. This is the first place to look when a write doesn't seem to take effect. The same coloring applies to System Logs and the Script console. Ctrl+scroll wheel over the log zooms its text size in and out.

Overview

The first tab, and the place devices are set up. A device is one Modbus unit: a name, its own connection (Modbus TCP - IP, port, framing including RTU over TCP - or a serial port with its baud rate and framing) and its Unit ID. Devices with identical connection settings - several meters behind one gateway, or on one RS-485 port - automatically share one link, and are polled one at a time over it; a serial port can only carry one set of line settings.

Adding and removing devices

  • Add Device - the device dialog: Name, Connection (Connection Settings... opens the usual connection dialog) and Unit ID. See Connecting to a Device.
  • Add from Profile... - creates a device with a saved profile's tags in one step: pick a Profile (only profiles that have tags are listed, as "Name (N tags)"), a Device name (suggested from the profile), the Connection (starts as the first device's) and the Unit ID (next free one on that connection). The card then shows the profile's name.
  • Find Unit IDs... - a Unit ID sweep over an already connected link (with several links it asks "Sweep Unit IDs on which connection?"). Set the Unit IDs range (1-247, default 1-10) and the Timeout per unit (100-3000 ms, default 500), then Start Sweep: each unit is asked for holding register 0, and anything that answers - data or a Modbus exception - is listed. Units already used on that link show greyed out as "(already: NAME)". Tick the ones you want and click Add Selected as Devices; they're named "Unit N" (rename them later). Stop monitoring first so the sweep has the line to itself, and keep the range small on a busy shared bus.
  • Remove Selected - each card has a small checkbox by its name; check the ones you want gone and click Remove Selected for one Keep Tags / Delete Their Tags Too confirmation covering all of them (same choice the single-device remove offers, just for a batch). Checking every device just runs Remove All Devices instead.
  • Remove All Devices - after a confirmation that shows how many devices and tags will go, disconnects and deletes every device and all their tags, alarms, scaling and pinned values, leaving zero devices - add one back from the Overview tab when you're ready. Export CSV or Save Session first if you might need them.

Device cards

Each device gets a card:

  • Its name and status: Disconnected, Connection failed, Reconnecting..., Connected, and while Tags monitoring is running Waiting for first poll, Online, Device exception (it answered with a Modbus exception), Partly failing, No response, Paused or No tags.
  • Its connection, profile (if it was added from one), Unit ID and tag count.
  • Up to six pinned values - its first four tags until you choose others with Pin Values.... A pinned tag shows its Engineering Value if it has scaling, else its Read Value.
  • A detail line: "N% OK of M reads · last good read Xs ago · NN ms" plus the last error, or "Not connected -- click Connect." / "Couldn't connect: ...".
  • Buttons: Connect/Disconnect (this device only), Open Tags (jumps to its tab in Tags), Pin Values..., and Pause/Resume - leaves the device out of Tags monitoring's polling (Trend pens on it are still read).
  • The ⋮ menu: Edit Device... (disconnect it first) and Remove Device... - which asks whether to Keep Tags (moved to another device) or Delete Its Tags Too. The last device can't be removed; edit it instead.

The Monitoring Controls box at the top (Start/Stop Monitoring, Interval 100-10000 ms) is the same control as on the Tags tab - the two stay in sync - and shows "N device(s), M connected".

Working with several devices

The bar at the top of the window shows every device with its status - click one to jump to its card here (it's highlighted for a moment). There's no single "active" device: Address Table, Script, Scanner and the Diagnostic Functions dialog each have their own Device dropdown (shown once there are two or more devices), so each can work with a different device at the same time; Tags and Trend pens carry their own device, and the Raw Data tab has a Device column and filter. Device Settings edits a device's connection (pick which; it has to be disconnected). Connect All / Disconnect All act on every device. A link that drops is reconnected automatically with back-off.

The device list is saved automatically (by the first ModbusLens window - see Multiple Windows) and in Sessions; an older session's single connection becomes a device called "Device 1".

Tags Monitoring

The Tags tab lets you name individual points scattered across different addresses and types, and watch or write them all at once - unlike the Address Table, which is one contiguous range. This is the tab you'll spend the most time in once you've mapped out a device: build the list once, then monitor it continuously.

Adding tags

Add Tag appends a new row (or inserts just below the selected row, so you're never stuck adding things only at the end). Once a list is built, drag a row's number in the left-hand gutter up or down to reorder it - a blue line shows exactly where it will land as you drag, and the row's live values, comment, and alarm configuration (if any) all move with it. Reordering is only available while monitoring is stopped, the same as adding or removing a tag. Each row has:

  • Tag Name, Mode (Read, Write, or Calc - see Calculated tags below), Type (Coil/Discrete Input/Holding Register/Input Register), Address, Count, Format.
  • Read Value - the decoded live value.
  • Raw (Hex) - the same value in hex, straight from the register(s), independent of format.
  • Write Value - type a value here and click Write Selected to send it (Write-mode tags only), or just press Enter in the cell to write that one row immediately without selecting it or clicking anything - mirrors the type-and-Enter workflow classic tools like Modbus Poll use. Goes through the exact same confirmation and safety interlock as Write Selected.
  • Comment and Timestamp.
  • Group - which Tag Group the row belongs to (pick + Add Group... to create one). Groups are shown as collapsible header rows ("Name (N tags)").
  • Engineering Value and Scale - see Engineering-unit scaling below.
  • Enabled - unticked rows are skipped by monitoring (a manual write still works).
  • Device (with two or more devices) - which device the tag belongs to; the dropdown ends with + Add Device....

Right-click the column header to show or hide columns; drag a header to reorder them. Right-click a row for Configure Alarm..., Bit View..., Show Bits Inline and Copy Row(s). Delete removes the selected rows and Ctrl+C copies them. Remove Selected Tag and Remove All Tags are next to Add Tag - with a device tab open, Remove All Tags removes only that device's tags. Scrolling the table with the mouse wheel never changes a dropdown or number under the pointer - it just scrolls; click a cell to edit it.

A duplicate start address on the same device (same type) is moved to the next free address automatically, with a note in the log. Start Monitoring refuses to run with duplicate addresses on one device and warns about overlapping ranges; the same address on two different devices is fine.

Devices in Tags

Every tag belongs to a device (see Overview). With more than one device, the Tags table shows a Device column next to Tag Name and a row of device tabs above it: All, one tab per device, and + to add another. A device tab ("Name (unit)") only shows that device's tags, and a tag you add while it's open belongs to that device; right-click a device tab for Edit Device... / Remove Device.... Add Device next to Add Tag opens the same device dialog as the Overview. The same address can be used on every device - two identical meters simply share one register map. Tags CSV exports carry Device and Unit ID columns, so an import recreates missing devices. Import CSV while a device's tab is open imports into that device only - it replaces that device's tags and leaves the others alone, so one meter-model CSV can be imported once per meter (a tag name already on that device is skipped). On the All tab it replaces the whole table, creating any device named in the file that doesn't exist yet (on the first device's connection); an older CSV without a Device column loads onto the first device. Write All and Write Selected also follow the open tab: with a device tab open, Write All writes only that device's Write-mode tags. Profiles stay device-independent: they describe a device type, not where it's connected.

Naming a tag

A tag name can only use letters, numbers, and underscores - no spaces or other symbols, and it can't start with a digit. Script language keywords (WRITE, READ, HR, COIL, and so on) and common reserved words are also blocked. Entering an invalid name shows a warning and clears the field rather than letting it stick, since a tag's name is also how it's referenced by name in a Script (see the Scripting topic) and in Trend's Add Pen picker.

Data formats

Bool, U16/S16, U32/S32/F32, U64/S64/F64 (plus _SWAP variants of the 32/64-bit formats for the opposite word order), and Hex. BOOL on a Coil/Discrete Input is a simple flag; BOOL on a Holding/Input Register instead shows the full 16-bit pattern (e.g. 0000000000000101) so you can read individual status/alarm bits out of a status word. 32-bit formats (U32/S32/F32) need a Count that's a multiple of 2; 64-bit formats (U64/S64/F64) need a multiple of 4 -- since they span 2 or 4 registers per value, respectively.

Bit View

Right-click a Holding/Input Register tag and choose Bit View... for a live, per-bit breakdown of its raw value - each of the register's bits shown on its own line with an editable name and current 0/1 state, useful for VFD-style control/status words that pack several independent booleans (running, ready, fault, at-speed, auto/manual, ...) into one register.

A Bool-format tag can also expand its bits directly inline, as rows in the Tags table itself - click the row number to toggle. For a Holding Register (writable), each bit row gets its own Write Value cell: type 1/0 (or true/false) and press Enter to read-modify-write just that one bit, leaving the rest of the register untouched. Input Registers are read-only, so their bit rows have no Write Value cell. Naming a bit in either the popup or the inline rows updates the other immediately - they're the same underlying bit names.

Engineering-unit scaling

Check the Scale box on a row to open a small popup with two modes:

  • Linear (Min/Max) - Raw Min/Max and Scaled Min/Max define a linear transform from whatever the device actually sends to a meaningful engineering unit (e.g. raw ADC counts 0-4095 mapped to 0-100 PSI).
  • Multiply by Constant - a single factor (e.g. raw 151 x 0.1 = 15.1), for the common case of a device sending a value scaled by a fixed power of ten.

Either way, the result appears in the Engineering Value column alongside the normal Read Value, live as the tag is polled - and in Trend, if a pen's tag has scaling enabled. Choose Real or Integer for how the scaled result is displayed. Unchecking Scale (or Cancelling the popup) turns scaling back off for that row.

Calculated tags

Set a row's Mode to Calc to make it a calculated tag: instead of reading a register, its value is an expression over other tags' current values - for example P1 + P2 + P3 for a total, or V1 * I1 * PF / 1000 for kW. An Expression column appears next to Mode once a Calc tag exists; Type, Address, Count, Format, Write Value and Scale don't apply to a Calc row and are greyed out.

  • Expressions use the Script language's arithmetic: numbers, + - * / and parentheses, and tag names. A plain name means that tag on the calc tag's own device, so the same V1 * I1 works on every meter; [DEVICE NAME].TAG (e.g. [METER 2].P1) uses another device's tag, for site totals.
  • Inputs use the value the Tags tab shows: the Engineering Value when scaling is on, otherwise the decoded Read Value; coils count as 1/0. Calc tags can use other calc tags.
  • Calc tags are worked out after every Tags poll cycle from the values just read, so they add no traffic on the bus. Start Monitoring needs at least one Read tag to work from.
  • If an input is missing or failed, a name isn't a tag, or two calc tags depend on each other, the Read Value shows ERROR - hover it for the reason. An expression with a mistake gets a red border when you finish typing it.
  • A calc tag works like any other tag: alarms (High/Low), Trend pens, Overview pinned values, Log to CSV, and Script reads. CSV export/import, Sessions, Profiles and the saved workspace carry the expression. A Script can read a calc tag but can't write to it.

Alarms

Right-click a tag row and choose Configure Alarm.... Numeric tags get a High and/or Low limit; coils, discrete inputs, and BOOL-format registers get an ON/OFF trigger instead. The Read Value cell turns red while the tag is in alarm.

Logging

Log to CSV appends a timestamped row for every monitored tag on every poll tick to a file you choose, including each tag's device. Export CSV/Import CSV save or load the tag list itself (not the live data) - handy for keeping a reusable tag set per device model.

Resilience while monitoring

A single tag that fails to read (bad address, wrong count for its format, device briefly unresponsive) shows ERROR in its own Read Value cell but doesn't stop the rest of the list from updating. Tags of a device that isn't connected, or is paused on the Overview, are simply skipped. Monitoring only auto-stops if every polled tag fails for three polls in a row, which is treated as a lost connection rather than a configuration mistake on one row; with several devices, one device dropping doesn't stop monitoring - its tags show ERROR until its link reconnects. Start Monitoring needs at least one connected device. The Interval (100-10000 ms, default 1000) is shared with the Overview.

Fast LAN Mode (a device's Connection Settings, TCP only - see Connecting to a Device) changes this slightly: after the first failed read in a poll cycle, it does one quick reachability check against the device instead of moving straight to the next tag. If the device is actually gone, every remaining tag in that cycle is marked ERROR immediately rather than each one individually timing out. If the check finds the device still reachable, polling continues normally - that one failure was specific to a single tag, not the connection.

Safety interlock

Writing is paused for the moment a value is being sent, then Read polling resumes automatically - this stops a write and a read from overlapping on the same connection.

Device Profiles

The Profiles tab saves a device's Address Table range and Tags list together as one named, reusable template - build it out once for a device model (e.g. a specific VFD or PLC), then apply it to any device of that model instead of rebuilding it by hand every time. Distinct from File > Save/Load Session: a session also carries your devices' own IP/port/serial settings and Unit IDs, a profile deliberately doesn't (its tags carry no Device or Unit ID), since the same device model gets reused across many different installations. The Overview's Add from Profile... creates a new device straight from a profile.

Local profiles

Create New Profile opens a page-navigated dialog: Profile Info (Name, Manufacturer, Type, Author), a Tags page where you check which of your currently configured Tags to capture into the profile (Select All / Select None; tags of every device are listed), and a placeholder Datasheet page. The Address Table's current range is captured automatically. Edit reopens the same dialog pre-filled with the profile's own saved contents - not just whatever happens to be live right now - so editing a profile for a device you're not currently connected to doesn't wipe it out. Delete removes a profile file permanently after confirming.

Double-clicking a profile card (or selecting it and pressing Enter) opens View Profile: a read-only look at its Tags, each marked with a colored dot against your current live Tags list - green means that tag is already there, red means it isn't, yellow (on a Tag Group header) means the group is a mix of both. Check the ones you want and click Apply to import them into your live Tags list - onto the device whose tab is open in Tags, else the first device; a tag name already on that device is skipped; Apply doesn't close the dialog, so you can adjust the selection and apply again, and the Address Table range is applied at the same time. Local profiles are stored under your own Documents folder (Documents/ModbusLens/Profiles), one file per profile, so they're easy to find, back up, or move by hand.

Community profiles

The Local/Community toggle at the top of the tab switches to profiles other ModbusLens users have shared and a maintainer has verified - fetched over the network the first time you open the Community tab (or click Refresh), no account or sign-in needed. Same as Local, double-clicking a card opens a preview - but here, Apply and a separate Download button are entirely independent: Apply imports the profile's tags into your live Tags list without saving anything to disk, Download saves a copy into your own Local profiles (picking it up in the Local tab), and you can do either, both, or neither before closing. A profile you've downloaded shows a small Community badge on its Local card so it's not mistaken for one you built by hand - the badge disappears if you later edit that profile, since at that point it's no longer literally the shared file.

Sharing a profile

Select a local profile and click Share to Community to submit it for review - one click, no GitHub account or sign-in required. A maintainer reviews every submission before it's added to the public Community list, so it won't appear there immediately. Resubmitting the exact same, unchanged profile a second time is silently skipped rather than sent again.

Raw Data

One row per Modbus transaction - the untouched data behind every read and write, independent of how a Tag or Address Table row happens to decode it. Useful when a decoded value looks wrong and you want to see exactly what came back before ModbusLens interpreted it as U16, F32, or anything else, and for spotting a device that's responding but slow. Ctrl+scroll wheel over the table zooms its text size in and out.

Columns

  • Time and Operation - when it happened and what it was (which Tag, Address Table function, or Script command triggered it). Tags polling and writes, the Address Table and Scripts are logged here; Trend, Scanner, Diagnostic Functions and the Unit ID sweep are not.
  • Device (shown second, with two or more devices) - which device the request went to.
  • Value - the raw register/coil result in decimal, or the error message if it failed.
  • Raw (Hex) - the same result in hex (registers as 0xNNNN, coils as 1/0) - blank for a failed transaction, since there's nothing to show.
  • TX Bytes / RX Bytes - the literal bytes ModbusLens sent and received on the wire for that exact transaction, captured straight from the connection itself. This is one level more raw than the Value/Raw (Hex) columns - those show the register values after pymodbus has already parsed the response; TX/RX Bytes show the full frame as bytes, including the function code, address, byte count, and (for serial) the CRC/LRC. RX is blank if the request timed out with no response at all.
  • Status - Success or Failed, color-coded the same way as the other logs.
  • Exception - the decoded Modbus exception description (e.g. "Illegal Data Address") when the device itself replied but refused the request. Left blank for a plain communications failure (timeout, no response at all), so you can tell "the device rejected this" apart from "nothing answered" at a glance instead of both just showing Failed. Hover over an Exception cell for a tooltip with the exception's plain-English meaning and a short list of likely causes (e.g. for Illegal Data Address: wrong start address, a 0/1-based addressing mismatch, the wrong register space) - a starting point for what to check next, not just a name to go look up.
  • Latency (ms) - how long that specific request took round-trip. A device that's technically working but degrading usually shows up here first, before it starts failing outright.

Filter

The text box filters by whatever's in the Operation, Value, or Exception columns - a tag name (e.g. pump), an address, a specific value, or part of an exception message. The dropdown next to it narrows to All, Success or Failed rows, and with two or more devices a Device dropdown ("All devices" or one device) shows only that device's transactions. All of them apply live as you type/select, and to new rows as they arrive - useful for watching one specific tag during a busy poll, or isolating every failure to see if they cluster around one address.

Show Statistics

Opens "Modbus Communication Statistics" for this window (all devices together): total requests, successful vs. failed counts, exception responses, which function codes were used, a breakdown of exception codes and failure causes, average/min/max response times over the last 500 responses, and a Devices list showing whether each device is connected - counted across everything logged so far, not just what's currently visible in the table, since old rows fall off after 1000. Reset Statistics zeroes the counters (Clear Data and Clear All Logs don't). Useful for confirming a "slow" feeling is real and quantifying it.

Clear Data

Empties the table (and the Frame Viewer panel below it) without affecting the connection or any other tab. Do this before reproducing an intermittent issue so the table only contains the run you care about.

Export CSV

Saves whatever rows are currently visible - i.e. respecting the text, status and device filters - to a CSV file (the Device column comes last in the file).

Right-click / Ctrl+C

Right-click a row (or a multi-selection of rows) for Copy Row(s) as Text - every column, tab-separated, ready to paste into a spreadsheet - or Copy Row(s) as Hex Bytes (TX/RX), which copies just the literal wire bytes without the decoded columns in the way. Ctrl+C does the same as "Copy Row(s) as Text" on whatever's currently selected.

Frame Viewer

Below the table, decodes whichever transaction row is currently selected into its TX and RX Modbus frames side by side - not just the register values, but the actual frame structure: the MBAP header (Transaction ID, Protocol ID, Length, Unit ID) for TCP, or Unit ID plus CRC/LRC for RTU/ASCII, then the function code, data bytes, and any exception code, plus the raw hex for both directions along the bottom. Each row is decoded with the framing of the device it went to - so an RTU-over-TCP gateway's traffic is shown as RTU frames, next to another device's Modbus TCP frames.

Select a row by clicking it or by navigating with the arrow keys, Home/End, or Page Up/Page Down - the Frame Viewer updates immediately either way, and the table scrolls to keep the selected row visible even while new transactions keep arriving underneath you.

The Hide Frame Viewer button (next to Export CSV) collapses the panel so the table gets the tab's full height when you just want to scan through transactions - click it again (Show Frame Viewer) to bring it back; the last decoded frame is still there, not cleared.

Every part of the decoded frame can be selected and copied: right-click (or Ctrl+C on a selection) either the TX or RX field table for Copy Row(s) - tab-separated, ready to paste into a spreadsheet. The raw hex line along the bottom can be click-dragged to select and copied directly (Ctrl+C or right-click) like any selectable text, for pasting straight into a hex viewer or a bug report.

Trend

Graphs up to 20 pens per page (up to 10 pages) over time, either following the live clock or reviewing history. Useful for spotting slow drift, verifying a control loop is actually responding, or capturing a transient you can't watch a numeric table fast enough to catch.

Pages

The tabs above the graph are pages, each its own graph with its own 20 pens - e.g. one page per meter. + adds a page (up to 10); double-click a page tab, or right-click it, to rename it; right-click to delete it (the last page can't be deleted; a page with pens asks first). Every page keeps polling while the trend runs, so switching pages shows full live history; the time window, zoom, scroll and Graph Properties are shared. Record / Replay records the page shown when recording starts.

Running

Start Trend / Stop Trend poll every pen at the Interval (200-60000 ms, default 1000). Each pen reads from its own tag's device; a pen whose device is offline is skipped, and the trend only stops ("Trend Stopped") when no device at all is connected. Pens are added while the trend is stopped. Each pen keeps its latest 20,000 points.

Pens

Add Pen opens the Trend Pens grid for the shown page: 20 slots (SCADA-style) with the columns On, Tag, Label, Index, Scale and Color. Enable the ones you want, click the ⋮ button in the Tag column to pick a tag from the Select Tag popup, and set a color. With two or more devices the popup has a Device filter and lists each tag as "V1 (METER 2)"; the legend then shows the device too. A pen's type, address, count, and format all come from whichever tag you pick, not from separate fields here. Only tags on Holding or Input Registers with a numeric (or Hex) format, and calculated tags (which plot their computed value), show up in that list - Coils, Discrete Inputs, and the Bool format are left out, since a trend line is meant for continuously varying values rather than on/off state. If you need to watch a digital point over time, add it as a Tag and check its Read Value column instead. If none exist yet, the popup's tag list is just empty - click its Add Tag... button to jump straight to the Tags tab and create one (this closes the Trend Pens grid, since adding a tag needs the Tags tab visible).

  • Label - an optional legend name instead of the tag's name.
  • Index (0-124) - which decoded value to plot from a multi-register tag (0 = the first).
  • Scale - Auto (default) plots the tag's engineering value if it has scaling enabled (see Tags Monitoring), else the raw value - turning scaling on or off for the tag changes what the pen shows immediately; Raw always plots the raw decoded value; Scaled always uses the scaling and plots nothing if there is none. Scaling applies at Index 0 only.

Navigating

The View Range row sits above the graph: Time Window, From/To + Go, Auto Scroll, Zoom In/Out, and Hide Stats, which collapses the Min/Max/Average table below the graph so the graph gets the whole height (the table is otherwise only as tall as its pens need).

The Auto Scroll checkbox shows and controls whether the view is following the live edge. While checked, it keeps advancing to "now" as new data arrives. Scrolling the history bar away from its live end, zooming, or jumping to a From/To range all uncheck it automatically and leave the view exactly where you put it, however long the trend keeps running - new data doesn't interrupt you. Checking it again jumps straight back to the live edge and resumes following; dragging the scrollbar all the way back to its live end does the same. Everything plotted is just what's been collected in the current session - there's no separate historical database to switch into.

The scrollbar just below the graph pans through everything collected so far, including while the trend is actively running - drag it right to catch up to the newest data, or left to look back. Time Window picks how much time is visible at once (1 min to 24 hours). Zoom In/Out halves or doubles that span around wherever you're currently looking (between 5 seconds and 7 days). From/To plus Go jumps straight to a specific range.

Hovering the mouse over the graph drops a crosshair line and updates the legend below each pen's name with its value at that point in time, so you can read an exact number off the trace without switching to CSV logging. The Min/Max/Average table below the graph updates to match the hovered point too; move the mouse away and everything goes back to showing the current live values.

Graph Properties

Set the X and Y axis titles, background/axis/grid colors, whether gridlines are shown, and whether the Y axis auto-ranges to the data or uses a fixed Min/Max. Gridlines default to black; change them here if you'd rather have something more subtle for a printed report.

Logging and printing

Log to CSV appends a timestamped row per pen on every poll tick (with several pages, the pen name is prefixed with its page). Print saves the current graph view as a PNG image or a PDF document - useful for attaching evidence of a fault condition to a service report.

Detach

Detach pops the whole Trend view out into its own resizable, maximizable window that stays on top of the main window, so you can watch it while working in another tab (Tags, Script, ...) instead of switching back and forth. The Trend tab itself shows a red X while detached, as a reminder that the real view has moved. Click Fixed at the bottom of the floating window (or just close it) to dock the view back into its tab.

Server Mode

The Server tab makes ModbusLens act as a Modbus TCP slave device instead of a client - useful for testing your own SCADA/PLC program against a fake device, without needing real hardware on hand, or for validating a Script or Tag configuration before pointing it at production equipment.

Starting the server

  1. Pick the Mode: Simulate (default) or Gateway (see below).
  2. Set Server Address (default 0.0.0.0, accepting connections on any network interface), Port (default 5020, 1-65535), and Unit ID (default 1). The server only answers requests for that one Unit ID.
  3. Click Start Server. Mode and these settings are locked while it runs.

Once running, pick a Data Space (Coils, Discrete Inputs, Holding Registers, or Input Registers), set a Start Address (0-999) and Count (1-200, default 20), and click Load to view that range - each data space has 1000 addresses (0-999). Changing the Data Space reloads by itself, and the table refreshes every half second (skipping a cell you're editing).

Editing values

Double-click a Value cell to set it directly, as if you were the field device generating that reading. Any Modbus master that connects to this server sees the same value. Coils and Holding Registers are also writable by a remote master; Discrete Inputs and Input Registers are read-only from the network side (as in real Modbus), but you can still set them yourself from the GUI to simulate a live sensor. A coil/discrete input cell takes 1, true or on (anything else is 0); a register cell takes a whole number, kept to 16 bits.

One server at a time

Only one Server tab can be running at once, across every open window. ModbusLens builds the simulator on pymodbus's ModbusSimulatorContext/ModbusServerContext for the datastore and its StartTcpServer helper to run the listener. StartTcpServer spins up its own asyncio event loop in the thread that calls it and is meant to run one instance per process - it isn't designed to have two independent listeners active at the same time. ModbusLens runs it in a background thread and tracks the single active instance itself; starting a second one while another is running shows a Server Already Running message. Stop the first one to free it up.

Connecting to your own server

Add a device on the Overview pointing at 127.0.0.1 (or your machine's LAN IP) on the server's port - remember the server defaults to 5020 while a new device defaults to 502, so change one of them - with the server's Unit ID, and connect it. You then have a complete self-contained loop for testing Tags, Trend, or a Script with zero risk to real equipment, all in one window. (A Script can also skip the network and talk to the server directly with Target Server (Local).)

Gateway mode

Switch Mode to Gateway to turn the Server tab from a simulator into a real TCP-to-serial bridge: instead of answering from a local, manually-set datastore, every request that arrives over TCP is relayed to a real downstream serial (RTU/ASCII) device, and the device's actual response is returned - the same Unit ID is used on both sides, as a transparent passthrough. This is how you make a serial-only device (an old PLC, energy meter, VFD, ...) reachable from anywhere on the network, without buying dedicated gateway hardware.

Fill in the Downstream Serial Device group (COM Port, Baud, Parity, Stop Bits, Byte Size, Framing - defaults 19200, None, 1, 8, RTU) the same way you would in a device's Connection Settings, then Start Server as usual. Only function codes 1, 2, 3, 4, 5, 6, 15 and 16 are relayed (anything else gets Illegal Function), and only for the server's Unit ID - one gateway relays to one downstream unit. The COM Port box also accepts a pyserial URL such as socket://192.168.1.254:4196 for a device behind a transparent serial-to-Ethernet converter (raw RTU over TCP) - baud/parity are then set on the converter itself, not here. The Gateway Activity table replaces Data Space View while running, logging every relayed request live: time, direction, function, address, and result - including a real device exception (e.g. Illegal Data Address) passed straight through, or, if the downstream device doesn't respond or the serial connection itself is down, the standard Modbus gateway exception codes (Gateway Path Unavailable / Gateway Target Device Failed to Respond). The table keeps the last 500 rows; Clear Log empties it.

Like every other feature in ModbusLens, Gateway mode only runs while the app itself stays open - closing the window, or the PC sleeping or restarting, stops it. It's an interactive bridge for testing, commissioning, or temporarily sharing access to a device, not an unattended 24/7 production gateway (which would need to run headless, auto-restart, and survive indefinitely without a GUI open - a different kind of tool than ModbusLens is today).

Scripting

The Script tab runs small test sequences against a device using a purpose-built command language - not a general-purpose one, just enough to write values, wait, read them back, and repeat. It's meant for repeatable acceptance tests, burn-in sequences, and quick automated checks you'd otherwise click through by hand every time.

Commands

WRITE COIL <addr> = ON|OFFWrite a coil.
WRITE HR <addr> = <expr>Write a holding register.
WRITE <tag name> = <expr>Write to whatever type/address that tag is configured for.
READ COIL|DI|HR|IR <addr>Read a value and log it.
READ <tag name>Same, by tag name instead of type/address.
DEVICE <device name>Send the following commands to that device (name as on the Overview, any case, spaces allowed, quotes optional). A script starts on the device picked in the Script tab's Device dropdown. Client Connection target only. Add Tag/Insert Tag add this line for you when the tag is on another device than the one in effect at the cursor.
LET <name> = <expr>Assign a variable.
LOG <expr>Print text/numbers to the console.
WAIT <expr, ms>Pause without freezing the UI.
REPEAT <expr> ... ENDLoop a block of commands a fixed number of times.
REPEAT UNTIL <expr> <op> <expr> ... END Loop until a condition becomes true, checked before each pass.
IF <expr> <op> <expr> THEN <command> Run one command conditionally. op is == != > < >= <=.
ASSERT <expr> <op> <expr> Check a condition and record PASS/FAIL in the Results panel. A FAIL is logged and recorded but does not stop the script - it's a check, not a stop condition. A genuine error while evaluating it (e.g. a failed read) still stops the script, same as any other command.

Syntax basics

  • Lines starting with # or // are comments; blank lines are ignored.
  • Commands are case-insensitive. Numbers can be decimal (12, 1.5, -3) or hex (0x1F); addresses too.
  • Addresses in a script are always raw 0-based protocol offsets (HR 0 is the first holding register, 40001 in 1-based terms), whatever the Tags tab's 0-Based setting.
  • A coil value in WRITE is ON/OFF, 1/0 or TRUE/FALSE. Inside expressions and comparisons use 1/0 (ON there would be read as a name).
  • A register WRITE sends a whole 16-bit number: decimals are truncated and negatives wrap (-1 is written as 65535).
  • IF ... THEN runs one command, which can't be REPEAT, END or another IF.

Expressions

An expression can mix numbers, "strings", variables, parentheses, and + - * / (/ gives a decimal result). Writing a bare HR 0 inside an expression reads that register inline (shorthand for READ HR 0); a bare tag name works the same way (e.g. LET x = Boiler_Temp + 1 does a fresh read of the Boiler_Temp tag). A tag reads the way the Tags tab shows it: its Count registers decoded with its Format (F32, a _SWAP word order, ...), and its Engineering Value if scaling is on - so LET v = V1 on an F32 meter tag gives the voltage, not a raw word. READ <tag> logs that value, and WRITE <tag> = <expr> on a multi-register tag encodes the value with the tag's Format and writes all its registers at once. The tag's Address is converted with the Tags tab's 0-/1-based setting, so it hits the same register the Tags tab does. Tag names are case-sensitive; with several devices, the current device's tag of that name is used (else the first one found on any device); [DEVICE NAME].TAG picks a tag on a specific device. A tag is always read from and written to its own device, wherever the script currently points. A calculated tag gives its latest computed value (Tags monitoring must be running). A tag name is only tried if the name isn't already a variable you've assigned with LET - a LET variable always takes priority over a tag of the same name. + also concatenates text with numbers, so LOG "value is " + x works as expected. Types: COIL, DI (Discrete Input), HR (Holding Register), IR (Input Register).

Compile and Run

Compile checks the script's syntax without touching the device - use it to catch typos before running anything. Run executes the script step by step; because it can write to a live device, Run on a Client Connection target shows a warning first (with a "Don't remind me again" option, which is remembered even if you then Cancel) reminding you to be careful on in-service equipment. Stop halts a running script at any point. Open... and Save... load and save scripts (.mls or .txt), and Clear Console empties the console (which keeps the last 5000 lines and is also copied to System Logs with a [Script] prefix). Every script READ/WRITE also appears in the Raw Data tab as "Script READ/WRITE <type> <addr>" with its device, and joins the same busy-range safety interlock as Tags - a clash logs "... SKIPPED -- safety interlock: range busy".

Target and Device

The Target dropdown picks whether the script talks to a real device (Client Connection) or to ModbusLens's own Server tab (Server (Local)), so you can dry-run a sequence safely with no real device attached - start a server in Simulate mode, switch the script to Server (Local), and run it exactly as it would run against the real thing. A Server (Local) script can write all four types (it's simulating the device itself), uses the server's 0-999 addresses, and doesn't work while the Server tab is in Gateway mode.

With two or more devices, a Device dropdown next to Target picks which device a Client Connection script starts on - it's hidden entirely while Target is Server (Local), since there's no device to pick in that mode; a DEVICE line switches device mid-script, so one script can work with several meters. The device must be connected to Run.

Variables panel

The panel on the right lists every variable your script assigns with LET, updating live as the script runs - no need to sprinkle LOG lines everywhere just to see what a variable currently holds. It populates as soon as you Compile (values blank until the script actually runs), keeps updating on every step while running, and holds the last values after the script finishes or is stopped, so you can still read them afterward.

Assertion Results panel

Below the Variables panel, this table logs every ASSERT outcome as the script runs: the assertion text, a color-coded PASS/FAIL/ERROR status, and the actual values compared. Unlike a failed read or write, a failed assertion doesn't stop the script - so a single run can report every check's outcome, not just the first failure - which makes ASSERT the right tool for a repeatable acceptance test where you want a full pass/fail summary at the end. It's cleared each time you Compile or Run.

Other tools in the editor

  • Add Tag - opens a popup listing every tag on the Tags tab (any type, not just analog), and picking one drops its name straight into the script at the cursor. If the tag you need doesn't exist yet, the popup's own Add Tag... button jumps to the Tags tab with a new, blank row ready to name and configure.
  • Insert Tag - the right-click menu shortcut for the same thing: drops a tag's name straight into the script at the cursor, so you don't have to remember or retype it - the script then resolves it against whatever that tag is currently configured as (see WRITE/READ above), so editing the tag later doesn't require touching the script.
  • CPU usage indicator - shows live system CPU load ("CPU: --" if it can't be measured), useful for spotting a runaway loop that's spinning the interpreter faster than intended.
  • With several devices, the tag popup has a Device filter and shows each tag's device.

Sample Scripts

A few complete, working examples to adapt - copy one into the editor, adjust the addresses for your device, and Compile before Run.

1. Basic write/read/log sequence

LET x = HR 0 + 10
WRITE HR 1 = x
WAIT 500
LOG "HR1 is now " + x
IF HR 1 >= 100 THEN LOG "over threshold"

Reads holding register 0, adds 10, writes the result to register 1, waits half a second for the device to settle, then logs and checks it against a threshold.

2. Toggle a coil N times (blink test)

REPEAT 5
    WRITE COIL 0 = ON
    WAIT 250
    WRITE COIL 0 = OFF
    WAIT 250
END
LOG "Blink test complete"

Good for a quick relay/output wiring check - watch the physical output or an LED toggle five times, half a second per cycle.

3. Poll a register until it reaches a target value

LET attempts = 0
REPEAT 60
    LET attempts = attempts + 1
    IF HR 2 >= 500 THEN LOG "Target reached after " + attempts + " checks"
    WAIT 1000
END
LOG "Done polling"

Checks holding register 2 once a second for up to a minute - useful for waiting on a startup sequence, a warm-up temperature, or any value that changes slowly on its own. Plain REPEAT doesn't have a break/exit, so this always runs the full 60 checks even after the target is reached; it's a bounded polling window with a built-in timeout. See the next example for a version that stops the instant the condition is met.

4. Wait until a value is reached, no fixed check count

REPEAT UNTIL HR 2 >= 500
    WAIT 1000
END
LOG "Target reached"

Same idea as the previous example, but stops the moment holding register 2 hits 500 instead of always running a fixed number of checks - and if it's already >= 500 before the loop starts, the body never runs at all (the condition is checked before each pass). If the condition never becomes true, this stops on its own with a clear error after a very large number of iterations rather than hanging forever - see Limits below.

5. Ramp a setpoint up gradually

LET setpoint = HR 10
REPEAT 10
    LET setpoint = setpoint + 5
    WRITE HR 10 = setpoint
    LOG "Setpoint now " + setpoint
    WAIT 2000
END

Steps a holding register up by 5 every 2 seconds instead of jumping straight to a final value - useful for equipment that shouldn't see a large setpoint change all at once.

6. Read several points and log them together

LET temp = HR 0
LET pressure = HR 1
LET running = COIL 0
LOG "Temp=" + temp + " Pressure=" + pressure + " Running=" + running

A one-shot snapshot across mixed types (registers and a coil) in a single readable log line - handy at the start or end of a longer script to record a baseline.

7. Conditional checks with IF

LET temp = HR 0
IF temp > 90 THEN LOG "WARNING: temperature high (" + temp + ")"
IF temp < 10 THEN LOG "WARNING: temperature low (" + temp + ")"
IF temp == 0 THEN LOG "Sensor may be disconnected"
IF COIL 0 != 1 THEN WRITE COIL 1 = ON

Each IF only runs one command when its condition is true, and there's no ELSE - that's why this reads as a sequence of independent checks rather than a single branching block. The last line shows an IF driving a WRITE instead of a LOG: turn on coil 1 (e.g. an alarm lamp) whenever coil 0 (e.g. "running") isn't set. Valid comparisons are == != > < >= <=, and either side can be a register/coil read, a variable, or a literal number.

8. The same thing, by tag name instead of type/address

IF Boiler_Temp > 90 THEN LOG "WARNING: temperature high (" + Boiler_Temp + ")"
WRITE Pump_Enable = ON

Assumes a Boiler_Temp (Holding/Input Register) and Pump_Enable (Coil) tag already exist on the Tags tab - use Insert Tag to drop the name in without retyping it. Reads and writes the tag with its own Format and scaling, and stays correct if that tag's address ever changes, since the script only cares about the name.

10. Two meters in one script

DEVICE METER 1
LET a = HR 1699
DEVICE METER 2
LET b = HR 1699
LOG "Raw V1 word: meter 1=" + a + " meter 2=" + b

Reads the same register from two devices on one gateway by switching with DEVICE.

9. Acceptance test with ASSERT

WRITE HR 0 = 100
WAIT 200
ASSERT HR 0 == 100
ASSERT COIL 0 == 1
LOG "Acceptance test complete - see Results panel"

Writes a value, then checks it and a coil's state landed as expected. Both checks are recorded in the Results panel with PASS/FAIL - a FAIL doesn't abort the script, so every assertion below it still runs and reports, giving you a complete pass/fail summary in one run.

Limits

To keep a typo from hanging the app or running forever: a script can have at most 5000 instructions, a REPEAT count at most 1,000,000 (REPEAT UNTIL shares that iteration cap - if the condition never becomes true, it stops with an error instead of looping forever), a WAIT at most 86,400,000 ms (24 hours), and expressions at most 100 levels of nesting; WAIT and REPEAT values can't be negative. A loop with no WAIT runs up to 200 instructions at a time, then hands control back to the interface for at least 20ms before continuing - so it never freezes the app, but a WAIT-less write loop still sends bursts of writes; put a WAIT in any loop that talks to a real device. See Troubleshooting for what the common error messages mean.

Scanner

Auto-discovers which addresses respond on a device - useful when you don't have a register map yet. Works the same way over TCP or serial. With two or more devices, the Device dropdown picks which device to scan; the line next to it shows "Target: <target> (Unit N)", or that the device isn't connected yet.

Pick a Function (Coils/Discrete Inputs/Holding/Input Registers; default Holding Registers), a Start/End address (raw 0-based offsets, 0-65535; default 0-999) and a Probe timeout (50-5000 ms, default 300), then Start Scan; Stop ends it early and Clear Results empties the grid and its page tabs. Rather than checking one address at a time, it probes the largest block the function allows first (125 registers or 2000 coils/inputs, with a short pause between probes) - a clean read means every address in that block responds. If a block doesn't fully respond, it's split in half and each half is probed again, narrowing down until it knows exactly which individual addresses do and don't respond. This is far fewer requests than a naive one-by-one sweep whenever most of a range is contiguous, which is the common case for a real device's register map.

Results show as a color-coded grid, one cell per address, labeled with its actual address: light green once that address is confirmed to respond, light red once it's confirmed not to, amber if it timed out with no response at all even after narrowing all the way down to that one address (inconclusive - not a confirmed answer either way, just try scanning it again) - cells stay neutral until the scan actually reaches them. A legend above the grid spells out what each color means, and its swatches update to match whenever Colorblind-friendly colors is toggled. A range too large for one screen splits into page tabs along the bottom, Excel-sheet style, each labeled with its own address range (e.g. "1700-1909", "1910-2000"); switching tabs keeps showing whatever's already been resolved, so flipping back to an earlier page doesn't lose anything. Text size (px) scales the grid's cells and text together for readability, and Colorblind-friendly colors swaps the green/red palette for a blue/orange one that's distinguishable under color vision deficiency - the amber "timeout" color becomes plain gray in that mode instead, so it stays distinct from the palette's own orange.

A device that returns Illegal Function for the whole range stops the scan immediately, since every address would fail the same way - try a different Function instead. A genuine timeout with no response at all is treated like any other inconclusive block, though: it's narrowed down the same way a device-exception reply is, and only marked amber at the single address level once narrowing can't get a clean answer - the rest of the range keeps scanning rather than the whole scan stopping, since one bad reply (common on a flaky link, especially one shared with another Modbus master) doesn't mean the device itself is gone. Only a real dropped connection stops the scan early; addresses it never reached stay neutral in the grid. Scan start/stop, the final summary (responding addresses as merged ranges, plus how many came back inconclusive) and any error all go to Diagnostics > System Logs, not a box inside the Scanner tab itself.

Reuses the device's existing connection rather than opening a second one. So nothing else polls while a scan is in progress, starting one stops Tags monitoring (for every device) and Address Table Live Monitoring - restart them yourself afterwards - and pauses Trend polling and the auto-reconnect watchdog, which resume when the scan ends. A scan is refused while a Script is running ("stop it before starting a scan").

A shorter Probe timeout makes a scan faster but can misreport a slow-but-valid address as not-responding, especially over a serial connection where every probe is one real bus round-trip. If a scan seems to be missing an address you know exists, try a longer timeout.

Don't know the serial connection parameters (baud rate, parity, stop bits) for a device in the first place? See Serial Discovery.

Create Tags From Scan

Once a scan finds responding addresses, Create Tags... opens a dialog listing the merged ranges found - check the ones you want and click Create Tags to generate one new row per individual address in the Tags tab, saving you from adding them one at a time by hand. Each generated tag is named with the classic 5-digit Modicon convention (type digit plus a 4-digit 1-based address, e.g. HR_40001 for a Holding Register, COIL_00001 for a Coil) so it stays addressable and unambiguous even without a real register map. An address that already has a tag of that type on that device is skipped rather than creating a duplicate. The tags are created on the device that was scanned, with the Function that was scanned, and use the Tags tab's 0-/1-based setting for their Address; the Tags tab opens afterwards.

Exporting Results

Export CSV... (next to Create Tags...) saves what the last scan resolved to a CSV file you can open in Excel: one row per address, with the Device, Unit ID, Function, Protocol Address (the raw 0-based offset), Tag Address (the same address in the Tags tab's 0-/1-based setting) and a Status of Responds, No response, or Timeout (inconclusive). It is enabled once a scan has finished or been stopped - an address a stopped scan never reached is left out - and greyed out again by Clear Results or a new scan. Find Devices and Serial Discovery have the same button for their list of found devices.

Serial Discovery

Diagnostics > Serial Discovery sweeps common baud rate/parity/stop-bit/Unit ID combinations against a COM port to find which one a serial device actually speaks, for when its settings aren't documented. The same sweep is also available in Diagnostics > Find Devices (Transport: Serial (Parameter Sweep)), which the Find Devices... button in a device's Connection Settings (Serial) opens.

Pick the COM Port and Framing (RTU/ASCII), a Start/End Unit ID range (0-255) to also try, and a Per-trial timeout (50-2000 ms, default 200), then Start Scan. It tries every combination of 8 common baud rates, 3 parity settings, 2 stop-bit settings, and each Unit ID in the range - byte size is fixed at 8, the near-universal default - opening a short-lived connection for each combination and sending one Holding Register read. Any reply, including a Modbus exception response, counts as a match, since that still proves the framing decoded correctly; a garbled response from a mismatched baud rate won't parse as a valid Modbus frame at all.

A match isn't always unique. Stop bits (and occasionally parity) are framing bits, not data - many UART/USB-serial adapters only check for at least one high bit-time before the next start bit, so a receiver set for 1 stop bit is easily satisfied by a sender actually using 2, and vice versa. It's normal to see the same baud/parity/Unit ID reported as a match at both stop-bit settings. When that happens, prefer whatever the device's own documentation or configuration screen actually says over guessing from the scan alone - the scan proves "a read got a response," not "these are the device's exact settings."

Matches are listed under "Matches found (select one, then Apply)". Select one and click Apply to Device... (or double-click it) to open Connection Settings pre-filled with it - for the device you pick, if there are several - then Save Settings. The dialog doesn't block the main window; closing it stops the scan. Export CSV... saves every match found so far (Port, Baud Rate, Parity, Stop Bits, Framing, Unit ID) to a file.

This doesn't reuse the app's shared connection - it opens its own for each combination, since testing a physical serial setting means actually reopening the port with it. That also means the port needs to be free: if a connected device in this window uses the port, the scan refuses to start ("... is in use by a connected device -- disconnect it first"); also close any other program (Modbus Poll, a terminal, another ModbusLens window) that might have it open. If the port can't be opened at all, the scan stops immediately with that message rather than repeating the same failure for every remaining combination.

Keep the Unit ID range narrow (it defaults to just 1) unless you actually need it wider - each additional Unit ID multiplies the total combination count by 48.

Diagnostic Functions

Diagnostics > Modbus Diagnostic Functions reaches the Modbus function codes beyond basic reads/writes - niche next to everyday polling, but a real gap for compliance and interop testing. With two or more devices, pick the Device at the top first. Then pick a function from the dropdown, fill in the couple of parameters it needs (most need none at all), and Run. The result - or "Failed: ..." with the device's error - shows in the box below and is also written to System Logs. Hex fields accept spaces, commas and 0x.

  • Read Exception Status (FC07) - an 8-bit vendor-specific status byte, a lightweight "is anything wrong" poll some devices support without a full register read.
  • Diagnostics: Loopback / Query Data (FC08-00) - sends bytes you type in Message (hex) (default 1234) and expects the device to echo them back unchanged - a pure comms sanity check that never touches a real register.
  • Diagnostics: Restart Communications (FC08-01) - asks the device to reinitialize its comm port; Clear event log/counters too (ticked by default) also clears its event log.
  • Diagnostics: Read Diagnostic Register (FC08-02) - device-specific status bits (e.g. listen-only mode); the meaning beyond the raw bits is vendor-defined.
  • Diagnostics: Clear Counters (FC08-0A) - clears the device's own diagnostic counters and register.
  • Get Comm Event Counter (FC11) / Get Comm Event Log (FC12) - a free-running counter the device bumps on every completed transaction, plus (for the Log) a short history of recent bus events and a ready/busy status flag.
  • Report Server ID (FC17) - a vendor-defined identifier string plus a run/stop indicator, historically called "Report Slave ID."
  • Read File Record (FC20) / Write File Record (FC21) - reads or writes records in the device's file storage, a second address space separate from registers/coils, mostly seen on energy meters and similar data loggers. Specify the File Number (default 4), Record Number, and (for a read) the Register Count (1-120), or (for a write) the Data (hex) (default 0001 0002).
  • Mask Write Register (FC22) - sets a register to (current_value AND and_mask) OR (or_mask AND NOT and_mask) atomically on the device, so changing a few bits doesn't race against another master's write to the same register between a plain read and write. Enter the Address, AND Mask (hex) (default FFFF) and OR Mask (hex) (default 0000).
  • Read FIFO Queue (FC24) - reads a FIFO queue's current contents (without removing them) from a pointer register, for devices that buffer captured values faster than a master polls them.
  • Read Device Information (FC43) - vendor name/product code/version and similar text objects, a standardized alternative to a vendor-specific register for "what device am I talking to." Read Code selects Basic, Regular, Extended, or a single specific object via Object Id.

Every function here goes through the chosen device's connection like everything else in ModbusLens - the device has to be connected ("... isn't connected -- connect it first"), and a run briefly uses its link like any other read/write.

Data Decoder

Diagnostics > Decode Registers opens a standalone "paste hex, see every interpretation" tool - no connection required, and no live Tag involved. Useful when you have a raw value from somewhere (the Raw Data tab, a device's datasheet, a captured frame) and don't know how to interpret it, without creating a Tags-tab row and guessing a Format against a live device.

Type or paste the raw hex bytes - spaces, commas, and 0x prefixes are all fine (41 48 00 00, 0x41,0x48,0x00,0x00, and 41480000 all work the same). The results table updates live as you type: U16, S16, HEX, and Binary per register, plus U32/S32/F32 when the number of registers is a multiple of 2 and U64/S64/F64 when it's a multiple of 4, ASCII (only shown when every byte is printable text) and BCD (only shown when every nibble is a valid 0-9 decimal digit), and each register's individual bits as a 16-character binary string. An odd trailing byte (half a register) is ignored; an odd number of hex digits or a non-hex character is reported right away.

The Byte/word order dropdown covers all four standard orderings - ABCD (plain big-endian Modbus, the default), BADC (the two bytes within each register swapped), CDAB (register order reversed - the same idea as the Tags table's _SWAP formats), and DCBA (both). Switching it re-decodes the same bytes immediately, no re-typing needed - useful for eyeballing which ordering actually makes a device's value make sense (e.g. a plausible-looking float vs. an implausible one). ASCII and BCD are always read in the order you typed them, independent of this dropdown, since reordering bytes doesn't make sense for text or packed-decimal data the way it does for a number.

It doesn't block the main window (like Find Devices and Serial Discovery) - it's meant to sit alongside the Raw Data tab or an external datasheet while you work.

Multiple Windows

One window already handles any number of devices (see Overview), so you rarely need a second one. File > New Window still opens a fully independent ModbusLens window - its own device list, Overview, Address Table, Profiles, Tags, Raw Data, Trend, Server, Script, and Scanner tab - for keeping two unrelated jobs apart.

Differences from the first window:

  • A new window starts with a single "Device 1" (127.0.0.1:502, Unit 1). The first window keeps its device list between runs; use Save Session to keep another window's devices.
  • Only one Server tab can be actively running at a time, across all open windows (see the Server Mode topic for why).

Troubleshooting

Symptom-first reference for the problems you're most likely to run into. If something here doesn't cover your case, the Status Log (Address Table) or the console (Script tab) usually has a more specific message worth reading closely.

Can't connect over TCP

When a device's Connect fails, a "Connection Failed" message shows "Couldn't connect NAME -- target, Unit N", the error, and a short checklist ("Check the IP address and port, network reachability (VPN/Tailscale routes too), and that the device/gateway accepts another connection."); Connect All lists every device that failed under "These devices couldn't connect:". The reasoning behind each check:

  • Is the Modbus server actually running? A gateway or PLC that's powered on but hasn't started its Modbus service will refuse the connection outright.
  • IP address and port correct? Double check for typos, and that you're not pointing at a different device's management IP instead of its Modbus interface.
  • Network connectivity? Try pinging the target first - if that fails, the problem is routing/cabling, not Modbus.
  • Unit ID matches? Some gateways route by Unit ID to different downstream serial devices; a wrong ID can connect fine but every read/write then fails or returns the wrong device's data.
  • Firewall? Windows Firewall or a network firewall blocking outbound port 502 (or whatever port you configured) will look identical to the device being offline.
  • Gateway connection limit? Small serial-to-Ethernet gateways often accept only a few TCP clients. Devices with identical settings share one connection, but two devices that differ in Fast LAN Mode, framing or interface open two.

Can't connect over RTU (Serial)

  • Does the COM port exist and is it free? Only one application can hold a serial port open at a time - close Modbus Poll, a terminal program, or another ModbusLens window that might already have it open.
  • Baud rate, parity, stop bits match the device? A mismatch here doesn't always fail cleanly - it can connect and then return garbage or timeouts instead of an obvious error.
  • Cable and power? Check the USB-to-RS485/RS232 adapter is recognized by Windows (Device Manager) and the device itself is powered.
  • Unit ID matches the device's configuration? Same reasoning as TCP above.
  • "COMx is already used by "NAME" with different settings" - in one window, every device on a COM port must share its baud rate, parity, stop bits and framing (they're on the same wire). Match the settings, or use a different port.

Values look exactly one address off

This is almost always the 0-based vs 1-based addressing setting. Toggle the 0-Based Addressing checkbox on the Address Table or Tags tab and compare - see the Connecting topic for the full explanation.

A 32-bit or 64-bit value (U32/S32/F32, U64/S64/F64) looks like nonsense

Try the _SWAP variant of the same format. Different vendors order the registers of a multi-register value differently, and there's no reliable way to detect which one a device uses - it's trial and error. The Tags table's Raw (Hex) column shows the untouched register bits regardless of format, which is the fastest way to confirm your mapping once you find the right combination.

A write silently didn't happen

  • Check the Status Log (Address Table), the tag's row, or the Raw Data tab - a rejected write shows a specific reason rather than just failing quietly, and Raw Data will show it as a Failed row with the error in the Value column.
  • If you've configured a write bound (Min/Max) on that register, a rejected write logs "Write rejected: value ... is outside the configured write bound [...]". This applies no matter whether the write came from the Address Table, a Tag, or a Script.
  • Discrete Inputs and Input Registers are read-only in the Modbus spec itself - no amount of configuration in ModbusLens will make them writable, because the device won't accept it either.

A tag shows ERROR in Tags Monitoring

  • Check the tag's Count matches its Format - 32-bit formats (U32/S32/F32, and their _SWAP variants) need a count that's a multiple of 2; 64-bit formats (U64/S64/F64, and their _SWAP variants) need a multiple of 4.
  • Check the address is actually valid on the device - some devices have gaps in their register map that return an exception rather than a value.
  • One failing tag no longer stops the rest of the list from updating, so if only one row shows ERROR while the others keep ticking, the problem is specific to that tag's configuration, not the connection.
  • If every tag shows ERROR at once, monitoring will auto-stop after three consecutive failed polls - that's treated as a lost connection rather than a tag problem (with several devices, one device dropping doesn't stop monitoring; only its tags show ERROR). ModbusLens will retry the connection itself automatically (see the next entry); you shouldn't need to do anything unless it can't recover.

Status shows "Reconnecting..." and it's taking a while

This is expected - once connected, ModbusLens watches each link and auto-retries with increasing delays (2s, 4s, 8s... capped at 30s, checked every few seconds) if it drops, rather than requiring a manual reconnect. If Tags monitoring was running and stopped because every tag failed at once, it resumes automatically the moment the connection recovers. If it's still stuck reconnecting after a while, the underlying cause is the same as an initial connection failure - work through the TCP or Serial checklist above (device power, cabling, IP/port, Unit ID). Clicking the device's Disconnect (Overview card) or Disconnect All stops the retry loop, if you want to give up on it.

"Duplicate Addresses" or "Overlapping Ranges" when starting monitoring

Two tags of the same Modbus type on the same device are pointing at the same address ("Duplicate Addresses" - monitoring won't start until you change them) or at overlapping ranges ("Overlapping Ranges" - a warning you can continue past). This is usually a copy-paste mistake when building a large tag list - check the Address column against your register map. The same address on two different devices is fine. ("Duplicate Address: No free address available" means a new tag couldn't be moved to a free address of that type.)

"Server Already Running"

Only one Server tab can be active at a time, across every open ModbusLens window, because the underlying Modbus library only supports one active server per process. Stop the other one first.

A script won't Compile

Compile errors describe exactly what's wrong and where, for example:

  • REPEAT without matching END / END without matching REPEAT - a REPEAT block wasn't closed, or an extra END has nothing to close.
  • unrecognized command: ... - a typo in a command keyword.
  • IF...THEN cannot contain REPEAT, END, or a nested IF - IF runs a single command.
  • unknown type 'X' (use COIL, DI, HR, or IR), invalid ON/OFF value: ..., REPEAT requires a count or 'UNTIL <condition>', DEVICE requires a device name, e.g. DEVICE METER 2, script exceeds the 5000-instruction limit.
  • invalid address: ... / address ... out of range (0-65535) - the address after a type (COIL/DI/HR/IR) isn't a valid number in range.

A script won't Run (compiles fine, fails immediately)

  • Not Connected: "Connect NAME before running a Client Connection script." - the device in the Script tab's Device dropdown isn't connected. Connect it first, or switch Target to Server (Local) to test against the Server tab instead. If a device disconnects mid-run (or a DEVICE line names one that isn't connected) the script stops with NAME is not connected.
  • Server Not Running: "Start the Server tab before running a Server (Local) script." - the opposite case (Server is not running - start it on the Server tab first if it stops mid-run).
  • unknown device '...' / DEVICE only applies to a Client Connection script - a DEVICE line names a device that doesn't exist, or the script targets the Server.
  • unknown tag '...' / undefined variable or tag '...' - a name that's neither a LET variable nor a tag (tag names are case-sensitive; ON/OFF aren't values inside an expression - use 1/0).
  • division by zero, expected a number here, got text, WAIT duration exceeds the 86400000ms limit, REPEAT count exceeds the 1000000 limit.
  • ... is read-only on a real device - a WRITE targeted a Discrete Input or Input Register, which are read-only by the Modbus spec.
  • read failed for Holding Register 12 (or read failed for tag '...') - the read inside an expression failed against the live device (or the range was busy); check the address is valid, the same way you would for a Tags tab ERROR.
  • REPEAT UNTIL exceeded the 1000000-iteration limit without the condition becoming true - the condition never became true; double-check the address/comparison, or that the device is actually changing the value you're waiting on.

Network Discovery isn't finding a device, or feels slow

Diagnostics > Network Discovery & Diagnostics scans the local network and checks which devices respond to Modbus - useful when you know a PLC is on the subnet but don't know its current IP. It combines a few techniques:

  • Fast TCP scan (the default) - probes the subnet's addresses in parallel for an open Modbus port and checks each hit really answers Modbus; a /24 takes about a second. No extra software needed.
  • ARP Mode (optional) - adds MAC address/vendor lookup and packet capture. This is the part that needs Npcap; without it, ARP Mode falls back to a slower ping-and-ARP-table sweep that misses devices on networks that block ICMP.
  • Device filtering - "Show only Modbus devices" hides everything else from the list, and Scan Unit IDs (1-247) asks each Modbus host which Unit IDs respond.

The first device found fills in the dialog's own IP/Port fields for its diagnostics. To set a found device up as a ModbusLens device, use Diagnostics > Find Devices, whose Apply to Device... fills in a device's connection for you.

For ARP Mode, install Npcap with WinPcap compatible mode enabled during setup, then restart ModbusLens - see the README's Notes section for the download link and exact install options.

The scan range is sized to the selected interface's actual subnet mask, not always a flat /24 - on a /25 or smaller it only probes that real range, and on anything wider than /24 (e.g. a VPN-routed /16) it's capped to the /24 containing that interface's own IP, since probing tens of thousands of addresses one TCP connect at a time isn't practical. If a device you know exists on a wider VPN subnet doesn't turn up, it may simply be outside that /24 - enter its IP directly instead of relying on discovery. The progress bar shows the specific IP currently being probed, not just a percentage, so you can see exactly where a scan is up to.

"Check for Updates" fails or times out

The Updates tab in Help > About queries GitHub directly and needs outbound internet access. If it can't reach GitHub (offline, a proxy, or a firewall blocking it), it reports the failure rather than hanging, and gives you a direct link to the Releases page to check manually.

Tips & Safety

  • ModbusLens can both read and write live Modbus values. An incorrect write to a production device can cause unexpected motion, changed setpoints, or bypassed safety logic. Know the device's register map and have authorization before writing to anything real.
  • Use Server Mode as a local practice target: start a server, add a device at 127.0.0.1 on the server's port (5020 by default) in the same window - or point a Script at Target Server (Local) - and try things out, including a full Script or Tag list, before pointing at real equipment.
  • For anything that writes automatically and repeatedly (a Script, or a Tag in Write mode with monitoring active), consider setting a write bound (Min/Max) on the target register first - see the Address Table topic. It costs nothing when everything is working, and catches a typo the moment it would otherwise reach the device.
  • With several devices, check which device a tool is pointed at before writing: Address Table, Script and Scanner each have their own Device dropdown, and Write All on the Tags tab writes the open device tab's tags (or every device's, on the All tab).
  • If something isn't behaving as expected - wrong values, failed writes, a script that won't run - check Troubleshooting before assuming it's a device problem; most of the common causes are configuration mismatches on this end (addressing mode, word order, Unit ID) rather than a fault on the device.