Modbus Communication Issue Between Ignition and jlibmodbus Code

What I'm attempting to do

I am working on a custom Java application that implements Modbus using this jlibmodbus library

My Modbus Client Code

I was able to get a simple ModbusTCP client going with this example code that I slightly modified

package org.example;

import com.intelligt.modbus.jlibmodbus.Modbus;
import com.intelligt.modbus.jlibmodbus.data.DataHolder;
import com.intelligt.modbus.jlibmodbus.data.ModbusCoils;
import com.intelligt.modbus.jlibmodbus.data.ModbusHoldingRegisters;
import com.intelligt.modbus.jlibmodbus.exception.IllegalDataAddressException;
import com.intelligt.modbus.jlibmodbus.exception.IllegalDataValueException;
import com.intelligt.modbus.jlibmodbus.ModbusSlave;
import com.intelligt.modbus.jlibmodbus.ModbusSlaveFactory;
import com.intelligt.modbus.jlibmodbus.tcp.TcpParameters;

import java.net.InetAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Handler;
import java.util.logging.LogRecord;

/*
 * Copyright (c) 2017 Vladislav Kochedykov
 * All rights reserved
 *
 * This file is part of JLibModbus.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation and/or
 * other materials provided with the distribution.
 *
 * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse
 * or promote products derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
 * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * Authors: Vladislav Y. Kochedykov, software engineer.
 * email: vladislav.kochedykov@gmail.com
 */
public class SimpleSlaveTCP {

    static public void main(String[] argv) {

        Modbus.log().addHandler(new Handler() {
            @Override
            public void publish(LogRecord record) {
                System.out.println(record.getLevel().getName() + ": " + record.getMessage());
            }

            @Override
            public void flush() {
                //do nothing
            }

            @Override
            public void close() throws SecurityException {
                //do nothing
            }
        });
        Modbus.setLogLevel(Modbus.LogLevel.LEVEL_DEBUG);

        try {

            final ModbusSlave slave;

            TcpParameters tcpParameters = new TcpParameters();

            tcpParameters.setHost(InetAddress.getLocalHost());
            tcpParameters.setKeepAlive(true);
            tcpParameters.setPort(Modbus.TCP_PORT);

            slave = ModbusSlaveFactory.createModbusSlaveTCP(tcpParameters);
            slave.setReadTimeout(0); // if not set default timeout is 1000ms, I think this must be set to 0 (infinitive timeout)
            Modbus.setLogLevel(Modbus.LogLevel.LEVEL_DEBUG);

            MyOwnDataHolder dh = new MyOwnDataHolder();
            dh.addEventListener(new ModbusEventListener() {
                @Override
                public void onWriteToSingleCoil(int address, boolean value) {
                    System.out.print("onWriteToSingleCoil: address " + address + ", value " + value);
                }

                @Override
                public void onWriteToMultipleCoils(int address, int quantity, boolean[] values) {
                    System.out.print("onWriteToMultipleCoils: address " + address + ", quantity " + quantity);
                }

                @Override
                public void onWriteToSingleHoldingRegister(int address, int value) {
                    System.out.print("onWriteToSingleHoldingRegister: address " + address + ", value " + value);
                }

                @Override
                public void onWriteToMultipleHoldingRegisters(int address, int quantity, int[] values) {
                    System.out.print("onWriteToMultipleHoldingRegisters: address " + address + ", quantity " + quantity);
                }
            });

            slave.setDataHolder(dh);
            ModbusHoldingRegisters hr = new ModbusHoldingRegisters(30);
            hr.set(0, 12345);
            slave.getDataHolder().setHoldingRegisters(hr);
            slave.setServerAddress(1);
            /*
             * using master-branch it should be #slave.open();
             */
            slave.listen();

            /*
             * since 1.2.8
             */
            if (slave.isListening()) {
                Runtime.getRuntime().addShutdownHook(new Thread() {
                    @Override
                    public void run() {
                        synchronized (slave) {
                            slave.notifyAll();
                        }
                    }
                });

                synchronized (slave) {
                    slave.wait();
                }

                /*
                 * using master-branch it should be #slave.close();
                 */
                slave.shutdown();
            }
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public interface ModbusEventListener {
        void onWriteToSingleCoil(int address, boolean value);

        void onWriteToMultipleCoils(int address, int quantity, boolean[] values);

        void onWriteToSingleHoldingRegister(int address, int value);

        void onWriteToMultipleHoldingRegisters(int address, int quantity, int[] values);
    }

    public static class MyOwnDataHolder extends DataHolder {

        final List<ModbusEventListener> modbusEventListenerList = new ArrayList<ModbusEventListener>();

        public MyOwnDataHolder() {

            setHoldingRegisters(new ModbusHoldingRegisters(128));
            setCoils(new ModbusCoils(128));

        }

        public void addEventListener(ModbusEventListener listener) {
            modbusEventListenerList.add(listener);
        }

        public boolean removeEventListener(ModbusEventListener listener) {
            return modbusEventListenerList.remove(listener);
        }

        @Override
        public void writeHoldingRegister(int offset, int value) throws IllegalDataAddressException, IllegalDataValueException {
            for (ModbusEventListener l : modbusEventListenerList) {
                l.onWriteToSingleHoldingRegister(offset, value);
            }
            super.writeHoldingRegister(offset, value);
        }

        @Override
        public void writeHoldingRegisterRange(int offset, int[] range) throws IllegalDataAddressException, IllegalDataValueException {
            for (ModbusEventListener l : modbusEventListenerList) {
                l.onWriteToMultipleHoldingRegisters(offset, range.length, range);
            }
            super.writeHoldingRegisterRange(offset, range);
        }

        @Override
        public void writeCoil(int offset, boolean value) throws IllegalDataAddressException, IllegalDataValueException {
            for (ModbusEventListener l : modbusEventListenerList) {
                l.onWriteToSingleCoil(offset, value);
            }
            super.writeCoil(offset, value);
        }

        @Override
        public void writeCoilRange(int offset, boolean[] range) throws IllegalDataAddressException, IllegalDataValueException {
            for (ModbusEventListener l : modbusEventListenerList) {
                l.onWriteToMultipleCoils(offset, range.length, range);
            }
            super.writeCoilRange(offset, range);
        }
    }
}

Python Client Validation

I am able to read & write to this using this Python code so I have validated that this client works.

from pyModbusTCP.client import ModbusClient

c = ModbusClient(auto_open=True, port=502)

print("--------------------")
print("Attempting register writes")
w_m10_reg = c.write_multiple_registers(0, [10,20,30,40,50,60,70,80,90,100])

if w_m10_reg:
    print("Wrote to registers")
else:
    print("Unable to write to registers")
    print(c.last_error_as_txt)

print("--------------------")
r_m10_reg = c.read_holding_registers(0, 10)
if r_m10_reg:
    print("Read registers")
    print(r_m10_reg)
else:
    print("Unable to read registers")
    print(c.last_error_as_txt)
print("--------------------")

Ignition Issue when connecting

But when I try to connect to this device with Ignition I do get a Connected status
{5B956AB1-E8EF-4D5B-A7D0-B91CAF6BC5CB}

I then setup my addresses and created a new OPC tag in Ignition that binds to this device.

But I get Bad value message and when I attempt to modify this value it takes a bit then I get this error
Error writing to ModBusTest1.value: Bad("Bad_Timeout: The operation timed out.")

In my wrapper.log I see this error.

INFO   | jvm 1    | 2025/07/23 13:26:12 | W [d.M.ReadCoilsRequest          ] [13:26:12.148]: Request failed. FailureType==TIMEOUT device-name=Test Modbus Device
INFO   | jvm 1    | 2025/07/23 13:26:12 | java.lang.Exception: Request failed by TimeoutDaemon due to timeout: ScheduledRequest[com.inductiveautomation.xopc.drivers.modbus2.requests.ReadCoilsRequest@1605772b]
INFO   | jvm 1    | 2025/07/23 13:26:12 | 	at com.inductiveautomation.xopc.driver.api.BasicRequestCycle$TimeoutDaemon.failRequests(BasicRequestCycle.java:376)
INFO   | jvm 1    | 2025/07/23 13:26:12 | 	at com.inductiveautomation.xopc.driver.api.BasicRequestCycle$TimeoutDaemon.run(BasicRequestCycle.java:328)
INFO   | jvm 1    | 2025/07/23 13:26:12 | 	at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
INFO   | jvm 1    | 2025/07/23 13:26:12 | 	at java.base/java.util.concurrent.FutureTask.runAndReset(Unknown Source)
INFO   | jvm 1    | 2025/07/23 13:26:12 | 	at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(Unknown Source)
INFO   | jvm 1    | 2025/07/23 13:26:12 | 	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
INFO   | jvm 1    | 2025/07/23 13:26:12 | 	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
INFO   | jvm 1    | 2025/07/23 13:26:12 | 	at java.base/java.lang.Thread.run(Unknown Source)
INFO   | jvm 1    | 2025/07/23 13:26:14 | W [d.M.B.B.TimeoutDaemon         ] [13:26:14.151]: ScheduledRequest[com.inductiveautomation.xopc.drivers.modbus2.requests.ReadCoilsRequest@1605772b] request with key "60" failed due to timeout. device-name=Test Modbus Device
INFO   | jvm 1    | 2025/07/23 13:26:14 | W [d.M.ReadCoilsRequest          ] [13:26:14.151]: Request failed. FailureType==TIMEOUT device-name=Test Modbus Device
INFO   | jvm 1    | 2025/07/23 13:26:14 | java.lang.Exception: Request failed by TimeoutDaemon due to timeout: ScheduledRequest[com.inductiveautomation.xopc.drivers.modbus2.requests.ReadCoilsRequest@1605772b]
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at com.inductiveautomation.xopc.driver.api.BasicRequestCycle$TimeoutDaemon.failRequests(BasicRequestCycle.java:376)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at com.inductiveautomation.xopc.driver.api.BasicRequestCycle$TimeoutDaemon.run(BasicRequestCycle.java:328)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at java.base/java.util.concurrent.FutureTask.runAndReset(Unknown Source)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(Unknown Source)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at java.base/java.lang.Thread.run(Unknown Source)
INFO   | jvm 1    | 2025/07/23 13:26:14 | W [d.M.ReadCoilsRequest          ] [13:26:14.151]: Request failed. FailureType==DISCONNECTED device-name=Test Modbus Device
INFO   | jvm 1    | 2025/07/23 13:26:14 | java.lang.Exception: Driver is disconnected.
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at com.inductiveautomation.xopc.driver.api.AbstractDriver$RequestSchedule.cancelCurrentSchedule(AbstractDriver.java:1255)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at com.inductiveautomation.xopc.driver.api.AbstractDriver.notifyDisconnectDone(AbstractDriver.java:658)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at com.inductiveautomation.xopc.driver.api.AbstractIODelegatingDriver.notifyDisconnectDone(AbstractIODelegatingDriver.java:70)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at com.inductiveautomation.xopc.driver.api.AbstractIODelegatingDriver.disconnect(AbstractIODelegatingDriver.java:42)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at com.inductiveautomation.xopc.driver.api.AbstractIODelegatingDriver.reconnect(AbstractIODelegatingDriver.java:92)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at com.inductiveautomation.xopc.drivers.modbus2.AbstractModbusDriver.access$000(AbstractModbusDriver.java:102)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at com.inductiveautomation.xopc.drivers.modbus2.AbstractModbusDriver$2.notifyCommunicationTimeout(AbstractModbusDriver.java:149)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at com.inductiveautomation.xopc.drivers.modbus2.requests.AbstractModbusRequest.fail(AbstractModbusRequest.java:212)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at com.inductiveautomation.xopc.drivers.modbus2.requests.AbstractModbusReadRequest.fail(AbstractModbusReadRequest.java:121)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at com.inductiveautomation.xopc.driver.api.ScheduledRequest.fail(ScheduledRequest.java:126)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at com.inductiveautomation.xopc.driver.api.BasicRequestCycle$TimeoutDaemon.failRequests(BasicRequestCycle.java:373)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at com.inductiveautomation.xopc.driver.api.BasicRequestCycle$TimeoutDaemon.run(BasicRequestCycle.java:328)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at java.base/java.util.concurrent.FutureTask.runAndReset(Unknown Source)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(Unknown Source)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at java.base/java.lang.Thread.run(Unknown Source)
INFO   | jvm 1    | 2025/07/23 13:26:14 | W [d.M.ReadCoilsRequest          ] [13:26:14.152]: Request failed. FailureType==DISCONNECTED device-name=Test Modbus Device
INFO   | jvm 1    | 2025/07/23 13:26:14 | java.lang.Exception: RequestCycle stopped.
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at com.inductiveautomation.xopc.driver.api.BasicRequestCycle.shutdown(BasicRequestCycle.java:278)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at com.inductiveautomation.xopc.driver.api.AbstractDriver.createNewRequestCycle(AbstractDriver.java:698)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at com.inductiveautomation.xopc.driver.api.AbstractDriver.notifyDisconnectDone(AbstractDriver.java:659)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at com.inductiveautomation.xopc.driver.api.AbstractIODelegatingDriver.notifyDisconnectDone(AbstractIODelegatingDriver.java:70)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at com.inductiveautomation.xopc.driver.api.AbstractIODelegatingDriver.disconnect(AbstractIODelegatingDriver.java:42)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at com.inductiveautomation.xopc.driver.api.AbstractIODelegatingDriver.reconnect(AbstractIODelegatingDriver.java:92)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at com.inductiveautomation.xopc.drivers.modbus2.AbstractModbusDriver.access$000(AbstractModbusDriver.java:102)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at com.inductiveautomation.xopc.drivers.modbus2.AbstractModbusDriver$2.notifyCommunicationTimeout(AbstractModbusDriver.java:149)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at com.inductiveautomation.xopc.drivers.modbus2.requests.AbstractModbusRequest.fail(AbstractModbusRequest.java:212)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at com.inductiveautomation.xopc.drivers.modbus2.requests.AbstractModbusReadRequest.fail(AbstractModbusReadRequest.java:121)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at com.inductiveautomation.xopc.driver.api.ScheduledRequest.fail(ScheduledRequest.java:126)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at com.inductiveautomation.xopc.driver.api.BasicRequestCycle$TimeoutDaemon.failRequests(BasicRequestCycle.java:373)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at com.inductiveautomation.xopc.driver.api.BasicRequestCycle$TimeoutDaemon.run(BasicRequestCycle.java:328)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at java.base/java.util.concurrent.FutureTask.runAndReset(Unknown Source)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(Unknown Source)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
INFO   | jvm 1    | 2025/07/23 13:26:14 | 	at java.base/java.lang.Thread.run(Unknown Source)
INFO   | jvm 1    | 2025/07/23 13:26:19 | W [d.M.B.B.TimeoutDaemon         ] [13:26:19.155]: ScheduledRequest[com.inductiveautomation.xopc.drivers.modbus2.requests.ReadCoilsRequest@4515db4] request with key "61" failed due to timeout. device-name=Test Modbus Device

Thoughts

Because I am able to read/write with my Python example code I'm assuming there is some issue with how Ignition expects Modbus data to look and how I'm actually sending it back with my Java Client. I am going to attempt to make a simpler Java proof of concept that works with Ignition just to see if this is viable.

If anyone has any suggestions on what's going wrong here or what I should try let me know. Thanks!

In Ignition, don't set up a Modbus Address map. Just create OPC tags in the Designer that use the Modbus address syntax described in the user manual.

e.g. [DeviceName]HR1 or [DeviceName]1.HR1.

The most common problem with addressing is that Ignition will default to unit/slave id 0 and some devices/implementations will default to 1, and not respond to 0 at all.

Wow that was it! I thought I already tried that but thank you for the help =D