|
// ----------------------------------------------------------------------------
// Program: EM111 Energy Meter Reader Demo
//
// Description:
// This educational program demonstrates how to read energy data from a
// Carlo Gavazzi EM111 energy meter using Modbus RTU and publish the
// results to an MQTT broker via the LAN (Ethernet) interface.
//
// Key Concepts Demonstrated:
// 1. LAN Communication:
// - Initializing the Ethernet interface using the LAN interface.
// - Configuring the MQTT client to bind specifically to the LAN interface.
//
// 2. Modbus RTU Master (RS485):
// - Configuring Serial Port 2 (19200, 8, N, 1).
// - Reading 32-bit registers (Voltage, Current, Power, Energy) from the meter.
// - Parsing LSW/MSW byte order specific to EM111.
//
// 3. Data Serialization & Publishing:
// - Formatting data as a JSON object.
// - Publishing to a public MQTT broker.
//
// Hardware Setup:
// - RTCU Device with RS485 on Port 2 and Ethernet.
// - Carlo Gavazzi EM111 connected to RS485 port 2.
// Recommended device: NX-400 with the meter connected to RS485-1.
// - Ethernet cable connected to a network with Internet access.
// ----------------------------------------------------------------------------
INCLUDE rtcu.inc
// ----------------------------------------------------------------------------
// Main Program
// ----------------------------------------------------------------------------
PROGRAM EM111_Reader;
VAR
// Handles
mb_net : INT; // Handle for Modbus Network
mq_h : INT; // Handle for MQTT Client
rc : INT; // Return code
// Modbus Function Block Instance
mb_rcv : modbusReceive;
// Buffers
tx_frame : ARRAY[1..10] OF SINT; // Buffer for request
rx_frame : ARRAY[1..255] OF SINT; // Buffer for response
mqtt_payload: ARRAY[1..512] OF BYTE; // Buffer for MQTT Payload
// Helper variables for Parsing
val_lsw : DINT;
val_msw : DINT;
raw_int32 : DINT;
raw_int16 : INT;
payload_len : INT;
// Engineering Values
em_volts : FLOAT;
em_amps : FLOAT;
em_watts : FLOAT;
em_hz : FLOAT;
em_kwh : FLOAT;
// Timer
poll_timer : TON;
// JSON
json_root : SYSHANDLE;
json_str : STRING;
// ID
device_id : STRING;
END_VAR;
// ------------------------------------------------------------------------
// PHASE 1: Initialization
// ------------------------------------------------------------------------
DebugMsg(message:="System: Booting...");
// 0. Generate device id.
// Used for connecting to the MQTT server and publishing data.
device_id:="RTCU_"+dintToStr(v:=boardSerialNumber());
// 1. Initialize LAN Interface (Ethernet)
// Using constant from tcpip.inc for LAN1
rc := netOpen(iface:=_NET_IFACE_LAN1);
IF rc = 0 THEN
DebugMsg(message:="Network: LAN1 Interface opened.");
ELSE
DebugFmt(message:="Network: Failed to open LAN1. RC=\1", v1:=rc);
END_IF;
// 2. Open Modbus RTU on Serial Port 2
// 19200 Baud, 8 Data bits, No Parity (0), 1 Stop bit
mb_net := modbusOpen(port:=2, baud:=19200, bit:=8, parity:=0, stopbit:=1, mode:=0);
IF mb_net < 1 THEN
DebugFmt(message:="Modbus: Open failed! Error=\1", v1:=mb_net);
ELSE
DebugMsg(message:="Modbus: Port 2 opened (19200,8,N,1).");
END_IF;
// 3. Open MQTT Connection via LAN
// Explicitly set iface:=_NET_IFACE_LAN1 to ensure traffic goes over Ethernet.
mq_h := mqttOpen(ip:="test.mosquitto.org", port:=1883, clientid:=device_id, iface:=_NET_IFACE_LAN1);
IF mq_h < 1 THEN
DebugMsg(message:="MQTT: Client init failed.");
ELSE
DebugMsg(message:="MQTT: Client initialized on LAN.");
END_IF;
// Initialize Poll Timer (5 seconds)
poll_timer(pt := 5000);
DebugMsg(message:="System: Entering Loop.");
// ------------------------------------------------------------------------
// PHASE 2: Cyclic Execution
// ------------------------------------------------------------------------
BEGIN
// Update timer
poll_timer(trig := TRUE);
// Trigger Poll
IF poll_timer.q THEN
// ------------------------------------------------------------
// Step 1: Send Modbus Request
// ------------------------------------------------------------
// EM111 Map: Volts (0), Amps (2), Power (4)... kWh (16)
// Reading 18 registers covers the main block.
tx_frame[1] := 4; // Func 04 (Read Input Registers)
tx_frame[2] := 0; // Addr Hi
tx_frame[3] := 0; // Addr Lo
tx_frame[4] := 0; // Qty Hi
tx_frame[5] := 18; // Qty Lo
rc := modbusSend(net_id:=mb_net, unit_id:=1, frame:=ADDR(tx_frame), size:=5);
IF rc = 0 THEN
// --------------------------------------------------------
// Step 2: Wait & Receive
// --------------------------------------------------------
IF modbusWaitData(net_id:=mb_net, timeout:=500) THEN
// Read data
mb_rcv(net_id:=mb_net, frame:=ADDR(rx_frame), maxsize:=255);
// Check Status (0 = Success) and Size
IF mb_rcv.status = 0 AND mb_rcv.size >= 38 THEN
// ------------------------------------------------
// Step 3: Parse Data
// ------------------------------------------------
// NOTE: SINT values are signed. Mask with 16#FF to ensure unsigned byte values.
// -- Voltage (Reg 0-1, Index 3) --
val_lsw := ((rx_frame[3] AND 16#FF) * 256) + (rx_frame[4] AND 16#FF);
val_msw := ((rx_frame[5] AND 16#FF) * 256) + (rx_frame[6] AND 16#FF);
raw_int32 := (shl32(in:=val_msw, n:=16)) OR val_lsw;
em_volts := FLOAT(raw_int32) * 0.1;
// -- Current (Reg 2-3, Index 7) --
val_lsw := ((rx_frame[7] AND 16#FF) * 256) + (rx_frame[8] AND 16#FF);
val_msw := ((rx_frame[9] AND 16#FF) * 256) + (rx_frame[10] AND 16#FF);
raw_int32 := (shl32(in:=val_msw, n:=16)) OR val_lsw;
em_amps := FLOAT(raw_int32) * 0.001;
// -- Active Power (Reg 4-5, Index 11) --
val_lsw := ((rx_frame[11] AND 16#FF) * 256) + (rx_frame[12] AND 16#FF);
val_msw := ((rx_frame[13] AND 16#FF) * 256) + (rx_frame[14] AND 16#FF);
raw_int32 := (shl32(in:=val_msw, n:=16)) OR val_lsw;
em_watts := FLOAT(raw_int32) * 0.1;
// -- Hz (Reg 15, Index 33) --
// Single INT16.
raw_int16 := INT((rx_frame[33] AND 16#FF) * 256) + INT(rx_frame[34] AND 16#FF);
em_hz := FLOAT(raw_int16) * 0.1;
// -- Total Energy kWh (Reg 16-17, Index 35) --
val_lsw := ((rx_frame[35] AND 16#FF) * 256) + (rx_frame[36] AND 16#FF);
val_msw := ((rx_frame[37] AND 16#FF) * 256) + (rx_frame[38] AND 16#FF);
raw_int32 := (shl32(in:=val_msw, n:=16)) OR val_lsw;
em_kwh := FLOAT(raw_int32)* 0.1;
// Debug Output
DebugFmt(message:="Read: V=\1 Hz=\2 W=\3 kWh=\4",
v1:=INT(em_volts),
v2:=INT(em_hz),
v3:=INT(em_watts),
v4:=DINT(em_kwh));
// ------------------------------------------------
// Step 4: Publish
// ------------------------------------------------
IF mqttConnected(handle:=mq_h) THEN
IF jsonCreateObject(o:=json_root) = 1 THEN
// Use jsonSetValueFloat for Objects
jsonSetValueFloat(o:=json_root, key:="voltage", value:=em_volts);
jsonSetValueFloat(o:=json_root, key:="current", value:=em_amps);
jsonSetValueFloat(o:=json_root, key:="power", value:=em_watts);
jsonSetValueFloat(o:=json_root, key:="freq", value:=em_hz);
jsonSetValueFloat(o:=json_root, key:="energy", value:=em_kwh);
jsonToString(o:=json_root, str:=json_str);
jsonFree(o:=json_root);
// Copy string to memory buffer for transmission
payload_len := strLen(str:=json_str);
strToMemory(dst:=ADDR(mqtt_payload), str:=json_str, len:=payload_len);
// Publish using the memory buffer
mqttPublish(handle:=mq_h, topic:=device_id+"/em111/data", data:=ADDR(mqtt_payload), size:=strLen(str:=json_str));
END_IF;
ELSE
// If not connected, rely on client/broker to re-establish or handle next loop
DebugMsg(message:="MQTT: Not connected (LAN)");
END_IF;
ELSE
DebugFmt(message:="Modbus: Bad response. Stat=\1 Len=\2", v1:=mb_rcv.status, v2:=mb_rcv.size);
END_IF;
ELSE
DebugMsg(message:="No data received.");
END_IF;
END_IF;
poll_timer(trig := FALSE);
END_IF;
END;
END_PROGRAM;
|