Implementing PLC Communication for .NET HMI Applications
Create a Windows Forms (.NET Framework) project as the base for the HMI application.
UI Asset and Control Configuration
- Create an
imagesdirectory in the project's Debug output folder to store static PNG and animated GIF assets for equipment state display and button states. Add a PictureBox control to the main form, set itsSizeModeproperty toZoom, and assign the default static motor idle image to it. - Add two Button controls for PLC connection toggle and equipment operation control respective. Set the
FlatStyleproperty of each button toFlatto enable custom image rendering, then assign default idle state images to theirImageproperty. - Add a ComboBox control named
cbbPlcModel, populate its Items collection with two options:S7-1200andS7-1500, and set the default selected index to 0. - For floating point read/write functionality, add two TextBox controls named
txtFloatWriteInputandtxtFloatReadOutput, plus two corresponding Button controls namedbtnWriteFloatandbtnReadFloat. Store the target data block offset value in theTagproperty of both buttons.
Control FlatStyle Configuration
The FlatStyle property for Windows Forms controls supports two common configurations for inudstrial HMI use cases:
- Flat: Removes 3D border effects, renders controls as flat surfaces suitable for custom image and theme integration
- Standard: Default 3D rendered style, used for standard system control appearance.
Equipment Status Entity Definition
Create a Models directory in the project root, add an EquipmentStatus.cs class to store equipment runtime state data:
namespace IndustrialHmiDemo
{
/// <summary>
/// Production equipment runtime status entity
/// </summary>
internal class EquipmentStatus
{
public bool IsRunning { get; set; }
public bool IsStopped { get; set; }
public bool Task1Active { get; set; }
public bool Task2Active { get; set; }
public bool Task3Active { get; set; }
public bool HealthState { get; set; } // true = normal operation, false = active alarm
public ushort OperationMode { get; set; } // 1=standard, 2=accelerated, 3=full load
public float PositionX { get; set; }
public float PositionY { get; set; }
public float PositionZ { get; set; }
public float FeedRate1 { get; set; }
public float FeedRate2 { get; set; }
}
}
PLC Comunication Service Implementation
Create a Services directory, add a PlcControlService.cs class to encapsulate all PLC communication logic, avoiding tight coupling with UI layer code:
using S7NetPlus;
using System;
namespace IndustrialHmiDemo.Services
{
/// <summary>
/// PLC connection and data operation management service
/// </summary>
internal class PlcControlService
{
private Plc _s7PlcClient = null;
/// <summary>
/// Establish connection to target S7 series PLC
/// </summary>
/// <param name="ipAddress">PLC LAN IP address</param>
/// <param name="plcModel">PLC model identifier</param>
/// <returns>Connection operation result message</returns>
public string EstablishConnection(string ipAddress, string plcModel)
{
try
{
CpuType parsedCpuType = (CpuType)Enum.Parse(typeof(CpuType), plcModel.Replace("-", ""), ignoreCase: true);
_s7PlcClient = new Plc(parsedCpuType, ipAddress, rack: 0, slot: 0);
_s7PlcClient.Open();
return "PLC connection established successfully";
}
catch (Exception ex)
{
return $"PLC connection failed: {ex.Message}";
}
}
/// <summary>
/// Terminate active PLC connection and release resources
/// </summary>
public void TerminateConnection()
{
if (_s7PlcClient != null && _s7PlcClient.IsConnected)
{
_s7PlcClient.Close();
}
}
/// <summary>
/// Write arbitrary data to specified PLC variable address
/// </summary>
/// <param name="fullVariableAddress">Full PLC variable address string</param>
/// <param name="payload">Data to write</param>
/// <returns>Write operation result</returns>
public string WriteToAddress(string fullVariableAddress, object payload)
{
lock (this)
{
try
{
_s7PlcClient.Write(fullVariableAddress, payload);
return "Data write completed successfully";
}
catch (Exception ex)
{
return $"Data write failed: {ex.Message}";
}
}
}
/// <summary>
/// Overload: Write single-precision float value to specified data block offset
/// </summary>
/// <param name="dbNumber">Target data block number</param>
/// <param name="byteOffset">Byte offset in target data block</param>
/// <param name="value">Float value to write</param>
/// <returns>Write operation result</returns>
public string WriteFloatValue(int dbNumber, int byteOffset, float value)
{
lock (this)
{
try
{
_s7PlcClient.Write(DataType.DataBlock, dbNumber, byteOffset, value);
return "Float value write successful";
}
catch (Exception ex)
{
return $"Float value write failed: {ex.Message}";
}
}
}
/// <summary>
/// Read single-precision float value from specified data block offset
/// </summary>
/// <param name="dbNumber">Target data block number</param>
/// <param name="byteOffset">Byte offset in target data block</param>
/// <returns>Read float value or error message</returns>
public string ReadFloatValue(int dbNumber, int byteOffset)
{
try
{
var readResult = _s7PlcClient.Read(DataType.DataBlock, dbNumber, byteOffset, VarType.Real, 1);
return readResult.ToString();
}
catch (Exception ex)
{
return $"Float value read failed: {ex.Message}";
}
}
}
}
Main Form Event Implementation
Bind event handlers to form controls to implement user interaction logic:
using IndustrialHmiDemo.Services;
using System;
using System.Drawing;
using System.Windows.Forms;
namespace IndustrialHmiDemo
{
public partial class MainInterface : Form
{
private readonly PlcControlService _plcControlService = new PlcControlService();
public MainInterface()
{
InitializeComponent();
// Initialize control state tags
btnPlcConnection.Tag = "off";
btnEquipmentControl.Tag = "off-DB1.DBX0.0";
btnWriteFloat.Tag = "0";
btnReadFloat.Tag = "0";
}
private void btnPlcConnection_Click(object sender, EventArgs e)
{
if (btnPlcConnection.Tag.ToString() == "off")
{
string connectionResult = _plcControlService.EstablishConnection("192.168.0.10", cbbPlcModel.Text);
btnPlcConnection.Tag = "on";
btnPlcConnection.Image = Image.FromFile(@"images\plc_connected.png");
MessageBox.Show(connectionResult);
}
else
{
_plcControlService.TerminateConnection();
btnPlcConnection.Tag = "off";
btnPlcConnection.Image = Image.FromFile(@"images\plc_disconnected.png");
}
}
private void btnEquipmentControl_Click(object sender, EventArgs e)
{
if (btnPlcConnection.Tag.ToString() == "off")
{
MessageBox.Show("Please establish PLC connection first", "Operation Prompt");
return;
}
string[] tagSegments = btnEquipmentControl.Tag.ToString().Split('-');
if (tagSegments[0] == "off")
{
picMotorDisplay.Image = Image.FromFile(@"images\motor_running.gif");
btnEquipmentControl.Image = Image.FromFile(@"images\btn_active.png");
btnEquipmentControl.Tag = $"on-{tagSegments[1]}";
_plcControlService.WriteToAddress(tagSegments[1], true);
}
else
{
picMotorDisplay.Image = Image.FromFile(@"images\motor_idle.png");
btnEquipmentControl.Image = Image.FromFile(@"images\btn_inactive.png");
btnEquipmentControl.Tag = $"off-{tagSegments[1]}";
_plcControlService.WriteToAddress(tagSegments[1], false);
}
}
private void btnWriteFloat_Click(object sender, EventArgs e)
{
if (!float.TryParse(txtFloatWriteInput.Text, out float writeValue))
{
MessageBox.Show("Please enter a valid floating point number", "Input Validation Error");
return;
}
int offset = int.Parse(btnWriteFloat.Tag.ToString());
string writeResult = _plcControlService.WriteFloatValue(1, offset, writeValue);
txtFloatWriteInput.Clear();
MessageBox.Show(writeResult);
}
private void btnReadFloat_Click(object sender, EventArgs e)
{
int offset = int.Parse(btnReadFloat.Tag.ToString());
string readResult = _plcControlService.ReadFloatValue(1, offset);
if (readResult.Contains("failed"))
{
MessageBox.Show(readResult);
return;
}
txtFloatReadOutput.Text = readResult;
}
}
}