Skip to content

Getting Started with Modbus

Günter Obiltschnig edited this page Mar 19, 2019 · 8 revisions

Getting Started with Modbus

macchina.io comes with support for the Modbus protocol. The TCP and RTU (serial port/RS-485) variants of Modbus are supported. In a Modbus network, macchina.io is always the master. So, macchina.io provides the ModbusMaster service interface, which provides support for sending and receiving all defined Modbus messages.

Multiple master instances can be set up simultaneously. For example, there may be a ModbusMaster service that uses an RS-485 connection to talk to various industrial devices, and there may simultaneously be multiple other ModbusMaster services, each talking to a different Modbus slave device over a TCP connection.

Configuring a Modbus RTU Master

In order to configure a Modbus RTU master, an RS-485 interface must be available. The RS-485 interface is accessed using the standard Linux (or POSIX) serial port device interface. To set-up a ModbusMaster service instance for a specific serial port, add the following settings to the macchina.properties configuration file.

The following sample configuration is for a RS-485 to USB adapter (FTDI) connected to a Mac.

#
# Modbus RTU
#
modbus.rtu.ports.adam.device = /dev/tty.usbserial-FTYSQO7T
modbus.rtu.ports.adam.speed = 19200

Note that the adam part of the parameter name can later be used to find the corresponding ModbusMaster instance in the service registry. In this case we'll connect an Advantech ADAM 4068 Relay module, thus we chose the name adam. Any other name (alphanumeric, starts with letter, may include dashes -) is fine as well.

The main things to configure are the serial device (modbus.rtu.ports.<id>.device) and the port speed (modbus.rtu.ports.adam.speed). You can also specify communication parameters (modbus.rtu.ports.<id>.params, defaults to "8N1") and timeouts for commands (modbus.rtu.ports.<id>.timeout, given in milliseconds, defaults to 2000) and frames (modbus.rtu.ports.<id>.frameTimeout, given in microseconds, defaults to 10000). There are a few more options for special cases, like the RS-485 support on a BeagleBone, which requires the configuration of an additional GPIO pin. See the Modbus RTU BundleActivator for more information.

After updating the configuration file and restarting macchina.io, a new ModbusMaster service instance will be available in the service registry.

The name used in the modbus.rtu.ports configuration settings is reflected in the service name. For the above example (adam), the service name would be io.macchina.modbus.rtu#adam.

Using the ModbusMaster Service

Following is some JavaScript code that can be run in the Playground to test the Modbus interface. The script assumes that an ADAM 4068 Relay device is connected to the RS-485 bus. The Modbus slave address of the ADAM 4068 should be set to 1. The script will turn on and off the relays of the ADAM 4068 one after another, generating a "running light" effect on the LEDs showing the relay states. It uses the Modbus Write Single Coil function (Modbus function code 0x05) to set the relay states.

var modbus = null;
var modbusRef = serviceRegistry.findByName('io.macchina.modbus.rtu#adam');
if (modbusRef)
{
   modbus = modbusRef.instance();
   logger.notice("Modbus service found");
}

var currentCoil = 0;
var nOfCoils = 8;
var slaveId = 1;

setInterval(() => {

    var lastCoil = currentCoil - 1;
    if (lastCoil < 0) lastCoil = nOfCoils - 1;

    modbus.writeSingleCoil(slaveId, 16 + lastCoil, false);
    modbus.writeSingleCoil(slaveId, 16 + currentCoil, true);

    currentCoil++;
    if (currentCoil >= nOfCoils) currentCoil = 0;

    
}, 500);

Configuring a Modbus TCP Master

To create a ModbusMaster service for a Modbus TCP device, the following configuration properties can be used:

#
# Modbus TCP
#
modbus.tcp.ports.adam.hostAddress = 192.168.1.123
modbus.tcp.ports.adam.portNumber = 502

The corresponding service name would then consequently be io.macchina.modbus.tcp#adam. Specifying the port number is optional if the default port 502 is used. Additionally, the command timeout (modbus.tcp.ports.<id>.timeout, default 2000) can be specified in milliseconds, as with the Modbus RTU configuration.

To use the Modbus TCP master in the JavaScript sample shown above, change the second line to:

var modbusRef = serviceRegistry.findByName('io.macchina.modbus.tcp#adam');

Using the ModbusMaster Interface from C++

Please see the ModbusRunningCoils sample for an implementation of the "running coils" JavaScript sample in C++.

It's implemented as a bundle that first finds a IModbusMaster service instance, then starts a timer to control the coils.

The complete BundleActivator code, which also contains the timer task, is shown in the following.

#include "Poco/OSP/BundleActivator.h"
#include "Poco/OSP/BundleContext.h"
#include "Poco/OSP/ServiceRegistry.h"
#include "Poco/OSP/ServiceFinder.h"
#include "Poco/OSP/ServiceRef.h"
#include "Poco/OSP/PreferencesService.h"
#include "Poco/Util/Timer.h"
#include "Poco/Util/TimerTask.h"
#include "Poco/Format.h"
#include "Poco/ClassLibrary.h"
#include "IoT/Modbus/IModbusMaster.h"


namespace ModbusRunningCoils {


class RunningCoilsTask: public Poco::Util::TimerTask
{
public:
	RunningCoilsTask(IoT::Modbus::IModbusMaster::Ptr pModbusMaster, Poco::UInt8 slaveId, int nOfCoils, int baseAddress):
		_pModbusMaster(pModbusMaster),
		_slaveId(slaveId),
		_nOfCoils(nOfCoils),
		_baseAddress(baseAddress)
	{
	}

	void run()
	{
		int lastCoil = _currentCoil - 1;
		if (lastCoil < 0) lastCoil = _nOfCoils - 1;

		_pModbusMaster->writeSingleCoil(_slaveId, static_cast<Poco::UInt16>(_baseAddress + lastCoil), false);
		_pModbusMaster->writeSingleCoil(_slaveId, static_cast<Poco::UInt16>(_baseAddress + _currentCoil), true);

		_currentCoil++;
		if (_currentCoil >= _nOfCoils) _currentCoil = 0;
	}

private:
	IoT::Modbus::IModbusMaster::Ptr _pModbusMaster;
	Poco::UInt8 _slaveId;
	int _nOfCoils;
	int _currentCoil = 0;
	int _baseAddress;
};


class BundleActivator: public Poco::OSP::BundleActivator
{
public:
	void start(Poco::OSP::BundleContext::Ptr pContext)
	{
		_pContext = pContext;
		_pPrefs = Poco::OSP::ServiceFinder::find<Poco::OSP::PreferencesService>(pContext);

		std::string modbusMasterName = _pPrefs->configuration()->getString("modbusRunningCoils.modbusMaster", "io.macchina.modbus.rtu#adam");
		Poco::UInt8 slaveId = static_cast<Poco::UInt8>(_pPrefs->configuration()->getUInt("modbusRunningCoils.modbusSlave", 1));
		int nOfCoils = _pPrefs->configuration()->getInt("modbusRunningCoils.nOfCoils", 8);
		int baseAddress = _pPrefs->configuration()->getInt("modbusRunningCoils.baseAddress", 16);
		long interval = _pPrefs->configuration()->getInt("modbusRunningCoils.interval", 500);

		_pModbusMasterRef = pContext->registry().findByName(modbusMasterName);
		if (_pModbusMasterRef)
		{
			_pModbusMaster = _pModbusMasterRef->castedInstance<IoT::Modbus::IModbusMaster>();
			_timer.scheduleAtFixedRate(new RunningCoilsTask(_pModbusMaster, slaveId, nOfCoils, baseAddress), interval, interval);
		}
		else
		{
			_pContext->logger().warning("No ModbusMaster found.");
		}
	}

	void stop(Poco::OSP::BundleContext::Ptr pContext)
	{
		_timer.cancel(true);
		_pModbusMaster.reset();
		_pModbusMaster.reset();
		_pPrefs.reset();
		_pContext.reset();
	}

private:
	Poco::OSP::BundleContext::Ptr _pContext;
	Poco::OSP::PreferencesService::Ptr _pPrefs;
	Poco::OSP::ServiceRef::Ptr _pModbusMasterRef;
	IoT::Modbus::IModbusMaster::Ptr _pModbusMaster;
	Poco::Util::Timer _timer;
};


} // namespace ModbusRunningCoils


POCO_BEGIN_MANIFEST(Poco::OSP::BundleActivator)
	POCO_EXPORT_CLASS(ModbusRunningCoils::BundleActivator)
POCO_END_MANIFEST