This is an abstraction layer over the libmodbus
library. The functionality added allow creation
of mappings that create more meaningful data from modbus registers, such as 32 bit types created from
two registers, strings, and single bits from registers.
This library contains Mappings
, the base building block. Mapping
implies a single, logical bit of data.
They are of different types different from the native modbus uint16_t
, like:
- INT16, UINT16 (where UINT16 is native for registers)
- INT32, UINT32, FLOAT
- STRING
- BOOL (where BOOL is native for discrete types)
These are built using operations
TYPE | OPERATIONS |
---|---|
INT32, UINT32 | MERGE_BIG_ENDIAN, MERGE_LITTLE_ENDIAN |
FLOAT | MERGE_FLOAT_BIG_ENDIAN, MERGE_FLOAT_LITTLE_ENDIAN |
STRING | STRINGIFY_ASCII, STRINGIFY_UNICODE |
BOOL | TAKE_BIT |
- STRINGIFY_ASCII (0-127) and STRINGIFY_UNICODE (0-255) both store two characters as a
single
uint16_t
(understandable to modbus) - TAKE_BIT in combination with an index takes an exact bit (0-15) of a 16-bit register
You can create a Device
that groups Mappings
. A Group
contains Mappings
that can be read with a single
modbus message, and then the data is aggregated to each Mapping
, and parsed to requested type.
Also, there exists a Reader
that takes in all devices. This part is multi-threaded, all devices get their own thread
for reading the groups and parsing the data.
This library requires:
- GNU GCC and G++
- CMake
- Libmodbus (this is downloaded by CMake automatically)
- autoconf, automake, libtool (needed by Libmodbus)
- lcov (necessary for test coverage)
So run
sudo apt install autoconf automake libtool gcc g++ cmake lcov
And after that, run the configuration script
./configure.sh
The out directory should be generated, so move in there, and run build
cd out
make -j$(nproc)
After that, you can run the example
./MoreModbusExample
To run tests
make tests -j$(nproc)
./MoreModbusTests # if you want to run them again, make will run them once for you.
Also, as bonus, after you ran the tests, you can check their coverage by back to the tools
directory
./coverage.sh
Create some mappings, you can create some normal mappings
All the HOLDING_REGISTER/INPUT_REGISTER mappings by default are UInt16Mappings, and INPUT_CONTACT/COIL are BoolMapping.
const auto& normalRegisterMapping =
std::make_shared<wolkabout::UInt16Mapping>("U16M", wolkabout::RegisterType::HOLDING_REGISTER, 0);
const auto& normalContactMapping =
std::make_shared<wolkabout::BoolMapping>("BM", wolkabout::RegisterType::INPUT_CONTACT, 0);
You can create a multi-register mapping, by listing the addresses in a vector. You of course, need to list the operation for the mapping, and make it as a Int32Mapping, UInt32Mapping or StringMapping based on the return value you want.
const auto& stringMapping = std::make_shared<wolkabout::StringMapping>(
"STR1", wolkabout::RegisterType::HOLDING_REGISTER, std::vector<int16_t>{0, 1, 2},
wolkabout::OperationType::STRINGIFY_ASCII);
You can create a bit mapping, one that will take a single bit from a register.You need to enlist as type HOLDING_REGISTER / INPUT_REGISTER, the TAKE_BIT operation and the bit index.
const auto& getFirstBitMapping =
std::make_shared<wolkabout::BoolMapping>("B4-1", wolkabout::RegisterType::HOLDING_REGISTER, 4,
wolkabout::OperationType::TAKE_BIT, 0);
Next you need to merge all the mappings into a device. While creating the device, you also hand it a slaveAddress, which will be used to access it over SERIAL/RTU if you use RS485 with multiple devices.
const auto& mappings = std::vector<std::shared_ptr<wolkabout::RegisterMapping>>{normalRegisterMapping,
normalContactMapping, stringMapping, getFirstBitMapping, getSecondBitMapping};
const auto& device = std::make_shared<wolkabout::ModbusDevice>(
"Test Device 1", 1, mappings);
The device is also automatically creating Groups
, so that part is completely out of the users hands.
If you want to be able to see mappings change in value, you have to set up the callback method.
Of course, since you're getting the std::shared_ptr<RegisterMapping>
you need to cast it by the output type,
if you want access to the parsed value.
device->setOnMappingValueChange([](const std::shared_ptr<wolkabout::RegisterMapping>& mapping) {
// You can do this for all output types.
if (mapping->getOutputType() == wolkabout::OutputType::BOOL)
{
const auto& boolMapping = std::dynamic_pointer_cast<wolkabout::BoolMapping>(mapping);
LOG(DEBUG) << "Application: Mapping is bool, value : " << boolMapping->getBoolValue();
if (!boolMapping->getBoolValue())
boolMapping->writeValue(true);
}
else if (mapping->getOutputType() == wolkabout::OutputType::STRING)
{
const auto& stringMapping = std::dynamic_pointer_cast<wolkabout::StringMapping>(mapping);
LOG(DEBUG) << "Application: Mapping is string, value : " << stringMapping->getStringValue();
if (stringMapping->getStringValue().empty())
stringMapping->writeValue("Test");
}
else
{
LOG(DEBUG) << "Application: Mapping " << mapping->getReference() << " value changed.";
}
});
You can create a client, TCP/IP or SERIAL/RTU depending on your needs. This will be necessary for the reader.
const auto& modbusClient = std::make_shared<wolkabout::LibModbusSerialRtuClient>(
"/dev/tty0", 115200, 8, 1, wolkabout::LibModbusSerialRtuClient::BitParity::NONE, std::chrono::milliseconds(500));
const auto& modbusClient =
std::make_shared<wolkabout::LibModbusTcpIpClient>("<IP ADDRESS>", 502, std::chrono::milliseconds(500));
And the final part, is the reader logic, which needs the client, and list of all devices that the reader should read.
const auto& reader = std::make_shared<wolkabout::ModbusReader>(
*modbusClient, std::vector<std::shared_ptr<wolkabout::ModbusDevice>>{device}, std::chrono::milliseconds(1000));
And the control to start/stop, you can invoke methods. While the reader is running, make sure your main thread doesn't stop running, because the whole program will stop, and the reader, with the program, will crash. You can do that with a while loop with a sleep method inside.
reader->start();
while (reader->isRunning())
{
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
}
reader->stop();