Skip to content

Free tool

modbus-address-scan — the register a point really asks for

A Modbus point carries two settings that both have to be right and that never check each other: the address, which is a string plus a format enum, and the register type. modbus-address-scan reads the driver out of a Niagara installation with javap, works the boundary values out of the constants it finds, and prints what a point of each format asks for.

  • Modbus RTU and TCP
  • Reads the shipped jars
  • No station needed
  • Read-only
  • Python 3
  • MIT

Install and run

One file, standard library only, and no station involved: it reads jars on disk. It needs a Niagara installation to read and a javap on the path, which the JDK that comes with Niagara already provides. Point it at the installation or set NIAGARA_HOME.

# download it next to wherever you are working
curl -O https://plantroomlabs.com/tools/modbus-address-scan.py

# read the Modbus driver out of an installation
python3 modbus-address-scan.py /opt/Niagara/Niagara-4.15.5.22

What it printed here

This is the whole of one run against the installation on the machine that built this page, pasted by the script that publishes it rather than retyped. The headline is in the first block: a new point reads its address as base 16, so a register documented as 40001 and typed in as 40001 goes out as register 1, and the point reads, goes ok, and is wrong.

$ python3 modbus-address-scan.py $NIAGARA_HOME
Modbus addressing: what a point is before anyone configures it
======================================================================

read from /opt/Niagara/Niagara-4.15.5.22
javap:    1.8.0_504

A point's address is two slots, and these are their defaults:
  addressFormat  hex        (the formats are hex, decimal, modbus)
  address        "0"
  BAddressFormatEnum.DEFAULT is hex as well.

  So a freshly made point reads its address as base 16. A number
  copied out of a device's Modbus documentation - 40001, 1000, 2000 -
  is a different number by the time it reaches the wire, and nothing
  in the point says so.

getDataAddress(), the method the poll path calls, by format:
  hex       Integer.valueOf(address, 16). No offset, no bounds.
  decimal   Integer.valueOf(address). No offset, no bounds.
  modbus    parse, then subtract by band, highest band first:
              > 40000  ->  n - 40001
              > 30000  ->  n - 30001
              > 20000  ->  n - 20001
              > 10000  ->  n - 10001
              else     ->  n - 1
  getDataAddressNoModbusAltering() parses and stops - it is the one
  the band predicates use, not the one the poll path uses.

The comparisons are if_icmple, so each band's own lower bound falls
through into the band below it. Working the boundaries through:
  modbus 40000  -> data address 9999
  modbus 30000  -> data address 9999
  modbus 20000  -> data address 9999
  modbus 10000  -> data address 9999
  All 4 of them land on the same register: 9999. Four different
  addresses, four different conventions, one register read.

isValid() is the only gate between a typed string and a poll:
  modbus format     0 <= n < 50000
  every other format    n >= 0, and that is the whole test.
  Modbus addresses are 16 bits. There is no 65535 anywhere in
  isValid(), so a hex or decimal address of any size is valid.

Which register type gets read is a separate property:
  determineRegisterType() returns holdingRegister or inputRegister
  purely on the point's own regType slot (holding/input). It never looks
  at the address or at any of the isModbus*Address predicates.
  regType defaults to holding.

  So the number and the function code are independent. A modbus-format
  address of 30001 - input-register numbering by every convention -
  resolves to data address 0 and, on a default point, is read with
  the holding-register function code. Nothing objects.

The six isModbus*Address predicates classify an address into a band,
and 3 of the 6 use if_icmple on the lower bound, so the bound itself
is in no band at all. The predicates parse base 10 directly and return
false for any non-modbus format, which makes them silent on exactly the
addresses the default format produces.

What ModbusReadRequest does with the number it is given:
  the constructor stores startAddress straight into the field. There
  is not one comparison in it.
  writeRtu and writeTcp emit two bytes: (a & 65280) >> 8, then a & 255.
  That is a 0xFFFF mask applied by omission. Everything above 16 bits is
  dropped with no exception, no log line and no status flag.

getRegisterCount by data type: 4, 3, 2, 1 registers per point, so one point's
span can read its address plus 3. The mask above is applied to the
start address, and the point count goes through the same two bytes.

Three consequences worth designing for
----------------------------------------------------------------------
  1. The default format is hex. A documented address of 40001 typed into
     a new point is 262145, which isValid() accepts, and the frame carries
     it as register 1. The point reads, goes ok, and is wrong.
  2. Nothing in the chain knows the protocol is 16-bit. The only bound
     in isValid() applies to the one format a new point does not have.
  3. The address convention and the function code are two properties
     that can disagree in silence. A driver or a tool that writes
     points should set both, and should refuse the four band boundaries.

Three checks one station settles in an afternoon
----------------------------------------------------------------------
  1. Make a point, leave the format alone, type 40001, and watch the
     frame: the address bytes should be 00 01.
  2. Set the format to modbus and try each of 40000, 30000, 20000, 10000
     in turn. If they all read one register, this install behaves
     like this one.
  3. Set regType to holding, set the format to modbus, and use an address
     in the 40001-50000 band. Confirm which function code goes out: the
     number does not pick it, the property does.

What it reads, and what that does not cover

It reads modbusCore-rt.jar and modbusTcp-rt.jar with javap and nothing else. No station is contacted, no device is polled, and nothing is written anywhere — which is also the limit of what the output is worth. It is a reading of compiled code, so it tells you what the driver is built to do rather than what a device did yesterday.

The numbers above came from one installation, 4.15.5.22, and the output names it. The controller this work usually targets runs 4.14.0.162. A different Niagara version is a different answer, so run it against yours rather than trusting this page — which is what the three checks at the end of the output are for. They settle it on a real station in an afternoon.

The address format property, and the four numbers that mean the same register is the reasoning behind the output, and what a protocol document does not say is why the jar is the thing being read rather than the manual.

The repository

The same file, MIT licensed, at github.com/UsamaIqbal0304/modbus-address-scan. Its README carries the finding and a run of the output, so the repository is checkable without downloading anything. Issues and pull requests are read.

The file you are downloading

Published here so the download is checkable rather than trusted. Both figures are read off the file served at /tools/modbus-address-scan.py when this page is built, so they cannot disagree with it.

PropertyValue
Filemodbus-address-scan.py
Size15,514 bytes
SHA-256 cb7aa318ea8bda4af284f69502c527609e937f8d0a4213e811cf7823567e5cb8
Licence MIT — LICENSE.txt
Source github.com/UsamaIqbal0304/modbus-address-scan

To check it, on Linux sha256sum modbus-address-scan.py, on macOS shasum -a 256 modbus-address-scan.py, on Windows certutil -hashfile modbus-address-scan.py SHA256. A different digest means a different file — not necessarily a hostile one, but not this one.

The repository holds the same file, byte for byte, together with everything needed to re-run the checks this page's claims rest on — so they can be run rather than read about. Issues and pull requests there are read.

Also free

The others

Same idea, a different protocol or a different file. Every free tool.

Next step

Send the point that reads the wrong number.

The address, the format and the register type are three settings and the frame is one number. Which of the three is wrong is usually clear from the frame and the point together, and that answer costs nothing either way.