# Virtual PLC interfacing with IO-Link Master

## Real-Time Vibration Monitoring with Smart Signaling via Modbus TCP/IP

{% embed url="<https://files.gitbook.com/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FLd2M9UNfMTnw9DjDdZJz%2Fuploads%2FHVHGB1UM5ra5lxFFou1P%2FOpenPLC.mp4?alt=media&token=2699e9aa-168a-4f6c-b84a-b42de8b725ea>" %}

***

### What This Project Does

In this project, we use a **reComputer running OpenPLC runtime (vPLC)** to:

* Read **real-time vibration data** (X, Y, Z axes + temperature) from a Balluff condition monitoring sensor
* Detect objects using a **laser photo-electric sensor**
* Drive a **Balluff Smart Light** as a visual signal output
* All sensor communication happens through a **Balluff IO-Link Master** over **Modbus TCP/IP**

***

{% embed url="<https://www.youtube.com/watch?feature=youtu.be&v=IMyKHp3iog0>" %}

### System Architecture

```
[Laptop / HMI]
     │
     │ eth0: 192.168.0.11  (www / remote access)
     │
[reComputer — OpenPLC vPLC]
  vPLC IP: 192.168.100.10
  eth1 IP: 192.168.100.2
     │
     │ Modbus TCP/IP
     │
[Balluff IO-Link Master — BNI00L3]
  IP: 192.168.100.3
     ├── Port 1 → Smart Light        (BNI IOL-812-205-K037)
     ├── Port 2 → Laser Sensor       (BOS R254K-UUI-LH10-S4)
     └── Port 3 → Condition Monitor  (BCM R15E-001-DI00-01,5-S4)
```

The vPLC connects to the IO-Link Master as a **Modbus TCP Client**, polling sensor data every scan cycle.

***

### Modbus Register Map

#### 🟢 Port 1 — Smart Light (BNI IOL-812-205-K037)

| Register No. | PLC Register | Function          |
| ------------ | ------------ | ----------------- |
| 1117         | QW0          | Smart Light State |
| 1118         | QW1          | Mode              |
| 1120 \~ 1124 | QW3 \~ QW7   | Seg1 \~ Seg5      |

***

#### 🔵 Port 2 — Laser / Photo-electric Sensor (BOS R254K-UUI-LH10-S4)

| Register No. | PLC Register | Function         |
| ------------ | ------------ | ---------------- |
| 1201         | IW10         | Object Detection |

***

#### 🟠 Port 3 — Condition Monitoring Sensor (BCM R15E-001-DI00-01,5-S4)

| Register No. | PLC Register | Function |
| ------------ | ------------ | -------- |
| 1300         | IW0          | Status   |
| 1301 \~ 1302 | IW1 \~ IW2   | X-VRMS   |
| 1303 \~ 1304 | IW3 \~ IW4   | Y-VRMS   |
| 1305 \~ 1306 | IW5 \~ IW6   | Z-VRMS   |
| 1307 \~ 1308 | IW7 \~ IW8   | Temp.    |

***

### Converting Modbus Words to REAL Float Values

The Balluff condition monitoring sensor sends each float value (e.g., X-VRMS) **split across 2 x 16-bit Modbus registers** in Big-Endian format.

To get a usable `REAL` value in OpenPLC, we need to:

1. Read the **High Word** and **Low Word** from two consecutive registers
2. **Byte-swap** them (Big-Endian → Little-Endian word order)
3. Reassemble into a **32-bit IEEE 754 float** using `memcpy`

#### 📐 Byte Swap Example

```
Register 1 (H): X-VRMS = 62    → 0x003E
Register 2 (L): X-VRMS = -9446 → 0xDB1A

Original byte order:  A  B  C  D  →  00 3E DB 1A
After word swap:      B  A  D  C  →  3E 00 1A DB

IEEE 754 interpret: 0x3E001ADB = 0.1251 g
```

#### C++ Function Block (Custom OpenPLC Extension)

This logic runs inside a **C++ Function Block** registered in OpenPLC. The `loop()` function executes every scan cycle.

```cpp
VAR_INPUT
	HighWord: uint;
	LowWord: uint;
END_VAR

VAR_OUTPUT
	RealOut: real;
END_VAR
```

```cpp
/* ================================================================
 *  C/C++ FUNCTION BLOCK
 *
 *  ---------------------------------------------------------------
 *  - This function block runs **in sync** with the PLC runtime.
 *  - The `setup()` function is called once when the block initializes.
 *  - The `loop()` function is called at every PLC scan cycle.
 *  - Block input and output variables declared in the variable table
 *    can be accessed directly by name in this C/C++ code.
 *
 *  This block executes as part of the main PLC process and follows
 *  the configured scan time in the Resources. Use it for real-time
 *  control logic, fast I/O operations, or any C-based algorithms.
 * ================================================================ */

#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>

void plc_log(char *msg);

int wtor_cycle_count = 0;

void setup()
{

}

void loop()
{
    uint16_t combined_array[2];
    float converted_val = 0;
    char print_msg[1000];

    uint8_t b[4];
    uint32_t u32;

    b[0] = (uint8_t)(HighWord & 0xFF);   // B = Low byte  of Register 1 (HighWord)
    b[1] = (uint8_t)(HighWord >> 8);     // A = High byte of Register 1 (HighWord)
    b[2] = (uint8_t)(LowWord  & 0xFF);   // D = Low byte  of Register 2 (LowWord)
    b[3] = (uint8_t)(LowWord  >> 8);     // C = High byte of Register 2 (LowWord)

    // placing each byte into its correct slot BADC,  b[0]=B, b[1]=A, b[2]=D, b[3]=C
    u32 = ((uint32_t)b[0] << 24) |
          ((uint32_t)b[1] << 16) |
          ((uint32_t)b[2] << 8)  |
          ((uint32_t)b[3]);

    memcpy(&RealOut, &u32, sizeof(RealOut));
    //u32 bytes = [ b0  b1  b2  b3 ] = [ B  A  D  C ]

    // Debug logs (print once every 100 cycles to avoid flooding the logs)
    if (wtor_cycle_count == 100)
    {
        sprintf(print_msg, "Low word: %02x", LowWord);
        plc_log(print_msg);
        sprintf(print_msg, "High word: %02x", HighWord);
        plc_log(print_msg);
        sprintf(print_msg, "Converted value: %f", RealOut);
        plc_log(print_msg);

        wtor_cycle_count = 0;
    }
    else
    {
        wtor_cycle_count++;
    }

}
```

> ⚠️ **Key insight:** Simply casting the INT values to REAL will give you garbage. The byte swap is mandatory because Balluff uses Big-Endian word ordering, while OpenPLC/x86 is Little-Endian.

#### C++ Print Code Function Block

```cpp
VAR_INPUT
	print_message : bool;
	content: string;
END_VAR
```

```cpp
/* ================================================================
 *  C/C++ FUNCTION BLOCK
 *
 *  ---------------------------------------------------------------
 *  - This function block runs **in sync** with the PLC runtime.
 *  - The `setup()` function is called once when the block initializes.
 *  - The `loop()` function is called at every PLC scan cycle.
 *  - Block input and output variables declared in the variable table
 *    can be accessed directly by name in this C/C++ code.
 *
 *  This block executes as part of the main PLC process and follows
 *  the configured scan time in the Resources. Use it for real-time
 *  control logic, fast I/O operations, or any C-based algorithms.
 * ================================================================ */

#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>

// These includes are only required by the plc_log function
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <time.h>

int log_fd = -1;
bool previous_print_msg = false;

/* ================================================================
 *  Utility function to print messages on the PLC Logs.
 * ================================================================ 
 */

void plc_log(char *msg) 
{
    if (log_fd < 0) 
    {
        struct sockaddr_un addr;
        log_fd = socket(AF_UNIX, SOCK_STREAM, 0);
        if (log_fd < 0) return;
        memset(&addr, 0, sizeof(addr));
        addr.sun_family = AF_UNIX;
        strncpy(addr.sun_path, "/run/runtime/log_runtime.socket",
                sizeof(addr.sun_path) - 1);
        if (connect(log_fd, (struct sockaddr *)&addr, sizeof(addr)) == -1) 
        {
            close(log_fd);
            log_fd = -1;
            return;
        }
    }
    char buf[512];
    snprintf(buf, sizeof(buf),
        "{\"timestamp\":\"%ld\",\"level\":\"INFO\",\"message\":\"%s\"}\n",
        (long)time(NULL), msg);
    if (write(log_fd, buf, strlen(buf)) == -1) 
    {
        close(log_fd);
        log_fd = -1;
    }
}

void setup()
{


}

void loop()
{
    if ((previous_print_msg == false) && (print_message == true))
    {
        plc_log((char *)content.body);
    }

    previous_print_msg = print_message;
}
```

***

#### Main Program

```
VAR
	wX_VRMSHighWord : uint AT %IW1;
	wX_VRMSLowWord : uint AT %IW2;
	wY_VRMSHighWord : uint AT %IW3;
	wY_VRMSLowWord : uint AT %IW4;
	wZ_VRMSHighWord : uint AT %IW5;
	wZ_VRMSLowWord : uint AT %IW6;
	wTempHighWord : uint AT %IW7;
	wTempLowWord : uint AT %IW8;
	wSensor : word AT %IW0;
	wSmartLightState : word AT %QW0;
	wMode : word AT %QW1;
	wSeg1 : word AT %QW3;
	wSeg2 : word AT %QW4;
	wSeg3 : word AT %QW5;
	wSeg4 : word AT %QW6;
	wSeg5 : word AT %QW7;
	rTemp : real;
	rX_VRMS : real;
	rY_VRMS : real;
	rZ_VRMS : real;
	WtoR_convert : WTOR;
	iVibrationState : int;
	rThresholdL : real;
	rThresholdH : real;
	rMaxVib : real;
END_VAR
```

```
//Initialized values
wSmartLightState  := 1;
wMode := 34049;
rThresholdH := 7.1;
rThresholdL := 2.5;

(* Temperature *)
WtoR_convert(HighWord := wTempHighWord, LowWord := wTempLowWord);
rTemp := WtoR_convert.RealOut;

(* X_VRMS *)
WtoR_convert(HighWord := wX_VRMSHighWord, LowWord := wX_VRMSLowWord);
rX_VRMS := WtoR_convert.RealOut;

(* Y_VRMS *)
WtoR_convert(HighWord := wY_VRMSHighWord, LowWord := wY_VRMSLowWord);
rY_VRMS := WtoR_convert.RealOut;

(* Z_VRMS *)
WtoR_convert(HighWord := wZ_VRMSHighWord, LowWord := wZ_VRMSLowWord);
rZ_VRMS := WtoR_convert.RealOut;

//Finding max Vib
(* Find max vibration *)
rMaxVib := rX_VRMS;

IF rY_VRMS > rMaxVib THEN
    rMaxVib := rY_VRMS;
END_IF;

IF rZ_VRMS > rMaxVib THEN
    rMaxVib := rZ_VRMS;
END_IF;

//Classify State
(* Classify state *)
IF rMaxVib < rThresholdL THEN
    iVibrationState := 0;

ELSIF rMaxVib > rThresholdL and rMaxVib < rThresholdH THEN
    iVibrationState := 1;

ELSE
    iVibrationState := 2;
END_IF;


IF wSensor = 1 THEN
CASE iVibrationState OF
    2: // Red Triple strobe - High vibration
    wSeg1 := 261;
    wSeg2 := 261;
    wSeg3 := 261;
    wSeg4 := 261;
    wSeg5 := 261;

    1: // Orange Rotating effect - Moderate vibration
    wSeg1 := 2055;
    wSeg2 := 2055;
    wSeg3 := 2055;
    wSeg4 := 2055;
    wSeg5 := 2055;

    ELSE // Green Stable - Low vibration 
    wSeg1 := 512;
    wSeg2 := 512;
    wSeg3 := 512;
    wSeg4 := 512;
    wSeg5 := 512;

END_CASE;

ELSE //White color, No object present
    wSeg1 := 2304;
    wSeg2 := 2304;
    wSeg3 := 2304;
    wSeg4 := 2304;
    wSeg5 := 2304;

END_IF;
```

***

### Hardware Used

| Component                     | Model                             |
| ----------------------------- | --------------------------------- |
| Edge Computer (vPLC host)     | Seeed reComputer                  |
| IO-Link Master                | Balluff BNI00L3                   |
| Smart Light                   | Balluff BNI IOL-812-205-K037      |
| Laser / Photo-electric Sensor | Balluff BOS R254K-UUI-LH10-S4     |
| Condition Monitoring Sensor   | Balluff BCM R15E-001-DI00-01,5-S4 |
| PLC Runtime                   | OpenPLC Runtime v3 (vPLC)         |

***

### Source Code

> 🔗 Source files for this project are available in the following zip file or you can access it here: <https://editor.autonomylogic.com/?project\\_id=cmmtfduw800hd07n3kzotofxv>
>
> {% file src="<https://1831238825-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FLd2M9UNfMTnw9DjDdZJz%2Fuploads%2FCgqWXiLlg5Bu1Enr5EAq%2FvPLC3.zip?alt=media&token=e51aebf8-f673-4797-8768-0f8d18156fba>" %}

***

### 🔗 Resources

* 🌐 [autonomylogic.com](https://autonomylogic.com)
* 📖 [OpenPLC Runtime Docs](https://openplcproject.com)
* 🧑‍💻 [PLC project](https://editor.autonomylogic.com/?project_id=cmmtfduw800hd07n3kzotofxv)&#x20;

{% embed url="<https://www.canva.com/design/DAHDEbIV0NU/FltviviauXtBdrJOqjFWFA/edit?utm_campaign=designshare&utm_content=DAHDEbIV0NU&utm_medium=link2&utm_source=sharebutton>" %}
Key notes
{% endembed %}

***

## ♥️ Work With Me

I regularly test **industrial automation and IIoT devices**. If you’d like me to **review your product** or showcase it in my courses and YouTube channel:

📧 Email: <rajvir@codeandcompile.com> or drop me a message on [LinkedIn](https://www.linkedin.com/in/singhrajvir/)
