Usage guide
Two layers
| Layer | Header | Use it when |
|---|---|---|
| Low-level core | slmp_minimal.h |
You want fixed caller-owned buffers, explicit slmp::DeviceAddress values, and direct sync or async SLMP calls. |
| High-level facade | slmp_high_level.h |
You want string addresses, typed values, named snapshots, and reusable polling plans. |
The high-level layer is optional. Include slmp_high_level.h explicitly when you use helpers such as slmp::highlevel::readTyped.
Every individual bit-write entry uses C++ bool, including direct, extended,
random, typed, named, and bit-in-word operations. No numeric or string
compatibility overload is provided. Packed bit-block words are a distinct
wire-level input and remain uint16_t values.
Semantic bit APIs accept only bit-addressable device families such as M,
X, Y, and B. Word families such as D, W, R, ZR, G, and HG
are rejected before request construction. Conversely, typed and named numeric
or string values require word-addressable families. Use an explicit low-level
word API when intentionally reading 16 packed bit-device states, and use .n
or writeBitInWord for one bit inside a word device. The library never changes
an invalid bit-unit call into a word mask or read-modify-write operation.
Every device span must fit the address namespace selected by the active wire
format. Legacy/Q-L device specifications end at 0xFFFFFF; iQ-R device
specifications end at 0xFFFFFFFF; link-direct device specifications always
use the Q/L layout and therefore always end at 0xFFFFFF. An ordinary
word-device word and a direct bit consume one device number per point; an
ordinary DWord or float32 consumes two. Word-unit access to a bit device
consumes sixteen bit numbers per wire word, its DWord form consumes thirty-two,
and one packed bit-block point also consumes sixteen. Native long/DWord device
families use their protocol-defined logical width—for example, four LTN/LSTN
current-value words represent one logical timer. A range crossing the wire
ceiling returns InvalidArgument before request construction or transport
activity. This is only a wire-representability check: the library does not use
a PLC profile's practical device-range catalog as a pre-send address guard.
Setup
This complete TCP setup creates the transport, buffers, slmp::SlmpClient, and profile configuration.
For UDP transport, keep the same host 192.168.250.100 and use UDP port 1035.
ArduinoUdpTransport requires a numeric remote IP address and discards every
datagram whose source IP address or port differs from that configured endpoint.
#include <Arduino.h>
#include <WiFi.h>
#include <slmp_arduino_transport.h>
#include <slmp_high_level.h>
#include <slmp_minimal.h>
constexpr char kWifiSsid[] = "YOUR_WIFI_SSID";
constexpr char kWifiPassword[] = "YOUR_WIFI_PASSWORD";
constexpr char kPlcHost[] = "192.168.250.100";
constexpr uint16_t kTcpPort = 1025;
constexpr auto kProfile = slmp::highlevel::PlcProfile::IqR;
WiFiClient tcp;
slmp::ArduinoClientTransport transport(
tcp,
slmp::configureEsp32WifiClientKeepAlive,
slmp::writeEsp32WifiClientNonBlocking);
uint8_t txBuffer[160] = {};
uint8_t rxBuffer[160] = {};
slmp::SlmpClient plc(transport, kProfile, slmp::TargetAddress{0x00, 0xFF, slmp::module_io::OwnStation, 0x00}, txBuffer, sizeof(txBuffer), rxBuffer, sizeof(rxBuffer));
void setup() {
Serial.begin(115200);
WiFi.begin(kWifiSsid, kWifiPassword);
while (WiFi.status() != WL_CONNECTED) {
delay(250);
}
if (plc.connect(kPlcHost, kTcpPort)) {
Serial.println("PLC connected");
}
}
void loop() {
delay(1000);
}
The TCP adapter requires a keepalive configurator and an explicit non-blocking
transmit policy. On ESP32, use the supplied helpers shown above: the transmit
helper performs one MSG_DONTWAIT socket send without waiting or retrying. For
another Arduino stack, writeArduinoClientWhenReady calls Client::write()
only after availableForWrite() reports positive immediate capacity and caps
the call to that capacity. Do not use it when that platform cannot guarantee
the readiness meaning. Missing policies and missing or failed keepalive setup
make the connection fail.
Routing / target station
Every client must receive a complete target when it is constructed. For a directly connected own station, specify all four values explicitly.
slmp::TargetAddress controls the SLMP destination header. It is not a device
family selector; routed devices such as Un\Gn and Jn\... still need their
own address syntax.
const slmp::TargetAddress target{
0x01,
0x02,
slmp::module_io::OwnStation,
0x00,
};
slmp::SlmpClient plc(transport, kProfile, target, txBuffer, sizeof(txBuffer), rxBuffer, sizeof(rxBuffer));
Use slmp::module_io constants such as slmp::module_io::MultipleCpu2 when routing
to multi-CPU targets. The target is fixed for the client lifetime.
For iQ-R multi-CPU U3En\HG... access, the qualified device never changes the
request target. Construct a separate client with the destination CPU target
when a write must be reflected there. A write can return a normal end code
without changing the intended CPU buffer when the selected request target
identifies a different CPU or Own Station. Cross-CPU reads remain valid. See the
shared iQ-R target guidance.
Extended device access
G, HG, and J devices are not normal standalone addresses. The C++ high-level
facade is for normal device strings such as D100:U; use the low-level
slmp::SlmpClient APIs for extended device routes.
| Address form | Low-level call shape |
|---|---|
U3\G100 |
readWordsModuleBuf(3, false, 100, ...) |
U3E0\HG0 |
readWordsModuleBuf(0x03E0, true, 0, ...) |
J2\SW10 |
readWordsLinkDirect(2, slmp::DeviceCode::SW, 0x10, ...) |
J1\X10 |
readBitsLinkDirect(1, slmp::DeviceCode::X, 0x10, ...) |
The selected PLC profile and the actual PLC configuration still decide whether the route is accepted.
Module-buffer writes change configured module state. Run this example only on a
controlled test PLC and a buffer range that the module documentation identifies
as safe to modify. Save the original words first. If a write or restoration
returns OutcomeUnknown, do not resend it automatically; reconnect, inspect the
module state, and reconcile it under an explicit test policy.
uint16_t originalModuleWords[4] = {};
const slmp::Error moduleReadErr =
plc.readWordsModuleBuf(3, false, 100, 4, originalModuleWords, 4);
if (moduleReadErr == slmp::Error::Ok) {
const uint16_t moduleWrite[] = {1, 2, 3, 4};
const slmp::Error moduleWriteErr =
plc.writeWordsModuleBuf(3, false, 100, moduleWrite, 4);
if (moduleWriteErr == slmp::Error::Ok) {
const slmp::Error restoreErr =
plc.writeWordsModuleBuf(3, false, 100, originalModuleWords, 4);
if (restoreErr == slmp::Error::OutcomeUnknown) {
// The restore may have taken effect. Inspect and reconcile the buffer manually.
} else if (restoreErr != slmp::Error::Ok) {
// Restoration failed. Inspect and reconcile the buffer manually.
}
} else if (moduleWriteErr == slmp::Error::OutcomeUnknown) {
// The test values may be present. Inspect state; do not retry or restore blindly.
} else {
// Report the confirmed write failure.
}
} else {
// Report the original-value read failure; no write was attempted.
}
uint16_t extendUnitWords[2] = {};
const slmp::Error extendReadErr =
plc.readExtendUnitWords(0, 2, slmp::module_io::MultipleCpu1, extendUnitWords, 2);
if (extendReadErr != slmp::Error::Ok) {
// Report the read failure.
}
uint16_t linkWords[1] = {};
const slmp::Error linkWordReadErr =
plc.readWordsLinkDirect(2, slmp::DeviceCode::SW, 0x10, 1, linkWords, 1);
if (linkWordReadErr != slmp::Error::Ok) {
// Report the read failure.
}
bool linkBits[16] = {};
const slmp::Error linkBitReadErr =
plc.readBitsLinkDirect(1, slmp::DeviceCode::X, 0x10, 16, linkBits, 16);
if (linkBitReadErr != slmp::Error::Ok) {
// Report the read failure.
}
For extended random access, build slmp::ExtDeviceSpec entries:
const slmp::ExtDeviceSpec wordDevices[] = {
slmp::ExtDeviceSpec::moduleBuf(3, false, 100),
slmp::ExtDeviceSpec::linkDirect(2, slmp::DeviceCode::SW, 0x10),
};
uint16_t values[2] = {};
const slmp::Error randomExtReadErr =
plc.readRandomExt(wordDevices, 2, values, 2, nullptr, 0, nullptr, 0);
if (randomExtReadErr != slmp::Error::Ok) {
// Report the read failure.
}
Extended random writes reject duplicate or overlapping destinations within one request. Module slot or link-network identity is part of the destination, so the same numeric device on two different qualified routes remains distinct.
Monitor, self-test, and Clear Error
Monitor registration and each cycle are separate one-request operations. Pass the registered Word and DWord counts to every cycle; the client does not auto-register, retry, or infer them. Calling a cycle before PLC registration sends one cycle request and returns the PLC error. The combined expected count must be nonzero and cannot exceed the selected profile's monitor-registration limit.
Monitor registration and Clear Error change PLC state. Run them only on a
controlled test PLC. Clear Error clears the current PLC error indication, so
record the diagnostic state first and check its result explicitly. An
OutcomeUnknown result requires manual state inspection and must not be retried
automatically.
const slmp::DeviceAddress words[] = {slmp::dev::D(kProfile, slmp::dev::dec(120))};
const slmp::DeviceAddress dwords[] = {slmp::dev::D(kProfile, slmp::dev::dec(200))};
const slmp::Error registerErr = plc.registerMonitorDevices(words, 1, dwords, 1);
if (registerErr == slmp::Error::Ok) {
uint16_t wordValues[1] = {};
uint32_t dwordValues[1] = {};
const slmp::Error cycleErr =
plc.runMonitorCycle(wordValues, 1, 1, dwordValues, 1, 1);
if (cycleErr != slmp::Error::Ok) {
// Report the monitor-cycle failure before issuing another command.
}
} else {
// Report registration failure. Do not run a cycle for an unconfirmed registration.
}
const uint8_t testData[] = {'A', '1', 'B', '2', 'C', '3', 'D', '4'};
uint8_t echo[sizeof(testData)] = {};
size_t echoLength = 0;
const slmp::Error selfTestErr =
plc.selfTestLoopback(testData, sizeof(testData), echo, sizeof(echo), echoLength);
if (selfTestErr != slmp::Error::Ok) {
// Report the self-test failure before issuing another command.
}
// Opt in only after recording the PLC error state on a controlled test PLC.
const bool clearErrorApproved = false;
if (clearErrorApproved) {
const slmp::Error clearErr = plc.clearError();
if (clearErr != slmp::Error::Ok) {
// Surface the result. OutcomeUnknown requires manual state reconciliation.
}
}
Self-test accepts only 1–960 ASCII 0-9/A-F bytes and succeeds only when the
declared length, actual length, and echo match exactly. Clear Error always uses
the fixed empty-payload command.
remoteReset() returns after the fixed RESET frame has been transmitted and
then closes the transport. Reconnect explicitly before another request and
verify PLC state when the application requires confirmation that RESET occurred.
Strict profile
SlmpClient enables strict profile checks by default. With a selected profile, operations known to be unavailable for that PLC are rejected before sending.
Profile guard bypass is not part of the normal public API. Profile evidence collection uses separate maintainer tooling.
Remote password
Remote password lock/unlock commands are available on the low-level slmp::SlmpClient.
The C++ high-level facade does not automatically unlock or lock a remote password.
Run this sequence only against a controlled test PLC whose password and recovery
route are known. If your PLC route uses remote password protection, unlock after
connecting and lock before closing.
Always surface the re-lock result. If unlock or re-lock returns OutcomeUnknown,
do not resend it automatically: reconnect through the approved route, inspect the
PLC lock state, and reconcile it explicitly because access may remain unlocked.
const slmp::Error unlockErr = plc.remotePasswordUnlock("secret");
if (unlockErr == slmp::Error::Ok) {
slmp::highlevel::Value value;
const slmp::Error readErr = slmp::highlevel::readTyped(plc, "D100:U", value);
if (readErr != slmp::Error::Ok) {
// Report the read failure, but still attempt to restore the lock state.
}
const slmp::Error lockErr = plc.remotePasswordLock("secret");
if (lockErr != slmp::Error::Ok) {
// Surface this result. OutcomeUnknown may mean access remains unlocked.
}
} else if (unlockErr == slmp::Error::OutcomeUnknown) {
// The lock state is unknown. Inspect and reconcile it; do not retry blindly.
} else {
// Report the confirmed unlock failure.
}
For C200-series password end codes, see the shared
SLMP Troubleshooting & Codes
page.
SLMP response end codes
When the PLC returns a non-zero SLMP end code, low-level calls return slmp::Error::PlcError.
Read lastEndCode() for the PLC response code and lastErrorInfo() when the PLC returned the structured error-information block.
When that block contains at least its structured nine bytes, its network, station, module I/O,
multidrop station, command, and subcommand must identify the active request. A mismatch is a
malformed response: reads return slmp::Error::ProtocolError, while a possibly transmitted
state-changing request returns slmp::Error::OutcomeUnknown with cause
slmp::Error::ProtocolError. The transport generation is closed and no retry or route fallback is
performed. Matching error information may contain trailing bytes; the complete frame remains
available through lastResponseFrame(). Responses with fewer than nine error-information bytes
retain their existing behavior.
slmp::highlevel::Value value;
const slmp::Error err = slmp::highlevel::readTyped(plc, "D100:U", value);
if (err == slmp::Error::PlcError) {
Serial.printf("SLMP end_code=0x%04X\n", plc.lastEndCode());
if (plc.hasLastErrorInfo()) {
const slmp::SlmpErrorInfo& info = plc.lastErrorInfo();
Serial.printf("command=0x%04X\n", info.command);
Serial.printf("subcommand=0x%04X\n", info.subcommand);
}
}
Timeout, close, and unknown write outcomes
The transaction timeout is one absolute deadline starting immediately before
the first transport write() attempt. Time spent after begin* and before the
first update() therefore does not consume the transaction budget. Partial
writes, response framing, discarded foreign
routes or 4E serials, body reads, and decoding do not restart it. Expiration
closes the transport generation. A later operation requires an explicit
successful reconnect.
Configure this deadline with setTimeoutMs() only while the client is idle. Every non-zero
uint32_t value is accepted while idle and applies to later requests; zero is
slmp::Error::InvalidArgument. While any request is active, valid values and zero both return
slmp::Error::Busy without changing timeoutMs(), the request state, or its deadline. The active
request snapshots the configured value at its first transport write attempt; exact-boundary and
32-bit clock-wrap behavior are therefore unaffected by later configuration attempts.
connect() is a separate explicit operation. Its blocking bound and failure
behavior belong to the selected ITransport; setTimeoutMs() controls the
SLMP transaction after connection, not a portable connection deadline.
Synchronous request methods use one library-owned wait loop. After update()
leaves an active request without sending, receiving, completing, or failing,
the loop calls ITransport::cooperativeWait(maximum_wait_ms). The supplied
bound is 0 for the first no-progress observation, requesting a scheduler
yield without an intentional time sleep. A consecutive no-progress observation
uses at most 1 ms, and any observed progress resets the phase to yield-first.
Every positive bound remains no greater than the remaining transaction deadline.
A custom transport must implement this required method by yielding to its scheduler/network stack
or waiting for relevant I/O readiness, may return early, and must not send,
receive, retry, complete, or otherwise mutate the active SLMP request. An empty
no-op implementation that permits an unbounded tight spin is not supported.
Asynchronous begin*/update() operation remains entirely caller-driven and
never invokes cooperativeWait(). The application chooses its own task,
event-loop, or scheduler cadence between update() calls.
A read that expires returns slmp::Error::Timeout. Closing an active read
returns slmp::Error::Closed. If any byte of a state-changing request may have
been sent and confirmation then fails, the result is
slmp::Error::OutcomeUnknown; inspect lastOutcomeUnknownReason() for the
machine-readable cause. The PLC may already contain the requested change, so
never retry that operation automatically.
const slmp::Error err = plc.writeOneWord(
slmp::dev::D(kProfile, slmp::dev::dec(100)), 1234);
if (err == slmp::Error::OutcomeUnknown) {
const slmp::Error cause = plc.lastOutcomeUnknownReason();
// Reconnect and inspect the controlled process/PLC state before deciding
// whether another write is safe. Do not automatically resend here.
(void)cause;
}
Only one request may be active. A second begin* call returns
slmp::Error::Busy before changing the active request, frame, counters, output
destination, or transport. High-level communicating helpers apply the same
Busy-first rule. connect() likewise returns false, exposes
slmp::Error::Busy through lastError(), and does not close or replace the
active operation. Do not invoke one client from multiple threads; use a
separate client, transport, and buffers per thread or task. Independent client
instances can progress concurrently.
Fixed-buffer capacity contract
Every operation proves all fixed capacities before request encoding, 4E serial
allocation, request-state publication, or transport activity. SLMP Ethernet
does not use byte escaping, so the TX worst case is the complete frame:
15 + command payload bytes for 3E or 19 + command payload bytes for 4E,
subject also to the protocol/profile limit and
ITransport::maximumRequestFrameSize().
The RX requirement is the frame prefix (9 bytes for 3E or 13 for 4E), the
two-byte end code, and the larger of the operation's maximum success data or
the nine-byte PLC error-information area. Caller-owned output arrays have an
independent element capacity. runMonitorCycle therefore requires explicit
word and DWord output capacities, and readRandomLabels requires
maximum_response_data_bytes because label value sizes are not derivable from
the request. A capacity failure returns BufferTooSmall; a protocol, profile,
point-count, or wire-field limit returns InvalidArgument. Neither result
sends or partially publishes a request.
Read a single value
slmp::highlevel::readTyped reads one logical value from one address.
| Address form | Value type | Field to read |
|---|---|---|
D100:U |
slmp::highlevel::ValueType::U16 |
value.u16 |
D100:S |
slmp::highlevel::ValueType::S16 |
value.s16 |
D200:D |
slmp::highlevel::ValueType::U32 |
value.u32 |
D200:L |
slmp::highlevel::ValueType::S32 |
value.s32 |
D300:F |
slmp::highlevel::ValueType::Float32 |
value.f32 |
M1000:BIT or D50.3 |
slmp::highlevel::ValueType::Bit |
value.bit |
#include <Arduino.h>
#include <WiFi.h>
#include <slmp_arduino_transport.h>
#include <slmp_high_level.h>
#include <slmp_minimal.h>
constexpr char kWifiSsid[] = "YOUR_WIFI_SSID";
constexpr char kWifiPassword[] = "YOUR_WIFI_PASSWORD";
constexpr char kPlcHost[] = "192.168.250.100";
constexpr uint16_t kTcpPort = 1025;
constexpr auto kProfile = slmp::highlevel::PlcProfile::IqR;
WiFiClient tcp;
slmp::ArduinoClientTransport transport(
tcp,
slmp::configureEsp32WifiClientKeepAlive,
slmp::writeEsp32WifiClientNonBlocking);
uint8_t txBuffer[160] = {};
uint8_t rxBuffer[160] = {};
slmp::SlmpClient plc(transport, kProfile, slmp::TargetAddress{0x00, 0xFF, slmp::module_io::OwnStation, 0x00}, txBuffer, sizeof(txBuffer), rxBuffer, sizeof(rxBuffer));
void setup() {
Serial.begin(115200);
WiFi.begin(kWifiSsid, kWifiPassword);
while (WiFi.status() != WL_CONNECTED) {
delay(250);
}
plc.connect(kPlcHost, kTcpPort);
}
void loop() {
slmp::highlevel::Value value;
const slmp::Error err = slmp::highlevel::readTyped(plc, "D100:U", value);
if (err == slmp::Error::Ok) {
Serial.printf("D100=%u\n", static_cast<unsigned>(value.u16));
} else {
Serial.printf("readTyped failed: %s\n", slmp::errorString(err));
}
delay(1000);
}
Write a single value
slmp::highlevel::writeTyped writes one logical value. Run this only on a
controlled test PLC and a reserved test address. The example saves the original
value, attempts restoration after a confirmed write even if readback fails, and
reports an unknown write or restoration outcome for manual reconciliation. It
never retries or restores blindly after an outcome-unknown write.
#include <Arduino.h>
#include <WiFi.h>
#include <slmp_arduino_transport.h>
#include <slmp_high_level.h>
#include <slmp_minimal.h>
constexpr char kWifiSsid[] = "YOUR_WIFI_SSID";
constexpr char kWifiPassword[] = "YOUR_WIFI_PASSWORD";
constexpr char kPlcHost[] = "192.168.250.100";
constexpr uint16_t kTcpPort = 1025;
constexpr auto kProfile = slmp::highlevel::PlcProfile::IqR;
WiFiClient tcp;
slmp::ArduinoClientTransport transport(
tcp,
slmp::configureEsp32WifiClientKeepAlive,
slmp::writeEsp32WifiClientNonBlocking);
uint8_t txBuffer[160] = {};
uint8_t rxBuffer[160] = {};
slmp::SlmpClient plc(transport, kProfile, slmp::TargetAddress{0x00, 0xFF, slmp::module_io::OwnStation, 0x00}, txBuffer, sizeof(txBuffer), rxBuffer, sizeof(rxBuffer));
bool writeAttempted = false;
void setup() {
Serial.begin(115200);
WiFi.begin(kWifiSsid, kWifiPassword);
while (WiFi.status() != WL_CONNECTED) {
delay(250);
}
plc.connect(kPlcHost, kTcpPort);
}
void loop() {
if (!writeAttempted) {
writeAttempted = true;
slmp::highlevel::Value original;
const slmp::Error originalReadErr =
slmp::highlevel::readTyped(plc, "D9000:U", original);
if (originalReadErr != slmp::Error::Ok) {
Serial.printf("original read failed: %s\n", slmp::errorString(originalReadErr));
} else {
const slmp::Error writeErr = slmp::highlevel::writeTyped(
plc,
"D9000:U",
slmp::highlevel::Value::u16Value(321U));
Serial.printf("write D9000: %s\n", slmp::errorString(writeErr));
if (writeErr == slmp::Error::Ok) {
slmp::highlevel::Value readback;
const slmp::Error readbackErr =
slmp::highlevel::readTyped(plc, "D9000:U", readback);
const slmp::Error restoreErr =
slmp::highlevel::writeTyped(plc, "D9000:U", original);
if (restoreErr == slmp::Error::OutcomeUnknown) {
Serial.println("restore outcome unknown; inspect D9000 manually");
} else if (restoreErr != slmp::Error::Ok) {
Serial.println("restore failed; inspect and reconcile D9000 manually");
}
if (readbackErr != slmp::Error::Ok) {
Serial.printf("readback failed: %s\n", slmp::errorString(readbackErr));
}
} else if (writeErr == slmp::Error::OutcomeUnknown) {
Serial.println("write outcome unknown; do not retry or restore blindly");
}
}
}
delay(1000);
}
Named snapshot
slmp::highlevel::readNamed reads mixed addresses in caller order using one
random-read request. Plans containing fallback or long-timer routes are
rejected during plan compilation, before transport. Use readTyped or the
dedicated long-timer helpers for those route-specific scalar reads. LCN
current values use readTyped/Random DWord, while LCS and LCC states use
readTyped/Direct bit access. slmp::highlevel::writeNamed sends one random
word/DWord request or one random-bit request; mixed families and bit-in-word
read-modify-write are rejected.
Executing a hand-built ReadPlan verifies that every entry appears in the
corresponding word or DWord batch. A missing batch key is an error; the library
does not invent a zero value for a device that was not read.
The write portion below is for a controlled test PLC and reserved test devices
only. It saves the original values and restores them after a confirmed write.
If either state-changing request returns OutcomeUnknown, it stops without an
automatic retry; reconnect and reconcile the affected devices manually.
#include <Arduino.h>
#include <WiFi.h>
#include <string>
#include <vector>
#include <slmp_arduino_transport.h>
#include <slmp_high_level.h>
#include <slmp_minimal.h>
constexpr char kWifiSsid[] = "YOUR_WIFI_SSID";
constexpr char kWifiPassword[] = "YOUR_WIFI_PASSWORD";
constexpr char kPlcHost[] = "192.168.250.100";
constexpr uint16_t kTcpPort = 1025;
constexpr auto kProfile = slmp::highlevel::PlcProfile::IqR;
WiFiClient tcp;
slmp::ArduinoClientTransport transport(
tcp,
slmp::configureEsp32WifiClientKeepAlive,
slmp::writeEsp32WifiClientNonBlocking);
uint8_t txBuffer[192] = {};
uint8_t rxBuffer[192] = {};
slmp::SlmpClient plc(transport, kProfile, slmp::TargetAddress{0x00, 0xFF, slmp::module_io::OwnStation, 0x00}, txBuffer, sizeof(txBuffer), rxBuffer, sizeof(rxBuffer));
bool writeAttempted = false;
void setup() {
Serial.begin(115200);
WiFi.begin(kWifiSsid, kWifiPassword);
while (WiFi.status() != WL_CONNECTED) {
delay(250);
}
plc.connect(kPlcHost, kTcpPort);
}
void loop() {
if (!writeAttempted) {
writeAttempted = true;
const std::vector<std::string> writeAddresses = {
"D9000:U", "D9002:S", "D9004:L", "D9008:F"
};
slmp::highlevel::Snapshot originalValues;
const slmp::Error originalReadErr =
slmp::highlevel::readNamed(plc, writeAddresses, originalValues);
slmp::highlevel::Snapshot updates = {
{"D9000:U", slmp::highlevel::Value::u16Value(100U)},
{"D9002:S", slmp::highlevel::Value::s16Value(-10)},
{"D9004:L", slmp::highlevel::Value::s32Value(-123456)},
{"D9008:F", slmp::highlevel::Value::float32Value(12.5f)}
};
if (originalReadErr != slmp::Error::Ok) {
Serial.printf("original read failed: %s\n", slmp::errorString(originalReadErr));
} else {
const slmp::Error writeErr = slmp::highlevel::writeNamed(plc, updates);
Serial.printf("writeNamed: %s\n", slmp::errorString(writeErr));
if (writeErr == slmp::Error::Ok) {
const slmp::Error restoreErr =
slmp::highlevel::writeNamed(plc, originalValues);
Serial.printf("restore: %s\n", slmp::errorString(restoreErr));
if (restoreErr == slmp::Error::OutcomeUnknown) {
Serial.println("restore outcome unknown; inspect devices manually");
} else if (restoreErr != slmp::Error::Ok) {
Serial.println("restore failed; inspect and reconcile devices manually");
}
} else if (writeErr == slmp::Error::OutcomeUnknown) {
Serial.println("write outcome unknown; do not retry or restore blindly");
}
}
}
const std::vector<std::string> addresses = {
"SM400:BIT",
"D100:U",
"D101:S",
"D200:F",
"D50.3"
};
slmp::highlevel::Snapshot snapshot;
const slmp::Error readErr = slmp::highlevel::readNamed(plc, addresses, snapshot);
if (readErr == slmp::Error::Ok && snapshot.size() == addresses.size()) {
Serial.printf(
"SM400:BIT=%u D100:U=%u D101:S=%d D200:F=%.3f D50.3=%u\n",
snapshot[0].value.bit ? 1U : 0U,
static_cast<unsigned>(snapshot[1].value.u16),
static_cast<int>(snapshot[2].value.s16),
static_cast<double>(snapshot[3].value.f32),
snapshot[4].value.bit ? 1U : 0U);
}
delay(1000);
}
Block reads
Contiguous low-level reads are limited to one protocol request. Requests above the point limit fail; the application must explicitly issue and label multiple requests when different acquisition times are acceptable.
Polling
slmp::highlevel::Poller stores one compiled slmp::highlevel::ReadPlan, one
validated Random Read payload, compact result indexes, and reusable value
storage. Repeated reads therefore do not re-parse addresses, revalidate device
spans, rebuild indexes, or re-encode device specifications. Each cycle still
uses the current client serial, monitoring timer, timeout and lifecycle, and
validates the complete response. The first cycle binds the Poller to that exact
client and its profile/frame/compatibility; use another Poller for another
client or configuration. Explicit close/reconnect of the same unchanged client
does not invalidate the prepared plan. The bound client must outlive the Poller;
destroying it invalidates the binding even if another client is constructed at
the same memory address.
#include <Arduino.h>
#include <WiFi.h>
#include <string>
#include <vector>
#include <slmp_arduino_transport.h>
#include <slmp_high_level.h>
#include <slmp_minimal.h>
constexpr char kWifiSsid[] = "YOUR_WIFI_SSID";
constexpr char kWifiPassword[] = "YOUR_WIFI_PASSWORD";
constexpr char kPlcHost[] = "192.168.250.100";
constexpr uint16_t kTcpPort = 1025;
constexpr auto kProfile = slmp::highlevel::PlcProfile::IqR;
WiFiClient tcp;
slmp::ArduinoClientTransport transport(
tcp,
slmp::configureEsp32WifiClientKeepAlive,
slmp::writeEsp32WifiClientNonBlocking);
uint8_t txBuffer[192] = {};
uint8_t rxBuffer[192] = {};
slmp::SlmpClient plc(transport, kProfile, slmp::TargetAddress{0x00, 0xFF, slmp::module_io::OwnStation, 0x00}, txBuffer, sizeof(txBuffer), rxBuffer, sizeof(rxBuffer));
slmp::highlevel::Poller poller;
slmp::highlevel::Snapshot snapshot;
void setup() {
Serial.begin(115200);
WiFi.begin(kWifiSsid, kWifiPassword);
while (WiFi.status() != WL_CONNECTED) {
delay(250);
}
plc.connect(kPlcHost, kTcpPort);
poller.compile({"D100:U", "D101:S", "D200:F", "M1000:BIT"}, kProfile);
}
void loop() {
const slmp::Error err = poller.readOnce(plc, snapshot);
Serial.printf("poller: %s values=%u\n", slmp::errorString(err), static_cast<unsigned>(snapshot.size()));
delay(1000);
}
Device range catalog
slmp::highlevel::readDeviceRangeCatalogForPlcProfile reads live device range bounds while asserting one explicit profile. The requested profile must exactly equal SlmpClient::plcProfile(), including unit-specific identities; sharing a base family is not enough. A mismatch returns InvalidArgument before any transport write. The API does not auto-discover the PLC profile.
The catalog is for diagnostics and application-layer validation. Normal read/write helpers do not use it to reject addresses by configured upper bound before sending a request.
The source rules for this catalog are maintained in the shared SLMP device ranges reference.
For profiles requiring live Z/ZR range probes, this function is an explicit
read-only aggregate. It validates and snapshots the complete profile, rule, and
bounded probe decision plan before its first request, then owns one synchronous
client turn. The catalog keeps the declared device-row order but is non-atomic:
PLC state can change between probe requests. Expected PLC NG responses delimit
a readable range. Timeout, transport, protocol, lifecycle, or local-validation
failure stops immediately and leaves the caller's catalog unchanged; partial
entries are never published.
#include <Arduino.h>
#include <WiFi.h>
#include <slmp_arduino_transport.h>
#include <slmp_high_level.h>
#include <slmp_minimal.h>
constexpr char kWifiSsid[] = "YOUR_WIFI_SSID";
constexpr char kWifiPassword[] = "YOUR_WIFI_PASSWORD";
constexpr char kPlcHost[] = "192.168.250.100";
constexpr uint16_t kTcpPort = 1025;
constexpr auto kProfile = slmp::highlevel::PlcProfile::IqR;
WiFiClient tcp;
slmp::ArduinoClientTransport transport(
tcp,
slmp::configureEsp32WifiClientKeepAlive,
slmp::writeEsp32WifiClientNonBlocking);
uint8_t txBuffer[192] = {};
uint8_t rxBuffer[192] = {};
slmp::SlmpClient plc(transport, kProfile, slmp::TargetAddress{0x00, 0xFF, slmp::module_io::OwnStation, 0x00}, txBuffer, sizeof(txBuffer), rxBuffer, sizeof(rxBuffer));
bool printedCatalog = false;
void setup() {
Serial.begin(115200);
WiFi.begin(kWifiSsid, kWifiPassword);
while (WiFi.status() != WL_CONNECTED) {
delay(250);
}
plc.connect(kPlcHost, kTcpPort);
}
void loop() {
if (!printedCatalog) {
slmp::highlevel::DeviceRangeCatalog catalog;
const slmp::Error err = slmp::highlevel::readDeviceRangeCatalogForPlcProfile(plc, kProfile, catalog);
if (err == slmp::Error::Ok && !catalog.entries.empty()) {
const slmp::highlevel::DeviceRangeEntry& entry = catalog.entries.front();
Serial.printf(
"%s supported=%u range=%s\n",
entry.device.c_str(),
entry.supported ? 1U : 0U,
entry.address_range.c_str());
} else {
Serial.printf("catalog failed: %s\n", slmp::errorString(err));
}
printedCatalog = true;
}
delay(1000);
}
Long device families
LTN, LSTN, LCN, and LZ are 32-bit families in the high-level API. Use slmp::highlevel::ValueType::U32 or slmp::highlevel::ValueType::S32.
LTN and LSTN use route-specific Direct helpers and are not valid inputs to
readNamed, compileReadPlan, or Poller. LCN and LZ use canonical Random
DWord access and remain valid in a named plan. A named plan must be
representable as exactly one canonical Random Read request.
compileReadPlan applies the selected profile's canonical/default
compatibility-mode limit. readNamed also revalidates the connected client's
effective compatibility mode before transport; a manual compatibility override
can therefore reject a plan that compiled for the profile default.
| Family | Unsigned form | Signed form | Caution |
|---|---|---|---|
LTN |
LTN0:D |
LTN0:L |
Plain 16-bit access yields wrong data. |
LSTN |
LSTN0:D |
LSTN0:L |
Plain 16-bit access yields wrong data. |
LCN |
LCN0:D |
LCN0:L |
Plain 16-bit access yields wrong data. |
LZ |
LZ0:D |
LZ0:L |
Plain 16-bit access yields wrong data. |
Address reference table
| Form | Meaning | Example |
|---|---|---|
:U |
Unsigned 16-bit word. | D100:U |
:S |
Signed 16-bit word. | D100:S |
:D |
Unsigned 32-bit value from two words. | D200:D |
:L |
Signed 32-bit value from two words. | D200:L |
:F |
IEEE-754 float32 from two words. | D300:F |
:BIT |
Direct bit device value. | M1000:BIT |
.n |
One bit inside a word device, where n is hexadecimal 0 through F. |
D50.3 |
Named addresses used with readTyped(address), readNamed, writeNamed, and Poller must include the intended type, for example D100:U or M1000:BIT.
The module-buffer route names only word-addressable G and HG. Use
readWordsModuleBuf and writeWordsModuleBuf; there is no module-buffer bit
API. Qualified G/HG entries are likewise invalid in Extended Random bit
writes.
Label data lengths
Array label unit_specification is 0 for a logical bit count and 1 for a
logical byte count. Both forms occupy whole two-byte wire units: bit counts use
ceil(array_data_length / 16) * 2 bytes and byte counts use
ceil(array_data_length / 2) * 2 bytes. The logical length must be positive,
and writeArrayLabels requires the exact padded data_bytes. Random label read
and write lengths must also be positive and even. Read responses must match the
requested count and, for array labels, each requested unit and logical length;
malformed or trailing data returns Error::ProtocolError.
Request payload limits
One SLMP request can carry at most 65,529 command-payload bytes over a stream transport. IPv4 UDP must also fit one complete datagram, so the Arduino UDP command-payload maximum is 65,492 bytes for 3E and 65,488 bytes for 4E. Array and random label requests use even-sized payloads and therefore have a largest protocol-representable payload of 65,528 bytes before the lower UDP limit is applied.
Oversized requests return Error::InvalidArgument before send, request-frame publication, or 4E
serial allocation and are never truncated or split automatically. Every custom transport must
implement ITransport::maximumRequestFrameSize(): datagram transports return their complete-frame
ceiling, while stream transports return static_cast<size_t>(-1) because the SLMP field limit still
applies.
All low-level request methods and named operations are single-request APIs and
never aggregate or split work. An application that intentionally issues
multiple requests owns their order, coherence, errors, and any
OutcomeUnknown result. writeBitInWord explicitly performs a read followed
by a write for a direct word. writeBitInWordModuleBuf and
writeBitInWordLinkDirect provide the same contract for immutable U-qualified
module-buffer and J-qualified link-direct word routes. Address-form writeTyped uses that same two-request behavior when
its address contains .bit; its other address forms remain single-request.
Every bit-in-word entry prevalidates both requests, owns one local client turn,
and uses one absolute deadline after admission. A successful read is always
followed by its write, even when the bit is unchanged. The pair is non-atomic
with respect to PLC logic or another controller, never retries automatically,
and a possibly transmitted unconfirmed write returns OutcomeUnknown.
Traffic statistics
Call client.trafficStats() for a TrafficStats client-lifetime snapshot containing
request_count, tx_bytes, and rx_bytes. Complete sends and complete received frames are
counted; close and reconnect do not reset the snapshot.