Skip to content

IO-Handler C++ Example

This example demonstrates how to write an IO-Handler that connects to an RTDB-based application as a TCP/IP client, using the TcpipClient library. The example is built with C++ Qt Widgets (6.11.1) and targets the IO_Example StateWORKS specification project.

In a real deployment, an IO-Handler reads physical hardware inputs (sensors, switches) and writes their values into the RTDB, and conversely receives RTDB output values and drives physical hardware (relays, actuators). Despite its close relationship to hardware, the IO-Handler has no privileged architectural role — it connects to the RTDB server as a TCP/IP client, exactly like a UI or monitoring tool would.

This example reflects that: it is a regular TcpipClient that both pokes input values into the RTDB and receives output values from it. In production, the manual UI controls would be replaced by actual hardware calls. The client role stays the same.

InitializeRTDBServerConnection() handles the full connection setup using three library functions:

  • initializes the callback RepEv() using Initialize(), called whenever an event arrives from the RTDB server
  • connects to the RTDB server using Connect()
  • subscribes to output value changes using AdviseStart()

The callback RepEv() may receive the RawData value of Do:001, the DataValue of No:001, or notification that the RTDB server has exited.

OnDiClicked() and OnNiEditingFinished() use Poke() to write Di:001 and Ni:001 into the RTDB — simulating what a real hardware driver would do.

~MainWindow() calls Disconnect() when the application closes.

Two further library functions are available but not explicitly called here:

  • Receive() — delivers a requested value from the RTDB on demand, at any time
  • AdviseStop() — seldom called explicitly; invoked internally by Disconnect()

Full implementation can be found in the Appendix.

Start SWExecStandard.exe first. On first launch you will be asked for the specification file to execute (.../Examples-Web/IO_Example/Conf/IO_Example.swd). The path is stored in .RTDB_Conf.par and reused on subsequent starts. To switch specification files, delete .RTDB_Conf.par or pass -cNAME on the command line.

Then start the IO handler application and connect.

io-example -small io-example -small

The window shows:

InputsOutputs
DI — Digital inputDO — Digital output
NI — Numerical (analog) inputNO — Numerical (analog) output
Manually set to simulate hardware input Driven by the RTDB and displayed here

Use SWMon alongside this window for testing. Setting DI and NI here simulates incoming hardware signals — watch the effects in SWMon. Conversely, changes to DO and NO triggered via SWMon will be reflected in this window.

io-example-testing -full

mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
// The callback function — called by the TcpipClient event thread on every RTDB event.
// Dispatch UI updates back to the main thread via QMetaObject::invokeMethod.
void RepEv(int Rep, const std::string &stName, const std::string &stVal, void *pOwner)
{
Q_UNUSED(Rep);
MainWindow *wnd = static_cast<MainWindow *>(pOwner);
if (!wnd)
return;
if (stName == "Do:001.Raw") {
QString value = QString::fromStdString(stVal);
QMetaObject::invokeMethod(
wnd, [wnd, value]() { wnd->UpdateDo(value); }, Qt::QueuedConnection);
}
if (stName == "No:001.Dat") {
QString value = QString::fromStdString(stVal);
QMetaObject::invokeMethod(
wnd, [wnd, value]() { wnd->UpdateNo(value); }, Qt::QueuedConnection);
}
if (stName == "IL" && stVal == "exited?") {
QMetaObject::invokeMethod(wnd, [wnd]() { wnd->Terminate(); }, Qt::QueuedConnection);
}
}
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
connect(ui->connectPushButton, &QPushButton::clicked, this, &MainWindow::OnConnectClicked);
connect(ui->DiLowRadioButton, &QRadioButton::toggled, this, &::MainWindow::OnDiClicked);
connect(ui->DiHighRadioButton, &QRadioButton::toggled, this, &::MainWindow::OnDiClicked);
connect(ui->NiLineEdit, &QLineEdit::editingFinished, this, &MainWindow::OnNiEditingFinished);
ui->statusbar->showMessage("Disconnected");
// Pass the callback function to CTCPClient
m_Client.Initialize(RepEv, this);
}
MainWindow::~MainWindow()
{
if (m_Client.Connected())
m_Client.Disconnect(true); // unadvise all before exit
delete ui;
}
// Connect to RTDB and subscribe to outputs.
bool MainWindow::InitializeRTDBServerConnection()
{
std::string host = ui->hostLineEdit->text().toStdString();
int port = ui->portLineEdit->text().toInt();
bool ok = m_Client.Connect(host, port);
ui->statusbar->showMessage(ok ? "Connected to RTDB server" : "Could not connect to RTDB server");
if (m_Client.Connected()) {
std::string value;
// AdviseStart triggers RepEv immediately with the current value,
// so no explicit Request() call is needed on startup.
m_Client.AdviseStart("Do:001", IAtt_RawData, value);
m_Client.AdviseStart("No:001", IAtt_DataValue, value);
return true;
}
return false;
}
void MainWindow::Terminate()
{
close();
}
void MainWindow::UpdateDo(const QString &value)
{
ui->DoLabel->setText(value);
}
void MainWindow::UpdateNo(const QString &value)
{
ui->NoLabel->setText(value);
}
void MainWindow::OnConnectClicked()
{
if (!InitializeRTDBServerConnection())
close();
ui->connect_frame->setDisabled(true);
ui->action_frame->setEnabled(true);
}
// Poke Di:001 into the RTDB — simulates a hardware digital input changing state.
void MainWindow::OnDiClicked()
{
std::string itemName = "Di:001";
std::string value = ui->DiHighRadioButton->isChecked() ? "1" : "0";
m_Client.Poke(itemName, value, IAtt_RawData);
}
// Poke Ni:001 into the RTDB — simulates a hardware analog input changing value.
void MainWindow::OnNiEditingFinished()
{
std::string itemName = "Ni:001";
std::string value = ui->NiLineEdit->text().toStdString();
m_Client.Poke(itemName, value, IAtt_RawData);
}