Production-oriented .NET communication library for Siemens S7-1200/1500 PLCs using S7CommPlus over TLS, with optional legacy challenge authentication for pre-V17/pre-TLS CPUs on net8.0 and later.
The public API for new applications is S7CommPlusClient. Low-level protocol/session types are internal implementation details; production code should use the client surface.
- Async connect, disconnect, browse, read, write, active-alarm, subscription, and legitimation methods
- Serialized request execution for safe concurrent callers
- Typed exceptions with PLC endpoint, operation, error code, and transient/non-transient classification
- Connection-state and communication-error events
- Read/browse reconnect retry support
- Explicit write enablement so accidental PLC writes are blocked by default
- Bounded disconnect behavior and better socket disconnect detection
- TLS and legacy S7CommPlus challenge authentication modes
- No library writes to
Console
The default mode supports CPUs and projects that allow secure PG/HMI communication over TLS:
- S7-1200 firmware V4.3 or newer, TLS 1.3 from V4.5
- S7-1500 firmware V2.9 or newer
- Software controllers supported by the upstream protocol implementation
The PLC project must also be configured with a TIA Portal version that supports secure communication, typically TIA Portal V17 or newer.
For older S7-1200/1500 CPUs that do not support TLS, use S7CommPlusSecurityMode.LegacyChallenge or Auto on net8.0/net9.0. Legacy mode uses HarpoS7-derived challenge authentication and packet digests. net6.0 builds remain TLS-only and fail fast if legacy mode is requested.
TLS communication uses the managed BouncyCastle backend by default. The older OpenSSL backend remains available through S7CommPlusClientOptions.TlsBackend = S7CommPlusTlsBackend.OpenSsl, but it depends on native runtime files and may be less portable across PLC firmware/OpenSSL combinations.
The package includes the required native OpenSSL runtime files for supported platforms and copies them to the output directory when the OpenSSL backend is selected. If OpenSSL is installed system-wide, make sure the matching native binaries are available on the process path.
Windows runtime files include:
libcrypto-3.dll/libssl-3.dllfor x86libcrypto-3-x64.dll/libssl-3-x64.dllfor x64libcrypto-3-arm64.dll/libssl-3-arm64.dllplusvcruntime140.dllfor ARM64
Create one S7CommPlusClient per PLC endpoint and reuse it for reads. The client serializes operations internally, so multiple callers can safely share the same instance.
await using var client = new S7CommPlusClient(new S7CommPlusClientOptions
{
Address = "10.0.110.120",
RequestTimeout = TimeSpan.FromSeconds(5),
AutoReconnect = true
});
client.ConnectionStateChanged += (_, e) =>
{
Console.WriteLine($"{e.OldState} -> {e.NewState}");
};
client.CommunicationError += (_, e) =>
{
Console.WriteLine($"{e.Exception.Operation}: 0x{e.Exception.ErrorCode:X8}");
};
await client.ConnectAsync();
var cpuInfo = await client.GetCpuInfoAsync();
var cpuCulture = await client.GetCpuCultureInfoAsync();
var variables = await client.BrowseAsync();
var tag = await client.GetTagBySymbolAsync("MyDb.MyValue");
var read = await client.ReadAsync(new[] { tag });
if (read.Items[0].IsSuccess)
{
Console.WriteLine(tag.ToString());
}
await client.DisconnectAsync();GetCpuCultureInfoAsync() reads the CPU text-container LCIDs and exposes both
the raw language IDs and resolved .NET cultures:
foreach (var culture in cpuCulture.Cultures)
{
Console.WriteLine($"{culture.LCID}: {culture.Name}");
}The built-in connection defaults match Siemens S7CommPlus HMI communication:
ISO-on-TCP port S7CommPlusDefaults.IsoTcpPort (102), local TSAP
S7CommPlusDefaults.LocalTsap (0x0600), and remote TSAP
S7CommPlusDefaults.RemoteTsapHmi (SIMATIC-ROOT-HMI). Project/engineering
captures sometimes use S7CommPlusDefaults.RemoteTsapEs
(SIMATIC-ROOT-ES). Remote TSAP values are validated as ASCII COTP
parameters before a socket is opened.
Initial session authentication and PLC password legitimation are separate
steps. If the PLC requires a password for the desired access level, either pass
credentials in the options so they are used during connect, or call
LegitimateAsync after connecting:
await using var client = new S7CommPlusClient(new S7CommPlusClientOptions
{
Address = "10.0.110.120",
Password = "plc-password"
});
await client.ConnectAsync();
// Or, for an already connected session:
await client.LegitimateAsync("plc-password", username: "");Legitimation is a session-security operation, not a PLC signal write, so it
does not require WriteEnabled = true. Failed legitimation attempts throw
S7CommPlusLegitimationException.
Active alarms are exposed through the same serialized read pipeline and can therefore use the normal timeout, reconnect, logging, and typed-error behavior:
var alarms = await client.GetActiveAlarmsAsync(languageId: 1033);Use this call to get alarms that are already active when your application connects. Alarm subscriptions report new notification frames on that session; they should not be used as the only source for pre-existing alarms.
PLC communication limits are available through the production client. This is useful before creating subscriptions or planning large batch reads:
var resources = await client.GetCommunicationResourcesAsync();
Console.WriteLine(resources.TagsPerReadRequestMax);
Console.WriteLine(resources.PlcSubscriptionsFree);Subscriptions are exposed as long-running, disposable objects. They own the client operation pipeline while active because notification frames arrive on the same PLC session as normal request/response traffic. Stop or dispose a subscription before issuing other reads or writes on the same client.
var tag = await client.GetTagBySymbolAsync("MyDb.MyValue");
await using var subscription = await client.SubscribeTagsAsync(
new[] { tag },
new S7CommPlusSubscriptionOptions
{
CycleTimeMilliseconds = 250,
NotificationTimeout = TimeSpan.FromSeconds(5)
});
subscription.NotificationReceived += (_, e) =>
{
foreach (var item in e.Notification.Items)
{
if (item.IsSuccess)
{
Console.WriteLine(item.Tag);
}
}
};
subscription.CommunicationError += (_, e) =>
{
Console.WriteLine($"{e.Exception.Operation}: 0x{e.Exception.ErrorCode:X8}");
};Alarm notifications use the same lifecycle and credit handling:
await using var alarmSession = await client.SubscribeAlarmsWithSnapshotAsync(languageId: 1033);
foreach (var activeAlarm in alarmSession.ActiveAlarms)
{
Console.WriteLine(activeAlarm.AlarmTexts?.AlarmText);
}
alarmSession.Subscription.NotificationReceived += (_, e) =>
{
foreach (var alarm in e.Notification.Alarms)
{
Console.WriteLine(alarm.ToString());
}
};Do not poll or read tags on the same S7CommPlusClient while a subscription is
running. S7CommPlus notification frames share the same encrypted session and the
client intentionally serializes access to avoid overlapping sequence numbers and
TLS record failures. Use a second client connection for independent polling.
SubscribeAlarmsWithSnapshotAsync creates that second connection automatically.
It creates the alarm subscription first, opens a temporary second
S7CommPlusClient with the same options to read active alarms, disposes the
temporary client, and buffers early notifications so there is no
read-then-subscribe blind spot. If you need custom connection ownership, use the
overload that accepts a separate snapshot client. Stopping an alarm subscription
closes the current PLC session; the next read or browse on the same client will
reconnect automatically when AutoReconnect is enabled.
Idle notification waits are not treated as failures by default. Set
MaxConsecutiveTimeoutsBeforeFault when a quiet subscription should become a
typed communication failure after a fixed number of empty waits. Write
protection is unchanged: subscriptions do not enable PLC signal writes.
TLS remains the default to avoid accidental security downgrades. Enable the older Siemens challenge authentication explicitly:
await using var client = new S7CommPlusClient(new S7CommPlusClientOptions
{
Address = "10.0.110.120",
SecurityMode = S7CommPlusSecurityMode.LegacyChallenge,
RequestTimeout = TimeSpan.FromSeconds(5)
});
await client.ConnectAsync();
Console.WriteLine(client.Options.NegotiatedSecurityMode);S7CommPlusSecurityMode.Auto tries TLS first and then falls back to legacy challenge authentication. Reconnect and write safety behave the same in all modes: read/browse may reconnect once, writes are never retried automatically, and writes still require WriteEnabled = true.
The HarpoS7 1.1.0 NuGet package currently declares unpublished dependency package IDs, so this repository references the required HarpoS7 source projects directly under src/HarpoS7. HarpoS7 source is MIT licensed; this project remains LGPL-3.0-or-later unless noted otherwise.
Legacy packet-capture notes, auth frame variants, and live PLC observations are tracked in docs/legacy-s7commplus-memory.md.
Recent legacy auth work parses ServerSessionVersion from the CreateObject
response where available, so S7-1500 V3.x authentication-frame selection is
response-driven before falling back to capture-compatible heuristics.
Writes are disabled by default. This is intentional for production services and tests.
await using var client = new S7CommPlusClient(new S7CommPlusClientOptions
{
Address = "10.0.110.120",
WriteEnabled = true
});
await client.ConnectAsync();
await client.WriteAsync(new[] { tag });If WriteEnabled is false, write calls throw S7CommPlusWriteDisabledException.
Operations throw S7CommPlusException subclasses instead of returning only integer error codes. The exception includes:
OperationEndpointErrorCodeIsTransient
Read and browse operations retry once after a transient communication failure when AutoReconnect is enabled. Writes are not retried automatically.
S7CommPlusClientOptions.Logger accepts an ILogger. If no logger is provided, NullLogger is used.
The library no longer writes diagnostics to Console. Lower-level diagnostics are written via System.Diagnostics.Trace.
TLS key logging for Wireshark analysis is still available through the low-level S7Client.WriteSslKeyToFile and S7Client.WriteSslKeyPath settings. Use this only in controlled diagnostic environments.
Run the normal build and unit tests:
dotnet build src\S7CommPlusDriver.slnx /nodeReuse:false
dotnet test src\S7CommPlusDriver.Tests\S7CommPlusDriver.Tests.csproj --no-restore /nodeReuse:falseThe live PLC smoke test is opt-in and read-only by default:
$env:S7COMMPLUS_LIVE_HOST = "10.0.110.120"
dotnet test src\S7CommPlusDriver.Tests\S7CommPlusDriver.Tests.csproj --filter LivePlcReadOnlySmokeTestBy default the live test only connects, reads CPU info, browses, and disconnects. To read explicit tags, provide semicolon-separated tag symbols:
$env:S7COMMPLUS_LIVE_TAGS = "MyDb.MyValue;OtherDb.Counter"To exercise legacy auth or auto fallback in the live test:
$env:S7COMMPLUS_LIVE_SECURITY_MODE = "LegacyChallenge" # or "Auto"Never use the live smoke test for writes.
Known tested targets include:
- S7 1211 firmware V4.5
- TIA PLCSIM V17 with NetToPLCSim
- TIA PLCSIM V18 with NetToPLCSim
- read-only smoke test against a real PLC at
10.0.110.120
The PlcTag classes convert PLC values into .NET-friendly types. Supported data types include scalar and array variants for common Siemens types such as Bool, Byte, Word, Int, DInt, Real, LReal, String, WString, Date, Date_And_Time, DTL, Time, LTime, pointer-like types, hardware identifiers, counters, and timers.
For exact mappings, see the implementations in src/S7CommPlusDriver/ClientApi/PlcTag.cs and src/S7CommPlusDriver/ClientApi/PlcTags.cs.
Unless otherwise noted, all source code is licensed under LGPL-3.0-or-later.
- Thomas Wiens - initial work - https://github.com/thomas-v2
- DotNetProjects contributors