Skip to main content
Blog
Blog · LRI AEM-60DC8

Simulador Modbus RTU em Python: tutorial passo a passo para integradores SCADA

Tutorial prático para construir um simulador Modbus RTU em Python e validar a integração SCADA com o AEM-60DC8 antes de receber o hardware físico.

LRI EngenhariaMon May 25 2026 21:00:00 GMT-0300 (Brasilia Standard Time)12 min

Quando um cliente fecha o pedido do AEM-60DC8, o relógio do projeto começa a correr. A engenharia de SCADA precisa adiantar a parametrização, mapear tags, desenhar telas e rodar testes de borda — só que o equipamento físico ainda está em produção. A saída prática é simular o dispositivo Modbus RTU em Python e expor um servidor que responde como se fosse o multimedidor real. Este tutorial mostra como subir esse simulador do zero, organizar um mapa de 147 holding registers compatível com a v1.03 do firmware AEM-60DC8, validar com clientes de bancada e plugar no seu SCADA via porta COM virtual. Você vai sair daqui com código rodando, não com slides.

Por que simular antes de comprar

Adiar a integração até o hardware chegar é uma decisão cara. Cada dia parado na obra é diária de comissionador, hora de engenheiro de campo e janela de paralisação que não volta. Antecipar com um simulador resolve três problemas concretos:

  • Economia de tempo. A parametrização de driver, criação de tags, alarmes e historiadores acontece em paralelo à fabricação. Quando o AEM-60DC8 chega, a engenharia já validou 90% do projeto SCADA.
  • Deploy mais limpo. Erros de endereçamento, escala e byte order são encontrados em laboratório, não no painel energizado às 3h da manhã.
  • Testes de borda. Simular um alarme de sobretensão, uma falha de comunicação ou um valor fora de faixa é trivial em software. No hardware, exige bancada, fonte programável e tempo.

Há ainda um ganho menos óbvio: documentação. O simulador vira referência viva do mapa de registradores. Quem entra no time depois consulta o código, não um PDF desatualizado.

Bibliotecas Python: comparativo rápido

Três bibliotecas dominam o ecossistema Python para Modbus. O comparativo abaixo é direto:

Biblioteca Pontos fortes Limitações Quando usar
pymodbus Ativa, async-friendly, suporta RTU/TCP/ASCII, datastore plugável API muda entre versões majors Padrão para qualquer projeto novo
modbus-tk Estável, simples, leve Manutenção esporádica, sem async Scripts curtos legados
libmodbus (binding) C nativo, performance Setup chato no Windows, menos pythônico Integração com C/C++ existente

Para este tutorial, pymodbus 3.6.x. Ela cobre RTU, TCP e ASCII, tem datastore customizável (importante para registradores dinâmicos) e roda em Windows, Linux e macOS. Fixe a versão menor — a API mudou entre 2.x e 3.x e mudará de novo em 4.x.

Instalação e ambiente

Crie um virtualenv. Misturar dependências de Modbus com o Python global do sistema é receita para conflito.

python -m venv .venv
# Windows
.venv\Scripts\activate
# Linux/macOS
source .venv/bin/activate

pip install --upgrade pip
pip install "pymodbus[serial]==3.6.9"

O extra [serial] arrasta o pyserial, necessário para RTU. Confirme com python -c "import pymodbus, serial; print(pymodbus.__version__, serial.__version__)" — deve sair 3.6.9 e algo como 3.5. Se quebrar aqui, é Python errado no PATH; resolva antes de continuar.

No Windows, instale o com0com para criar pares de portas COM virtuais (COM10 <-> COM11). No Linux, use socat para criar dois pty conectados:

sudo apt install socat
socat -d -d pty,raw,echo=0,link=/tmp/ttyV0 pty,raw,echo=0,link=/tmp/ttyV1

Mantenha esse terminal aberto. ttyV0 será do simulador; ttyV1, do cliente ou do SCADA.

Hello world: servidor Modbus RTU mínimo

Salve como server_min.py. O servidor escuta em /tmp/ttyV0 (Linux) ou COM10 (Windows), responde como escravo 1, e mantém 10 holding registers com valores fixos.

# server_min.py
import logging
from pymodbus.datastore import ModbusSequentialDataBlock, ModbusSlaveContext, ModbusServerContext
from pymodbus.server import StartSerialServer
from pymodbus.framer import ModbusRtuFramer

logging.basicConfig(level=logging.INFO)

PORT = "/tmp/ttyV0"  # Windows: "COM10"

# 10 holding registers, todos iniciando em 0
hr_block = ModbusSequentialDataBlock(0, [0] * 10)
slave_ctx = ModbusSlaveContext(hr=hr_block, zero_mode=True)
server_ctx = ModbusServerContext(slaves={1: slave_ctx}, single=False)

if __name__ == "__main__":
    # Pré-carrega alguns valores
    hr_block.setValues(0, [2200, 2201, 2199, 2202, 2198, 2200, 2203, 2197, 0, 0])
    StartSerialServer(
        context=server_ctx,
        framer=ModbusRtuFramer,
        port=PORT,
        baudrate=19200,
        bytesize=8,
        parity="E",
        stopbits=1,
        timeout=1,
    )

Rode em um terminal com python server_min.py. Você verá log do pymodbus indicando que o servidor abriu a porta. Se aparecer could not open port, é a porta serial errada ou o socat parou.

Mapa do AEM-60DC8: 147 holding registers

Exemplo simplificado. O mapa abaixo é didático e serve para validar fluxo de integração. O mapa oficial completo da v1.03 está no manual do AEM-60DC8 (LRI). Use este apenas como esqueleto.

Organize 147 holding registers em blocos lógicos. Endereços decimais, zero_mode=True:

Faixa Conteúdo Tipo
0–7 Tensão CH1–CH8 (V × 100) uint16
8–15 Corrente CH1–CH8 (mA) uint16
16–23 Potência CH1–CH8 (W) int16
24–39 Energia acumulada CH1–CH8 uint32 (pares)
40–47 Status digital CH1–CH8 uint16 (bitmap)
48–63 Alarmes (sobretensão, subtensão etc.) uint16
64–95 Configuração (baudrate, slave ID, escalas) uint16
96–146 Reservados / diagnóstico uint16

Salve como aem_map.py:

# aem_map.py
from dataclasses import dataclass

@dataclass(frozen=True)
class Block:
    start: int
    length: int
    label: str

BLOCKS = (
    Block(0,  8,  "tensao_ch"),
    Block(8,  8,  "corrente_ch"),
    Block(16, 8,  "potencia_ch"),
    Block(24, 16, "energia_ch"),
    Block(40, 8,  "status_ch"),
    Block(48, 16, "alarmes"),
    Block(64, 32, "config"),
    Block(96, 51, "diag"),
)

TOTAL_REGISTERS = sum(b.length for b in BLOCKS)  # = 147
assert TOTAL_REGISTERS == 147, f"esperado 147, obteve {TOTAL_REGISTERS}"

def initial_values() -> list[int]:
    values = [0] * TOTAL_REGISTERS
    # tensões nominais (220.0 V) -> 22000 com escala x100
    for ch in range(8):
        values[ch] = 22000
    # baudrate default 19200 codificado como 3
    values[64] = 3
    # slave id
    values[65] = 1
    return values

Agora um servidor que carrega esse mapa. Salve como server_aem.py:

# server_aem.py
import logging
from pymodbus.datastore import ModbusSequentialDataBlock, ModbusSlaveContext, ModbusServerContext
from pymodbus.server import StartSerialServer
from pymodbus.framer import ModbusRtuFramer
from aem_map import initial_values, TOTAL_REGISTERS

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")

PORT = "/tmp/ttyV0"  # Windows: "COM10"
SLAVE_ID = 1

hr_block = ModbusSequentialDataBlock(0, initial_values())
slave_ctx = ModbusSlaveContext(hr=hr_block, zero_mode=True)
server_ctx = ModbusServerContext(slaves={SLAVE_ID: slave_ctx}, single=False)

if __name__ == "__main__":
    logging.info("Simulador AEM-60DC8 v1.03 | %d registros | %s", TOTAL_REGISTERS, PORT)
    StartSerialServer(
        context=server_ctx,
        framer=ModbusRtuFramer,
        port=PORT,
        baudrate=19200,
        bytesize=8,
        parity="E",
        stopbits=1,
        timeout=1,
    )

Validando com um cliente Modbus

Duas abordagens. A primeira, via linha de comando com mbpoll (cross-platform, instalável via Chocolatey no Windows ou apt no Linux):

# Lê 8 holding registers a partir do endereço 0, slave 1, 19200 8E1
mbpoll -m rtu -a 1 -b 19200 -P even -t 4 -r 1 -c 8 /tmp/ttyV1

Observação: mbpoll usa endereçamento começando em 1 por padrão, daí -r 1 para ler o registro de endereço 0. Você deve ver oito valores 22000.

A segunda, cliente Python — útil para automatizar testes de regressão:

# client_check.py
from pymodbus.client import ModbusSerialClient
from pymodbus.framer import ModbusRtuFramer

client = ModbusSerialClient(
    port="/tmp/ttyV1",  # Windows: "COM11"
    framer=ModbusRtuFramer,
    baudrate=19200,
    bytesize=8,
    parity="E",
    stopbits=1,
    timeout=1,
)

if not client.connect():
    raise SystemExit("falha ao abrir a porta cliente")

rr = client.read_holding_registers(address=0, count=8, slave=1)
if rr.isError():
    print("erro:", rr)
else:
    for i, v in enumerate(rr.registers):
        print(f"CH{i+1}: {v/100:.2f} V")

client.close()

Saída esperada: oito linhas CHn: 220.00 V. Se aparecer timeout, confira o par de portas e a paridade — 90% dos erros de RTU são paridade ou stop bit.

Em ferramentas gráficas, qModMaster (Linux/Windows) e Modbus Poll (Windows) cobrem o mesmo cenário e são úteis para mostrar o mapa para o time de operação.

Simulando comportamento dinâmico

Registradores fixos provam que a comunicação está de pé, mas não exercitam o SCADA. Faça os valores oscilarem em uma thread separada:

# server_dynamic.py
import logging
import threading
import time
import math
import random
from pymodbus.datastore import ModbusSequentialDataBlock, ModbusSlaveContext, ModbusServerContext
from pymodbus.server import StartSerialServer
from pymodbus.framer import ModbusRtuFramer
from aem_map import initial_values, TOTAL_REGISTERS

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")

PORT = "/tmp/ttyV0"
SLAVE_ID = 1

hr_block = ModbusSequentialDataBlock(0, initial_values())
slave_ctx = ModbusSlaveContext(hr=hr_block, zero_mode=True)
server_ctx = ModbusServerContext(slaves={SLAVE_ID: slave_ctx}, single=False)

def simulate():
    t0 = time.time()
    while True:
        t = time.time() - t0
        for ch in range(8):
            # 220 V +/- 2 V senoidal com ruido pequeno
            v = 22000 + int(200 * math.sin(2 * math.pi * 0.1 * t + ch))
            v += random.randint(-20, 20)
            hr_block.setValues(ch, [v])
        # alarme de sobretensao a cada 30 s no CH1
        alarm = 1 if int(t) % 30 < 2 else 0
        hr_block.setValues(48, [alarm])
        time.sleep(0.5)

if __name__ == "__main__":
    threading.Thread(target=simulate, daemon=True).start()
    logging.info("Simulador dinamico AEM-60DC8 | %d regs", TOTAL_REGISTERS)
    StartSerialServer(
        context=server_ctx,
        framer=ModbusRtuFramer,
        port=PORT,
        baudrate=19200,
        bytesize=8,
        parity="E",
        stopbits=1,
        timeout=1,
    )

Agora as tensões oscilam levemente e um alarme dispara periodicamente. Isso já é suficiente para validar trending, deadbands, alarmes e disparo de eventos no historiador.

Integrando com seu SCADA

A ponte é a porta serial virtual. O simulador segura uma ponta, o SCADA consome a outra.

Windows com Ignition, Elipse E3 ou AVEVA Edge. Instale com0com, crie o par COM10 <-> COM11. Aponte o servidor Python para COM10 e o driver Modbus RTU do SCADA para COM11. Parâmetros consistentes em ambos os lados: 19200 8E1, slave ID 1, timeout 1000 ms. As velocidades suportadas pelo AEM-60DC8 são 4800/9600/19200/38400/57600/115200 bps — use a mesma do projeto final.

Linux. O socat cria /tmp/ttyV0 e /tmp/ttyV1. Aponte o gateway Modbus do SCADA para /tmp/ttyV1. Funciona bem para Ignition rodando em container.

Configure tags apontando para o mapa do simulador, valide leituras, escreva nos registradores de configuração para garantir round-trip e force alarmes para testar a árvore de notificações. Esse ciclo, feito em laboratório, economiza dias de campo.

Cuidado com dois pontos: byte order (a v1.03 usa big-endian em uint32, padrão Modbus) e timeout — o AEM-60DC8 real responde em até 50 ms, então o simulador é mais rápido que o hardware. Não calibre timeout pelo simulador.

Próximos passos

Este tutorial cobre o esqueleto. Em produção, você vai querer simular falhas de CRC, latência variável, perda intermitente de pacotes e respostas com slave busy — todos eventos reais em barramentos RS-485 longos.

A LRI mantém o simulador oficial do AEM-60DC8 como projeto open-source. Ele já vem com o mapa completo da v1.03, perfis de falha, modo de gravação para regravar tráfego capturado em obra e exemplos de integração com Ignition e Elipse. Baixe em github.com/lri-engenharia/aem60dc8-simulator (Secure by Design por padrão: o simulador não expõe portas de rede sem configuração explícita).

FAQ

1. Posso rodar o simulador sem hardware serial nenhum? Sim. No Linux, o socat cria pares de pty totalmente em software. No Windows, o com0com faz o mesmo com portas COM. Nenhum cabo, conversor USB-RS485 ou adaptador é necessário em laboratório.

2. Qual versão do pymodbus usar? Fixe pymodbus[serial]==3.6.9. A API mudou entre 2.x e 3.x; misturar tutoriais antigos com a versão nova é a maior fonte de erro em projetos novos.

3. O simulador serve para certificação? Não. Use para integração, parametrização SCADA e testes funcionais. Certificação metrológica e de protocolo exige o hardware real e laboratório acreditado.

4. Como simular falha de CRC? Em pymodbus 3.x, isso exige um framer customizado que corrompe o último byte da resposta com probabilidade configurável. O simulador oficial da LRI traz esse modo pronto.

5. Funciona com Modbus TCP também? Sim. Troque StartSerialServer por StartTcpServer e o ModbusRtuFramer pelo ModbusSocketFramer. O datastore e o mapa do AEM-60DC8 permanecem idênticos.

Conteúdo relacionado

Outros materiais técnicos da LRI sobre temas adjacentes.