Introduction

This is a set of packages implementing Modbus Client sending requests to a remote device (i.e. Modbus Server).

The split of the packages is done to minimize dependencies on your project.

Notes for serial implementation

  • For Flutter projects: you only need to add flutter_libserialport package to your own project (i.e. run flutter pub add flutter_libserialport)
  • For Dart projects: you need to build the serial library libserialport build instructions. As a workaround you can create a dummy Flutter project, add the flutter_libserialport package, build it and copy the auto-generated serialport library from the build folder into your project's root folder or wherever your binary code will run.

Features

  • Auto connection mode: specify how the send command behaves by auto connecting and auto disconnecting from the client by setting the ModbusConnectionMode
  • Unit id: both the Modbus Client and the Request can specify the target unit id. This can be useful when using serial clients where more units/devices can be attached to one serial client.
  • Response timeout: A timeout waiting the response can be set in the Modbus Client instance or in the Request itself.
  • Connection timeout (TCP only): specify a connection timeout for the Modbus Client TCP.
  • Delay after connect (TCP only): you can apply an optional delay after server connection. In some cases (e.g. Huawei SUN2000 inverter) the server will not respond if requests are sent right after the connection.
  • Element types: this package offers a variety of element types: ModbusNumRegister (int16, uint16, int32, uint32, int64, uint64, float and double), ModbusBitElement, ModbusEnumRegister, ModbusStatusRegister, ModbusBitMaskRegister, ModbusEpochRegister, ModbusBytesRegister.
  • File records: support File Records function code 0x14 and 0x15 of different types of numeric records (int16, uint16, int32, uint32, float and double).
  • Endianness: Define how bytes are arranged in the modbus register.
    • ABCD(swapWord: false, swapByte: false)
    • CDAB(swapWord: true, swapByte: false)
    • BADC(swapWord: false, swapByte: true)
    • DCBA(swapWord: true, swapByte: true)
  • Group of elements: in order to optimize request you can create group of elements.
  • Custom requests implementation: you can easily implement custom request by overriding the Request class, assigning the request PDU (i.e. the protocolDataUnit) and override the internalSetFromPduResponse method or you override the Element Request class by overriding the internalSetElementData method.
  • Logging: Modbus Client libraries have logging enabled. You can activate logging with the root logger or by creating a ModbusAppLogger instance in your app.

Usage

Using modbus client is simple. You define your elements, create a read or write request out of them and use the client to send the request.

Read Request

import 'package:modbus_client/modbus_client.dart';
import 'package:modbus_client_serial/modbus_client_serial.dart';

void main() async {
  // Create a modbus int16 register element
  var batteryTemperature = ModbusInt16Register(
      name: "BatteryTemperature",
      type: ModbusElementType.inputRegister,
      address: 22,
      uom: "°C",
      multiplier: 0.1,
      onUpdate: (self) => print(self));

  // Create the modbus client.
  var modbusClient = ModbusClientSerialRtu(portName: "COM1", unitId: 1);

  // Send a read request from the element
  await modbusClient.send(batteryTemperature.getReadRequest());

  // Ending here
  modbusClient.disconnect();
}

Write Request

import 'package:modbus_client/modbus_client.dart';
import 'package:modbus_client_serial/modbus_client_serial.dart';

enum BatteryStatus implements ModbusIntEnum {
  offline(0),
  standby(1),
  running(2),
  fault(3),
  sleepMode(4);

  const BatteryStatus(this.intValue);

  @override
  final int intValue;

  @override
  String toString() {
    return name;
  }
}

void main() async {
  var batteryStatus = ModbusEnumRegister(
      name: "BatteryStatus",
      address: 11,
      type: ModbusElementType.holdingRegister,
      enumValues: BatteryStatus.values,
      onUpdate: (self) => print(self));

  var modbusClient = ModbusClientSerialRtu(portName: "COM1", unitId: 1);

  var req = batteryStatus.getWriteRequest(BatteryStatus.running);
  var res = await modbusClient.send(req);
  print(res.name);

  modbusClient.disconnect();
}

Modbus Elements

This library has a wide range of defined modbus elements having a name, a description, a modbus address and an update callback you can use in case the element value has been updated.

Bit and Numeric Elements

Typical elements are simple bit and numeric values:

  • ModbusDiscreteInput
  • ModbusCoil
  • ModbusInt16Register
  • ModbusUint16Register
  • ModbusInt32Register
  • ModbusUint32Register
  • ModbusInt64Register
  • ModbusUint64Register
  • ModbusFloatRegister
  • ModbusDoubleRegister

Numeric elements have an uom (i.e. unit of measure), a multiplier and offset to make conversion from raw to engineering values (i.e. value = read_value*multiplier + offset), and viewDecimalPlaces to print out only needed decimals.

Enum Element

To read and write an enum as an element you can use a ModbusEnumRegister

/// Implement [ModbusIntEnum] to use it with a [ModbusEnumRegister]
enum BatteryStatus implements ModbusIntEnum {
  offline(0),
  standby(1),
  running(2),
  fault(3),
  sleepMode(4);

  const BatteryStatus(this.intValue);

  @override
  final int intValue;

  @override
  String toString() {
    return name;
  }
}


void main() async {
  ModbusAppLogger(Level.FINEST);

  var batteryStatus = ModbusEnumRegister(
      name: "BatteryStatus",
      address: 11,
      type: ModbusElementType.holdingRegister,
      enumValues: BatteryStatus.values);

  var modbusClient = ModbusClientTcp("127.0.0.1", unitId: 1);
  await modbusClient.send(batteryStatus.getReadRequest());
  modbusClient.disconnect();
}

Status Element

Similar to enum is a status element. Within a ModbusStatusRegister you can define all the possible statues (i.e. numeric <-> string pairs) for the element.

ModbusStatusRegister(
    name: "Device status",
    type: ModbusElementType.holdingRegister,
    address: 32089,
    statusValues: [
      ModbusStatus(0x0000, "Standby: initializing"),
      ModbusStatus(0x0001, "Standby: detecting insulation resistance"),
      ModbusStatus(0x0002, "Standby: detecting irradiation"),
      ModbusStatus(0x0003, "Standby: drid detecting"),
      ModbusStatus(0x0100, "Starting"),
      ModbusStatus(0x0200, "On-grid (Off-grid mode: running)"),
      ModbusStatus(0x0201,
          "Grid connection: power limited (Off-grid mode: running: power limited)"),
      ModbusStatus(0x0202,
          "Grid connection: selfderating (Off-grid mode: running: selfderating)"),
      ModbusStatus(0x0203, "Off-grid Running"),
      ModbusStatus(0x0300, "Shutdown: fault"),
      ModbusStatus(0x0301, "Shutdown: command"),
      ModbusStatus(0x0302, "Shutdown: OVGR"),
      ModbusStatus(0x0303, "Shutdown: communication disconnected"),
      ModbusStatus(0x0304, "Shutdown: power limited"),
      ModbusStatus(0x0305, "Shutdown: manual startup required"),
      ModbusStatus(0x0306, "Shutdown: DC switches disconnected"),
      ModbusStatus(0x0307, "Shutdown: rapid cutoff"),
      ModbusStatus(0x0308, "Shutdown: input underpower"),
      ModbusStatus(0x0401, "Grid scheduling: cosφ-P curve"),
      ModbusStatus(0x0402, "Grid scheduling: Q-U curve"),
      ModbusStatus(0x0403, "Grid scheduling: PF-U curve"),
      ModbusStatus(0x0404, "Grid scheduling: dry contact"),
      ModbusStatus(0x0405, "Grid scheduling: Q-P curve"),
      ModbusStatus(0x0500, "Spotcheck ready"),
      ModbusStatus(0x0501, "Spotchecking"),
      ModbusStatus(0x0600, "Inspecting"),
      ModbusStatus(0X0700, "AFCI self check"),
      ModbusStatus(0X0800, "I-V scanning"),
      ModbusStatus(0X0900, "DC input detection"),
      ModbusStatus(0X0A00, "Running: off-grid charging"),
      ModbusStatus(0xA000, "Standby: no irradiation"),
    ]),

Bit Mask Element

Use ModbusBitMaskRegister if your device has registers where each bit value has a special meaning. You can define both an active and inactive value for each ModbusBitMask object.

ModbusBitMaskRegister(
    name: "Alarm 1",
    type: ModbusElementType.holdingRegister,
    address: 32008,
    bitMasks: [
      ModbusBitMask(0, "High String Input Voltage [2001 Major]"),
      ModbusBitMask(1, "DC Arc Fault [2002 Major]"),
      ModbusBitMask(2, "String Reverse Connection [2011 Major]"),
      ModbusBitMask(3, "String Current Backfeed [2012 Warning]"),
      ModbusBitMask(4, "Abnormal String Power [2013 Warning]"),
      ModbusBitMask(5, "AFCI Self-Check Fail. [2021 Major]"),
      ModbusBitMask(6, "Phase Wire Short-Circuited to PE [2031 Major]"),
      ModbusBitMask(7, "Grid Loss [2032 Major]"),
      ModbusBitMask(8, "Grid Undervoltage [2033 Major]"),
      ModbusBitMask(9, "Grid Overvoltage [2034 Major]"),
      ModbusBitMask(10, "Grid Volt. Imbalance [2035 Major]"),
      ModbusBitMask(11, "Grid Overfrequency [2036 Major]"),
      ModbusBitMask(12, "Grid Underfrequency [2037 Major]"),
      ModbusBitMask(13, "Unstable Grid Frequency [2038 Major]"),
      ModbusBitMask(14, "Output Overcurrent [2039 Major]"),
      ModbusBitMask(15, "Output DC Component Overhigh [2040 Major]"),
    ]),

Epoch/DateTime Element

Use ModbusEpochRegister if your device holds timestamp values as Epoch/Unix time in seconds.

ModbusEpochRegister(
    name: "Startup time",
    type: ModbusElementType.holdingRegister,
    address: 32091,
    isUtc: false);

Epoch/DateTime Element

Use ModbusEpochRegister if your device holds timestamp values as Epoch/Unix time in seconds.

ModbusEpochRegister(
    name: "Startup time",
    type: ModbusElementType.holdingRegister,
    address: 32091,
    isUtc: false);

Bytes Array Element

Use ModbusBytesRegister if you want to read and write multiple bytes/registers at a time. Note that byteCount cannot exceed 250 bytes which is the multiple read bytes limit for Modbus/RTU. Note that the protocol limit depends on multiple factors:

  • Read & Write have different limits
  • Modbus RTU and TCP have different limits
  • Device dependent limits To get the right limit please refer to Modbus specs and your device manual.
var bytesRegister = ModbusBytesRegister(
    name: "BytesArray",
    address: 4,
    byteCount: 10,
    onUpdate: (self) => print(self));

// Create the modbus client.
var modbusClient = ModbusClientSerialRtu(portName: "COM1", unitId: 1);

var req1 = bytesRegister.getWriteRequest(Uint8List.fromList(
    [0x01, 0x02, 0x03, 0x04, 0x05, 0x66, 0x07, 0x08, 0x09, 0x0A]));
var res = await modbusClient.send(req1);
print(res);

var req2 = bytesRegister.getReadRequest();
res = await modbusClient.send(req2);
print(bytesRegister.value);

modbusClient.disconnect();

Element Group

You can define a ModbusElementsGroup to optimize the elements reading. The most the element addresses are contiguous the most performant is the request. The address range limit for bits is 2000 and 125 for registers. You can use the ModbusElementsGroup object as a kind of list of element.

import 'package:modbus_client/modbus_client.dart';
import 'package:modbus_client_tcp/modbus_client_tcp.dart';

enum BatteryStatus implements ModbusIntEnum {
  offline(0),
  standby(1),
  running(2),
  fault(3),
  sleepMode(4);

  const BatteryStatus(this.intValue);

  @override
  final int intValue;
}

void main() async {
  // Create a modbus elements group
  var batteryRegs = ModbusElementsGroup([
    ModbusEnumRegister(
        name: "BatteryStatus",
        type: ModbusElementType.holdingRegister,
        address: 37000,
        enumValues: BatteryStatus.values),
    ModbusInt32Register(
        name: "BatteryChargingPower",
        type: ModbusElementType.holdingRegister,
        address: 37001,
        uom: "W",
        description: "> 0: charging - < 0: discharging"),
    ModbusUint16Register(
        name: "BatteryCharge",
        type: ModbusElementType.holdingRegister,
        address: 37004,
        uom: "%",
        multiplier: 0.1),
    ModbusUint16Register(
        name: "BatteryTemperature",
        type: ModbusElementType.holdingRegister,
        address: 37022,
        uom: "°C",
        multiplier: 0.1),
  ]);

  // Create the modbus client.
  var modbusClient = ModbusClientTcp("127.0.0.1", unitId: 1);

  // Send a read request from the group
  await modbusClient.send(batteryRegs.getReadRequest());
  print(batteryRegs[0]);
  print(batteryRegs[1]);
  print(batteryRegs[2]);
  print(batteryRegs[3]);

  // Ending here
  modbusClient.disconnect();
}

Modbus File Records

This library supports function codes 0x14 and 0x15 to read and write different types of numeric records.

File Record Types

  • ModbusFileInt16Record
  • ModbusFileUint16Record
  • ModbusFileInt32Record
  • ModbusFileUint32Record
  • ModbusFileFloatRecord
  • ModbusFileDoubleRecord
  • ModbusFileMultipleRecord
import 'dart:typed_data';

import 'package:logging/logging.dart';
import 'package:modbus_client/modbus_client.dart';
import 'package:modbus_client_serial/modbus_client_serial.dart';

void main() async {
  // Simple modbus logging
  ModbusAppLogger(Level.FINE);

  // Create the modbus client.
  var modbusClient = ModbusClientSerialRtu(portName: "COM1", unitId: 1);

  // Write two file records
  var r1 = ModbusFileUint16Record(
      fileNumber: 4,
      recordNumber: 1,
      recordData: Uint16List.fromList([12573, 56312]));
  var r2 = ModbusFileDoubleRecord(
      fileNumber: 3,
      recordNumber: 9,
      recordData: Float64List.fromList([123.5634, 125756782.8492]));
  await modbusClient.send(ModbusFileRecordsWriteRequest([r1, r2]));

  // Read two file records
  r1 = ModbusFileUint16Record.empty(
      fileNumber: 4, recordNumber: 1, recordDataCount: 2);
  r2 = ModbusFileDoubleRecord.empty(
      fileNumber: 3, recordNumber: 9, recordDataCount: 2);
  await modbusClient.send(ModbusFileRecordsReadRequest([r1, r2]));

  // Write multiple records
  var multipleRecords =
      ModbusFileMultipleRecord(fileNumber: 4, recordNumber: 1);
  multipleRecords.addNext(ModbusRecordType.int16, -123);
  multipleRecords.addNext(ModbusRecordType.uint16, 5000);
  multipleRecords.addNext(ModbusRecordType.int32, -1234567890);
  multipleRecords.addNext(ModbusRecordType.uint32, 1234567890);
  multipleRecords.addNext(ModbusRecordType.float, 123.45);
  multipleRecords.addNext(ModbusRecordType.double, 12345.6789);
  await modbusClient.send(multipleRecords.getWriteRequest());

  multipleRecords = ModbusFileMultipleRecord.empty(
      fileNumber: 4, recordNumber: 1, recordDataByteLength: 24);
  await modbusClient.send(multipleRecords.getReadRequest());
  multipleRecords.start();
  print(multipleRecords.getNext(ModbusRecordType.int16));
  print(multipleRecords.getNext(ModbusRecordType.uint16));
  print(multipleRecords.getNext(ModbusRecordType.int32));
  print(multipleRecords.getNext(ModbusRecordType.uint32));
  print(multipleRecords.getNext(ModbusRecordType.float));
  print(multipleRecords.getNext(ModbusRecordType.double));

  // Ending here
  modbusClient.disconnect();
}