Salta el contingut

Modbus TCP - Llibreries i exemples

Introducció

Modbus TCP és l'adaptació del protocol Modbus sobre TCP/IP. És un dels protocols industrials més utilitzats per la seva simplicitat i àmplia compatibilitat.

Llibreries disponibles

Llibreria Llenguatge Rol Llicència
pymodbus Python Client i Server BSD
modbus-serial Node.js Client ISC
jsmodbus Node.js Client i Server MIT
node-red-contrib-modbus Node-RED Client i Server BSD

Python: pymodbus

La llibreria més completa per Modbus en Python. Suporta Modbus TCP, RTU i ASCII.

Instal·lació

pip install pymodbus

Client bàsic

from pymodbus.client import ModbusTcpClient

# Connexió
client = ModbusTcpClient('192.168.1.100', port=502)
client.connect()

# Llegir 10 holding registers a partir de l'adreça 0
result = client.read_holding_registers(address=0, count=10, slave=1)
if not result.isError():
    print(f"Registres: {result.registers}")

# Tancar connexió
client.close()

Funcions de lectura

from pymodbus.client import ModbusTcpClient

client = ModbusTcpClient('192.168.1.100', port=502)
client.connect()

# Funció 01: Read Coils (sortides digitals)
coils = client.read_coils(address=0, count=16, slave=1)
print(f"Coils: {coils.bits[:16]}")

# Funció 02: Read Discrete Inputs (entrades digitals)
inputs = client.read_discrete_inputs(address=0, count=16, slave=1)
print(f"Inputs: {inputs.bits[:16]}")

# Funció 03: Read Holding Registers (registres R/W)
holding = client.read_holding_registers(address=0, count=10, slave=1)
print(f"Holding: {holding.registers}")

# Funció 04: Read Input Registers (registres només lectura)
input_regs = client.read_input_registers(address=0, count=10, slave=1)
print(f"Input Registers: {input_regs.registers}")

client.close()

Funcions d'escriptura

from pymodbus.client import ModbusTcpClient

client = ModbusTcpClient('192.168.1.100', port=502)
client.connect()

# Funció 05: Write Single Coil
client.write_coil(address=0, value=True, slave=1)

# Funció 06: Write Single Register
client.write_register(address=0, value=1234, slave=1)

# Funció 15: Write Multiple Coils
client.write_coils(address=0, values=[True, False, True, True], slave=1)

# Funció 16: Write Multiple Registers
client.write_registers(address=0, values=[100, 200, 300, 400], slave=1)

client.close()

Treballar amb tipus de dades

Modbus només treballa amb registres de 16 bits. Per a altres tipus cal combinar registres:

from pymodbus.client import ModbusTcpClient
from pymodbus.payload import BinaryPayloadDecoder, BinaryPayloadBuilder
from pymodbus.constants import Endian

client = ModbusTcpClient('192.168.1.100', port=502)
client.connect()

# Llegir un valor float (32 bits = 2 registres)
result = client.read_holding_registers(address=0, count=2, slave=1)
decoder = BinaryPayloadDecoder.fromRegisters(
    result.registers,
    byteorder=Endian.BIG,
    wordorder=Endian.BIG
)
float_value = decoder.decode_32bit_float()
print(f"Float: {float_value}")

# Llegir un valor int32 (32 bits = 2 registres)
result = client.read_holding_registers(address=2, count=2, slave=1)
decoder = BinaryPayloadDecoder.fromRegisters(
    result.registers,
    byteorder=Endian.BIG,
    wordorder=Endian.BIG
)
int32_value = decoder.decode_32bit_int()
print(f"Int32: {int32_value}")

# Escriure un valor float
builder = BinaryPayloadBuilder(byteorder=Endian.BIG, wordorder=Endian.BIG)
builder.add_32bit_float(3.14159)
registers = builder.to_registers()
client.write_registers(address=0, values=registers, slave=1)

# Escriure un valor int32
builder = BinaryPayloadBuilder(byteorder=Endian.BIG, wordorder=Endian.BIG)
builder.add_32bit_int(123456)
registers = builder.to_registers()
client.write_registers(address=2, values=registers, slave=1)

client.close()

Llegir strings

from pymodbus.client import ModbusTcpClient
from pymodbus.payload import BinaryPayloadDecoder
from pymodbus.constants import Endian

client = ModbusTcpClient('192.168.1.100', port=502)
client.connect()

# Llegir string (10 registres = 20 caràcters màxim)
result = client.read_holding_registers(address=100, count=10, slave=1)
decoder = BinaryPayloadDecoder.fromRegisters(
    result.registers,
    byteorder=Endian.BIG,
    wordorder=Endian.BIG
)
string_value = decoder.decode_string(20).decode('utf-8').strip('\x00')
print(f"String: {string_value}")

client.close()

Client asíncron

import asyncio
from pymodbus.client import AsyncModbusTcpClient

async def main():
    client = AsyncModbusTcpClient('192.168.1.100', port=502)
    await client.connect()

    # Llegir registres
    result = await client.read_holding_registers(address=0, count=10, slave=1)
    print(f"Registres: {result.registers}")

    # Escriure
    await client.write_register(address=0, value=999, slave=1)

    client.close()

asyncio.run(main())

Servidor Modbus (simulador)

from pymodbus.server import StartTcpServer
from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext
from pymodbus.datastore import ModbusSequentialDataBlock

# Crear blocs de dades
# Paràmetres: adreça inicial, valors inicials
store = ModbusSlaveContext(
    di=ModbusSequentialDataBlock(0, [False] * 100),  # Discrete Inputs
    co=ModbusSequentialDataBlock(0, [False] * 100),  # Coils
    hr=ModbusSequentialDataBlock(0, [0] * 100),      # Holding Registers
    ir=ModbusSequentialDataBlock(0, [0] * 100)       # Input Registers
)

context = ModbusServerContext(slaves=store, single=True)

# Iniciar servidor
print("Servidor Modbus TCP iniciat a port 502...")
StartTcpServer(context=context, address=("0.0.0.0", 502))

Gestió d'errors

from pymodbus.client import ModbusTcpClient
from pymodbus.exceptions import ModbusException, ConnectionException

client = ModbusTcpClient('192.168.1.100', port=502, timeout=3)

try:
    if not client.connect():
        raise ConnectionException("No s'ha pogut connectar")

    result = client.read_holding_registers(address=0, count=10, slave=1)

    if result.isError():
        print(f"Error Modbus: {result}")
    else:
        print(f"Registres: {result.registers}")

except ConnectionException as e:
    print(f"Error de connexió: {e}")
except ModbusException as e:
    print(f"Error Modbus: {e}")
finally:
    client.close()

Node.js: modbus-serial

Llibreria popular per a Node.js que suporta Modbus TCP i RTU.

Instal·lació

npm install modbus-serial

Client bàsic

const ModbusRTU = require('modbus-serial');

const client = new ModbusRTU();

async function main() {
    // Connexió TCP
    await client.connectTCP('192.168.1.100', { port: 502 });
    client.setID(1);  // Slave ID

    // Llegir 10 holding registers
    const data = await client.readHoldingRegisters(0, 10);
    console.log('Registres:', data.data);

    client.close();
}

main().catch(console.error);

Funcions de lectura

const ModbusRTU = require('modbus-serial');

const client = new ModbusRTU();

async function readAll() {
    await client.connectTCP('192.168.1.100', { port: 502 });
    client.setID(1);

    // Funció 01: Read Coils
    const coils = await client.readCoils(0, 16);
    console.log('Coils:', coils.data);

    // Funció 02: Read Discrete Inputs
    const inputs = await client.readDiscreteInputs(0, 16);
    console.log('Inputs:', inputs.data);

    // Funció 03: Read Holding Registers
    const holding = await client.readHoldingRegisters(0, 10);
    console.log('Holding:', holding.data);

    // Funció 04: Read Input Registers
    const inputRegs = await client.readInputRegisters(0, 10);
    console.log('Input Registers:', inputRegs.data);

    client.close();
}

readAll().catch(console.error);

Funcions d'escriptura

const ModbusRTU = require('modbus-serial');

const client = new ModbusRTU();

async function writeData() {
    await client.connectTCP('192.168.1.100', { port: 502 });
    client.setID(1);

    // Funció 05: Write Single Coil
    await client.writeCoil(0, true);

    // Funció 06: Write Single Register
    await client.writeRegister(0, 1234);

    // Funció 15: Write Multiple Coils
    await client.writeCoils(0, [true, false, true, true]);

    // Funció 16: Write Multiple Registers
    await client.writeRegisters(0, [100, 200, 300, 400]);

    console.log('Escriptura completada');
    client.close();
}

writeData().catch(console.error);

Treballar amb floats

const ModbusRTU = require('modbus-serial');

const client = new ModbusRTU();

// Funció per convertir 2 registres a float32
function registersToFloat(reg1, reg2) {
    const buffer = Buffer.alloc(4);
    buffer.writeUInt16BE(reg1, 0);
    buffer.writeUInt16BE(reg2, 2);
    return buffer.readFloatBE(0);
}

// Funció per convertir float32 a 2 registres
function floatToRegisters(value) {
    const buffer = Buffer.alloc(4);
    buffer.writeFloatBE(value, 0);
    return [buffer.readUInt16BE(0), buffer.readUInt16BE(2)];
}

async function main() {
    await client.connectTCP('192.168.1.100', { port: 502 });
    client.setID(1);

    // Llegir float (2 registres)
    const data = await client.readHoldingRegisters(0, 2);
    const floatValue = registersToFloat(data.data[0], data.data[1]);
    console.log('Float:', floatValue);

    // Escriure float
    const registers = floatToRegisters(3.14159);
    await client.writeRegisters(0, registers);

    client.close();
}

main().catch(console.error);

Polling continu

const ModbusRTU = require('modbus-serial');

const client = new ModbusRTU();
let isConnected = false;

async function connect() {
    try {
        await client.connectTCP('192.168.1.100', { port: 502 });
        client.setID(1);
        client.setTimeout(2000);
        isConnected = true;
        console.log('Connectat');
    } catch (err) {
        console.error('Error de connexió:', err.message);
        isConnected = false;
    }
}

async function poll() {
    if (!isConnected) {
        await connect();
        return;
    }

    try {
        const data = await client.readHoldingRegisters(0, 10);
        console.log('Dades:', data.data);
    } catch (err) {
        console.error('Error de lectura:', err.message);
        isConnected = false;
    }
}

// Polling cada 500ms
connect().then(() => {
    setInterval(poll, 500);
});

Gestió d'errors amb reconnexió

const ModbusRTU = require('modbus-serial');

class ModbusClient {
    constructor(ip, port = 502, slaveId = 1) {
        this.ip = ip;
        this.port = port;
        this.slaveId = slaveId;
        this.client = new ModbusRTU();
        this.connected = false;
    }

    async connect() {
        try {
            await this.client.connectTCP(this.ip, { port: this.port });
            this.client.setID(this.slaveId);
            this.client.setTimeout(3000);
            this.connected = true;
            console.log(`Connectat a ${this.ip}:${this.port}`);
        } catch (err) {
            this.connected = false;
            throw err;
        }
    }

    async ensureConnected() {
        if (!this.connected) {
            await this.connect();
        }
    }

    async readRegisters(address, count) {
        await this.ensureConnected();
        try {
            return await this.client.readHoldingRegisters(address, count);
        } catch (err) {
            this.connected = false;
            throw err;
        }
    }

    async writeRegister(address, value) {
        await this.ensureConnected();
        try {
            return await this.client.writeRegister(address, value);
        } catch (err) {
            this.connected = false;
            throw err;
        }
    }

    close() {
        this.client.close();
        this.connected = false;
    }
}

// Ús
const plc = new ModbusClient('192.168.1.100');

async function main() {
    const data = await plc.readRegisters(0, 10);
    console.log(data.data);
}

main().catch(console.error);

Node.js: jsmodbus

Alternativa amb suport per servidor.

Instal·lació

npm install jsmodbus

Client

const Modbus = require('jsmodbus');
const net = require('net');

const socket = new net.Socket();
const client = new Modbus.client.TCP(socket, 1);  // unit ID = 1

socket.on('connect', async () => {
    try {
        // Llegir holding registers
        const resp = await client.readHoldingRegisters(0, 10);
        console.log('Registres:', resp.response.body.values);

        // Escriure un registre
        await client.writeSingleRegister(0, 1234);

        socket.end();
    } catch (err) {
        console.error(err);
    }
});

socket.on('error', console.error);
socket.connect({ host: '192.168.1.100', port: 502 });

Servidor

const Modbus = require('jsmodbus');
const net = require('net');

const server = new net.Server();
const modbusServer = new Modbus.server.TCP(server);

// Configurar dades inicials
modbusServer.holding.writeUInt16BE(100, 0);  // Registre 0 = 100
modbusServer.holding.writeUInt16BE(200, 2);  // Registre 1 = 200

server.listen(502, '0.0.0.0', () => {
    console.log('Servidor Modbus TCP escoltant a port 502');
});

Node-RED

Instal·lació

cd ~/.node-red
npm install node-red-contrib-modbus

Nodes disponibles

  • modbus-client: Configuració de connexió
  • modbus-read: Llegir registres
  • modbus-write: Escriure registres
  • modbus-getter: Llegir sota demanda
  • modbus-flex-getter: Lectura flexible
  • modbus-server: Servidor Modbus

Eines de diagnòstic

Modbus Poll (Windows)

Client Modbus amb interfície gràfica per testejar dispositius.

mbpoll (Linux)

# Instal·lació
sudo apt install mbpoll

# Llegir 10 holding registers
mbpoll -a 1 -r 0 -c 10 192.168.1.100

# Escriure un valor
mbpoll -a 1 -r 0 192.168.1.100 1234

pymodbus.console

# Instal·lar
pip install pymodbus[repl]

# Iniciar consola
pymodbus.console tcp --host 192.168.1.100 --port 502

# Dins la consola:
> client.read_holding_registers 0 10 1

Mapa de registres típic

Exemple d'organització de registres en un dispositiu:

Adreça Tipus Descripció
0-9 Holding Setpoints
10-19 Holding Paràmetres configuració
100-109 Input Valors de procés
110-119 Input Estat dels sensors
0-15 Coil Sortides digitals
0-15 Discrete Entrades digitals

Recursos