Modbus Logger Documentation


Introduction

This project addresses common challenges encountered in data logging using the Modbus communication protocol. Modbus was introduced in 1979 by Modicon (now Schneider Electric) to standardize communication between industrial electronic devices. It defines a universal protocol that allows different devices to exchange data reliably. Since 2004, the protocol has been maintained by the Modbus Organization, Inc.

One classification of Modbus communication is the Master–Slave model, also known as Modbus RTU (Remote Terminal Unit). In this model, the master device initiates commands to request data from one or more slave devices. Slaves only respond when queried by the master. Typically, Modbus RTU operates over RS-485 or RS-232 serial connections and supports one master with multiple slaves.

Another type is the Client–Server model, referred to as Modbus TCP. Here, communication occurs over Ethernet or a local network. Although conceptually similar to the master–slave architecture, Modbus TCP allows multiple clients and servers to communicate simultaneously, enhancing scalability and flexibility.

Modbus supports several function codes, each representing a different type of request:

Depending on the type of data required, the user selects the appropriate function code to retrieve information from a Modbus device.


Coding and File Structure

The main working directory of this Python project is Modbus_loggers. It contains three key subdirectories:

The core script, modbus_logger.py, is located in the Modbus_loggers directory. This script provides the foundational structure for the entire data logging system. It defines the main workflow and executes essential functions sequentially or in a continuous loop. Users are not advised to modify this file unless they fully understand its internal logic.


Configuration JSON File

The configs subdirectory contains JSON configuration files that define communication and logging parameters for each Modbus device.

Example: Modbus RTU Configuration

{
  "modbus": {
    "type": "serial",
    "timeout": 0.2,
    "port": "/dev/ttyS0",
    "baudrate": 19200,
    "stopbits": 1,
    "bytesize": 8,
    "parity": "N"
  },
  "device": {
    "name": "dcm_3366",
    "start_addr": 0,
    "reg_count": 40,
    "id_range": [1,2,3,4,5,6,7,8]
  },
  "logging": {
    "base_folder": "/mnt/data_storage/Modbus_loggers/data_storage/dcm_3366",
    "log_retention_days": 30,
    "file_suffix": "dcm_3366",
    "header": ["Datetime", "Device_ID",
    "Forward_energy_kWh", "Active_power_kW",
    "Current_A", "Voltage_V", "Error"],
    "time_step": 2
  }
}

Example: Modbus TCP Configuration

{
  "modbus": {
    "type": "tcp",
    "timeout": 0.2,
    "host": "192.168.1.10",
    "port": 502
  },
  "device": {
    "name": "list_regis",
    "start_addr": 4096,
    "reg_count": 125,
    "id_range": [1]
  },
  "logging": {
    "base_folder": "/mnt/data_storage/Modbus_loggers/data_storage/list_regis",
    "log_retention_days": 30,
    "file_suffix": "regis",
    "header": ["Datetime", "Device_ID"],
    "time_step": 2
  }
}

Parameter Explanation

Modbus Section:

Device Section:

Logging Section:


Utilities Folder

The utils directory includes:

common_utils.py provides helper functions used by the main script. validate_config.py verifies JSON files for formatting or logical errors. The device_specific_func.py file is where users can define custom functions for their Modbus devices.
This modular approach allows users to log data from different devices by simply writing their own device-specific function without modifying the main logging structure.

Example Device-Specific Function


import struct
import csv
import logging
from datetime import datetime
from pymodbus.client import ModbusSerialClient, ModbusTcpClient

logger = logging.getLogger(__name__)

def dcm_3366(client: ModbusSerialClient, start_addr: int, reg_count: int,
             csv_file: str, device_range: range) -> None:
    """Read DC meter (DCM3366) and save readings."""
    for device_id in device_range:
        logger.info(f"[dcm_3366] Reading device with Modbus ID = {device_id} ...")

        try:
            response = client.read_holding_registers(
                address=start_addr, count=reg_count, device_id=device_id
            )
        except Exception as e:
            logger.error(f"Error reading device {device_id}: {e}")
            now = datetime.now().isoformat()
            with open(csv_file, "a", newline="") as f:
                csv.writer(f).writerow([now, device_id, None, None, None, None, "Error"])
            continue

        regs = response.registers
        Forward_energy = (regs[0] << 16) + regs[1]
        Active_power = (regs[20] << 16) + regs[21]
        Current = (regs[22] << 16) + regs[23]
        Voltage = (regs[24] << 16) + regs[25]
        now = datetime.now().isoformat()

        with open(csv_file, "a", newline="") as f:
            csv.writer(f).writerow([
                now, device_id,
                round(Forward_energy / 100, 3),
                round(Active_power / 1000, 3),
                round(Current / 10000, 3),
                round(Voltage / 10000, 1),
                "No error"
            ])

Other GitHub Projects

Full project available on GitHub: https://github.com/hngjesse


Contact

📧 Email: hngjesse@gmail.com
🔗 GitHub: hngjesse