Modbus Energy Meter Reader

Top  Previous  Next

// ----------------------------------------------------------------------------
// 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.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;