# OpenPLC 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="/files/PHBRLfnDomciHCUXTlg9" %}

***

### 🔗 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/)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://wiki.codeandcompile.com/product-reviews/smart-platforms/virtual-plcs/autonomy-edge-openplc-redefined/openplc-interfacing-with-io-link-master.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
