Skip to main content
Advanced Industrial Database Dashboard

Industrial Modbus SCADA

Build a lightweight SCADA system to monitor and control PLCs, VFDs, and other Modbus devices. Read holding registers for process values, write coils for remote control, log all data to PostgreSQL, and visualize on a real-time dashboard with gauges and charts.

6
Nodes Used
~45min
Build Time
TCP/RTU
Protocol
1s
Poll Interval

Flow Architecture

=== READ PATH (Monitoring) ===

[Inject: 1s poll] --> [Modbus Read] --> [Function: Scale Values] --> [PostgreSQL: INSERT] --> [Chart]
                     (HR 40001-40010)    (raw to eng. units)          (readings table)          |
                                                |                                          [Gauge]
                                                v
                                         [Debug: Values]

=== WRITE PATH (Control) ===

[Dashboard Button] --> [Function: Build Command] --> [Modbus Write]
  "Start Pump"          (coil address + value)        (write coil)

What You'll Need

Hardware

  • Raspberry Pi or Linux PC running EdgeFlow
  • Modbus TCP device (PLC, VFD, energy meter) on network
  • Or: RS-485 USB adapter for Modbus RTU devices
  • Ethernet or RS-485 cabling as needed

Software

  • EdgeFlow installed with Modbus module
  • PostgreSQL 14+ installed
  • Device register map (from manufacturer docs)
  • Optional: Modbus simulator for testing (diagslave, ModRSsim2)

Modbus Register Map

The register map defines how raw Modbus values map to real-world process values. This example uses a typical PLC/process controller layout. Adjust addresses and scale factors based on your actual device documentation.

Register Name Data Type Scale Unit Range
40001 Temperature INT16 ÷ 10 °C -40 to 150
40002 Pressure UINT16 ÷ 100 bar 0 to 25
40003 Flow Rate UINT16 ÷ 10 L/min 0 to 500
40004 Tank Level UINT16 ÷ 10 % 0 to 100
40005 Motor Speed UINT16 × 1 RPM 0 to 3600
40006 Motor Current UINT16 ÷ 100 A 0 to 50
40007 Voltage UINT16 ÷ 10 V 0 to 480
40008 Power UINT16 × 10 W 0 to 50000
40009 Run Hours UINT16 × 1 hours 0 to 65535
40010 Status Word UINT16 bitmask -- Bit 0=Run, 1=Fault, 2=Ready

Important: Register Addressing

Modbus register 40001 maps to protocol address 0 (zero-based). Some devices use 1-based addressing. Check your device documentation. In EdgeFlow, you configure the protocol address (0-based) and the node handles the offset automatically.

Step-by-Step Setup

1

Identify Your Modbus Registers

Obtain the register map from your device manufacturer. You need to know the register addresses, data types, and scaling factors. Use the table above as a template and fill in your actual register information.

2

Configure Network or Serial Connection

For Modbus TCP, ensure the device is reachable on your network. For Modbus RTU, connect the RS-485 adapter and identify the serial port.

# Test Modbus TCP connectivity
ping 192.168.1.100

# For RTU, find the serial port
ls /dev/ttyUSB*
# Typically: /dev/ttyUSB0

# Test with modbus-cli (optional)
# pip install modbus-cli
modbus --host 192.168.1.100 --port 502 read 40001 10
3

Set Up PostgreSQL

Create the database and table schema for storing SCADA readings:

-- Create database
CREATE DATABASE scada_monitor;

-- Connect and create table
\c scada_monitor;

CREATE TABLE readings (
    id SERIAL PRIMARY KEY,
    timestamp TIMESTAMPTZ DEFAULT NOW(),
    temperature DECIMAL(5,1),
    pressure DECIMAL(5,2),
    flow_rate DECIMAL(6,1),
    tank_level DECIMAL(4,1),
    motor_speed INTEGER,
    motor_current DECIMAL(5,2),
    voltage DECIMAL(5,1),
    power INTEGER,
    run_hours INTEGER,
    status_word INTEGER
);

-- Create index for time-based queries
CREATE INDEX idx_readings_timestamp
  ON readings (timestamp DESC);

-- Optional: auto-delete old data (keep 90 days)
CREATE EXTENSION IF NOT EXISTS pg_cron;
SELECT cron.schedule('cleanup-old-readings',
  '0 3 * * *',
  $$DELETE FROM readings WHERE timestamp < NOW() - INTERVAL '90 days'$$
);
4

Import the Flow

Copy the flow JSON below. In EdgeFlow, go to Menu → Import, paste the JSON, and click Import.

5

Configure and Deploy

Double-click the Modbus node to set the host/port (TCP) or serial port/baud (RTU). Update the PostgreSQL connection credentials. Configure the register map in the function node if your device uses different addresses. Then click Deploy.

Function Node: Scale Raw Values

This function converts raw Modbus register values to engineering units using the register map configuration:

// Scale raw Modbus register values to engineering units
// Input: msg.payload = array of 10 raw UINT16 values from registers 40001-40010

var raw = msg.payload;

// Register map: [index, name, scale_divisor, unit, signed]
var registerMap = [
    [0, "temperature",   10,   "C",     true ],
    [1, "pressure",      100,  "bar",   false],
    [2, "flow_rate",     10,   "L/min", false],
    [3, "tank_level",    10,   "%",     false],
    [4, "motor_speed",   1,    "RPM",   false],
    [5, "motor_current", 100,  "A",     false],
    [6, "voltage",       10,   "V",     false],
    [7, "power",         0.1,  "W",     false],
    [8, "run_hours",     1,    "hours", false],
    [9, "status_word",   1,    "",      false]
];

var scaled = {};

registerMap.forEach(function(reg) {
    var idx = reg[0];
    var name = reg[1];
    var scale = reg[2];
    var unit = reg[3];
    var signed = reg[4];

    var value = raw[idx];

    // Handle signed INT16
    if (signed && value > 32767) {
        value = value - 65536;
    }

    // Apply scaling
    if (scale >= 1) {
        scaled[name] = Math.round((value / scale) * 100) / 100;
    } else {
        scaled[name] = Math.round(value / scale);
    }

    scaled[name + "_unit"] = unit;
});

// Decode status word bits
var statusWord = raw[9];
scaled.status = {
    running:  !!(statusWord & 0x01),
    fault:    !!(statusWord & 0x02),
    ready:    !!(statusWord & 0x04),
    auto:     !!(statusWord & 0x08),
    remote:   !!(statusWord & 0x10)
};

scaled.timestamp = new Date().toISOString();
msg.payload = scaled;

return msg;

Configuration Details

Node Property Value Notes
modbus-read host 192.168.1.100 Modbus device IP
modbus-read port 502 Default Modbus TCP port
modbus-read unitId 1 Modbus slave address
modbus-read functionCode 3 (Read Holding) FC3 for holding registers
modbus-read startAddress 0 0-based (maps to 40001)
modbus-read quantity 10 Read 10 consecutive registers
postgresql host localhost PostgreSQL server
postgresql database scada_monitor Database name
inject interval 1000ms Poll every 1 second

Complete Flow JSON

Copy and import this flow into EdgeFlow via Menu → Import.

{
  "name": "Industrial Modbus SCADA",
  "nodes": [
    {
      "id": "inject_poll",
      "type": "inject",
      "name": "Poll 1s",
      "interval": 1,
      "intervalUnit": "s",
      "repeat": true,
      "x": 120,
      "y": 180
    },
    {
      "id": "modbus_read",
      "type": "modbus-read",
      "name": "Read Holding Registers",
      "server": "modbus_tcp_server",
      "functionCode": "3",
      "startAddress": 0,
      "quantity": 10,
      "x": 340,
      "y": 180
    },
    {
      "id": "modbus_tcp_server",
      "type": "modbus-tcp-config",
      "name": "PLC",
      "host": "192.168.1.100",
      "port": 502,
      "unitId": 1,
      "reconnectTimeout": 5000
    },
    {
      "id": "func_scale",
      "type": "function",
      "name": "Scale to Eng. Units",
      "code": "var raw = msg.payload;\nvar registerMap = [[0,'temperature',10,'C',true],[1,'pressure',100,'bar',false],[2,'flow_rate',10,'L/min',false],[3,'tank_level',10,'%',false],[4,'motor_speed',1,'RPM',false],[5,'motor_current',100,'A',false],[6,'voltage',10,'V',false],[7,'power',0.1,'W',false],[8,'run_hours',1,'hours',false],[9,'status_word',1,'',false]];\nvar scaled = {};\nregisterMap.forEach(function(reg) { var value = raw[reg[0]]; if (reg[4] && value > 32767) value -= 65536; scaled[reg[1]] = reg[2] >= 1 ? Math.round((value / reg[2]) * 100) / 100 : Math.round(value / reg[2]); });\nvar sw = raw[9]; scaled.status = { running: !!(sw & 1), fault: !!(sw & 2), ready: !!(sw & 4) };\nscaled.timestamp = new Date().toISOString();\nmsg.payload = scaled;\nreturn msg;",
      "x": 580,
      "y": 180
    },
    {
      "id": "pg_insert",
      "type": "postgresql",
      "name": "Log to PostgreSQL",
      "host": "localhost",
      "port": 5432,
      "database": "scada_monitor",
      "user": "edgeflow",
      "password": "YOUR_PASSWORD",
      "query": "INSERT INTO readings (temperature, pressure, flow_rate, tank_level, motor_speed, motor_current, voltage, power, run_hours, status_word) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)",
      "queryParams": "payload.temperature,payload.pressure,payload.flow_rate,payload.tank_level,payload.motor_speed,payload.motor_current,payload.voltage,payload.power,payload.run_hours,payload.status_word",
      "x": 820,
      "y": 140
    },
    {
      "id": "gauge_temp",
      "type": "gauge",
      "name": "Temperature",
      "group": "SCADA Dashboard",
      "label": "Temperature",
      "unit": "°C",
      "min": -40,
      "max": 150,
      "property": "payload.temperature",
      "x": 820,
      "y": 220
    },
    {
      "id": "gauge_pressure",
      "type": "gauge",
      "name": "Pressure",
      "group": "SCADA Dashboard",
      "label": "Pressure",
      "unit": "bar",
      "min": 0,
      "max": 25,
      "property": "payload.pressure",
      "x": 820,
      "y": 290
    },
    {
      "id": "chart_trend",
      "type": "chart",
      "name": "Trend Chart",
      "group": "SCADA Dashboard",
      "chartType": "line",
      "label": "Process Trends",
      "x": 820,
      "y": 360
    },
    {
      "id": "debug_values",
      "type": "debug",
      "name": "Scaled Values",
      "x": 820,
      "y": 430
    },
    {
      "id": "btn_start",
      "type": "button",
      "name": "Start Pump",
      "group": "SCADA Dashboard",
      "label": "Start Pump",
      "color": "#10b981",
      "x": 120,
      "y": 500
    },
    {
      "id": "btn_stop",
      "type": "button",
      "name": "Stop Pump",
      "group": "SCADA Dashboard",
      "label": "Stop Pump",
      "color": "#ef4444",
      "x": 120,
      "y": 570
    },
    {
      "id": "func_write_start",
      "type": "function",
      "name": "Coil ON",
      "code": "msg.payload = { address: 0, value: true };\nreturn msg;",
      "x": 340,
      "y": 500
    },
    {
      "id": "func_write_stop",
      "type": "function",
      "name": "Coil OFF",
      "code": "msg.payload = { address: 0, value: false };\nreturn msg;",
      "x": 340,
      "y": 570
    },
    {
      "id": "modbus_write",
      "type": "modbus-write",
      "name": "Write Coil",
      "server": "modbus_tcp_server",
      "functionCode": "5",
      "x": 580,
      "y": 535
    }
  ],
  "connections": [
    { "from": "inject_poll", "to": "modbus_read" },
    { "from": "modbus_read", "to": "func_scale" },
    { "from": "func_scale", "to": "pg_insert" },
    { "from": "func_scale", "to": "gauge_temp" },
    { "from": "func_scale", "to": "gauge_pressure" },
    { "from": "func_scale", "to": "chart_trend" },
    { "from": "func_scale", "to": "debug_values" },
    { "from": "btn_start", "to": "func_write_start" },
    { "from": "btn_stop", "to": "func_write_stop" },
    { "from": "func_write_start", "to": "modbus_write" },
    { "from": "func_write_stop", "to": "modbus_write" }
  ]
}

Expected Output

The function node outputs scaled engineering values every second:

{
  "payload": {
    "temperature": 42.5,
    "temperature_unit": "C",
    "pressure": 3.82,
    "pressure_unit": "bar",
    "flow_rate": 125.3,
    "flow_rate_unit": "L/min",
    "tank_level": 67.8,
    "tank_level_unit": "%",
    "motor_speed": 1480,
    "motor_speed_unit": "RPM",
    "motor_current": 12.45,
    "motor_current_unit": "A",
    "voltage": 398.2,
    "voltage_unit": "V",
    "power": 8650,
    "power_unit": "W",
    "run_hours": 12847,
    "run_hours_unit": "hours",
    "status": {
      "running": true,
      "fault": false,
      "ready": true
    },
    "timestamp": "2026-02-12T14:30:00.000Z"
  }
}

Troubleshooting

Modbus connection timeout

Verify the device IP and port are correct. Check that no firewall is blocking port 502. Ensure the Modbus device is powered on and connected to the network. For RTU, verify baud rate, parity, and stop bits match the device settings.

Illegal Function or Illegal Data Address error

The register addresses do not match the device. Double-check the register map. Some devices use 0-based addressing while EdgeFlow uses 0-based protocol addresses. Verify the function code (FC3 for holding registers, FC4 for input registers, FC1 for coils).

Values seem wrong or out of range

Check the scale factors in the function node. Verify the data type (signed vs unsigned). Some devices use 32-bit values across two registers (FLOAT32) -- you may need to combine two consecutive UINT16 values. Check byte order (Big-Endian vs Little-Endian).

PostgreSQL insert fails

Verify database credentials and that the table schema matches the INSERT statement. Check that PostgreSQL is running and accepting connections. Review the EdgeFlow debug console for specific SQL error messages.

Next Steps