You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@plc4x.apache.org by sr...@apache.org on 2026/09/25 08:17:34 UTC
(plc4x) branch develop updated: feat(plc4net): add .NET 8 API and SPI foundation (#2771)
This is an automated email from the ASF dual-hosted git repository.
sruehl pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/plc4x.git
The following commit(s) were added to refs/heads/develop by this push:
new 676de7ee3e feat(plc4net): add .NET 8 API and SPI foundation (#2771)
676de7ee3e is described below
commit 676de7ee3e0b8c70f96b04636d1a5e544b548a18
Author: TomNewChao <ch...@gmail.com>
AuthorDate: Fri Sep 25 16:17:29 2026 +0800
feat(plc4net): add .NET 8 API and SPI foundation (#2771)
* feat(plc4net): add .NET 8 API and SPI foundation
* fix(plc4net): resolve Copilot review findings on nullable and numeric safety
Consolidates three related fixes into a single commit:
1. Enable nullable analysis (Directory.Build.props) and correct nullable contracts
- Switches from 'annotations' to 'enable' for full null-safety checking
- Annotates feature-gated builders, transports, authentication as nullable
- Makes PlcValueAdapter reference-type accessors throw instead of returning null
2. Align DefaultDotNetType table to native .NET storage types
- WORD/DWORD/LWORD: short/int/long → ushort/uint/ulong (unsigned)
- USINT/UINT/UDINT: ushort/uint/ulong → byte/ushort/uint (unsigned)
- WCHAR: short → char (actual storage)
- DATE/LDATE: DateTime → DateOnly (actual storage)
- TIME_OF_DAY/LTIME_OF_DAY: TimeSpan → TimeOnly (actual storage)
3. Add numeric bounds validation for temporal fields
- PlcLTIME.GetDuration(): check nanoseconds overflow
- PlcDATE_AND_LTIME: validate nanosecondsOfSecond in [0, 999_999_999]
- PlcDATE_AND_LTIME.OfSegments(): validate nanoseconds parameter
- PlcLTIME_OF_DAY: guard against overflow on large values
Tests: 138/138 passing
Build: 0 errors, 0 warnings
---
.github/workflows/dotnet-platform.yml | 53 +++
plc4net/Directory.Build.props | 77 +++++
plc4net/PACKAGE.md | 40 +++
plc4net/api/IPlcConnection.cs | 59 ++++
plc4net/api/IPlcDriver.cs | 47 +++
plc4net/api/PlcDriverManager.cs | 151 ++++----
plc4net/api/{plc4net-api.csproj => api.csproj} | 24 +-
plc4net/api/api/IPlcConnection.cs | 89 -----
plc4net/api/api/IPlcDriver.cs | 57 ---
plc4net/api/api/model/IPlcField.cs | 39 ---
plc4net/api/api/types/PlcValueType.cs | 98 ------
.../{api => }/authentication/IPlcAuthentication.cs | 0
.../PlcUsernamePasswordAuthentication.cs | 0
.../{api => }/exceptions/PlcConnectionException.cs | 8 -
plc4net/api/{api => }/exceptions/PlcException.cs | 12 +-
.../PlcIncompatibleDatatypeException.cs} | 22 +-
.../exceptions/PlcInvalidFieldException.cs | 10 +-
.../extensions/Plc4NetServiceExtensions.cs} | 41 +--
plc4net/api/{api => }/messages/IPlcMessage.cs | 2 +-
plc4net/api/{api => }/messages/IPlcReadRequest.cs | 4 +-
.../{api => }/messages/IPlcReadRequestBuilder.cs | 10 +-
plc4net/api/{api => }/messages/IPlcReadResponse.cs | 0
plc4net/api/{api => }/messages/IPlcRequest.cs | 0
.../api/{api => }/messages/IPlcRequestBuilder.cs | 0
plc4net/api/{api => }/messages/IPlcResponse.cs | 2 +-
.../messages/IPlcSubscriptionEventArgs.cs | 0
.../{api => }/messages/IPlcSubscriptionRequest.cs | 2 +-
.../messages/IPlcSubscriptionRequestBuilder.cs | 2 +-
.../{api => }/messages/IPlcSubscriptionResponse.cs | 2 +-
.../IPlcTagRequest.cs} | 24 +-
.../IPlcTagResponse.cs} | 18 +-
.../messages/IPlcUnsubscriptionRequest.cs | 4 +-
.../messages/IPlcUnsubscriptionRequestBuilder.cs | 2 +-
.../messages/IPlcUnsubscriptionResponse.cs | 4 +-
plc4net/api/{api => }/messages/IPlcWriteRequest.cs | 2 +-
.../{api => }/messages/IPlcWriteRequestBuilder.cs | 72 ++--
.../IPlcWriteResponse.cs} | 5 +-
.../metadata/DefaultPlcConnectionMetadata.cs} | 32 +-
.../{api => }/metadata/IPlcConnectionMetadata.cs | 0
.../api/{api => }/model/IPlcSubscriptionHandle.cs | 0
.../values/PlcDINT.cs => api/model/IPlcTag.cs} | 17 +-
plc4net/api/{api => }/types/PlcResponseCode.cs | 9 +-
plc4net/api/types/PlcValueType.cs | 98 ++++++
plc4net/api/{api => }/value/IPlcValue.cs | 13 +-
plc4net/plc4net.sln | 103 ++++--
plc4net/pom.xml | 30 +-
plc4net/spi/drivers/ConnectionBase.cs | 148 ++++++++
plc4net/spi/drivers/ConnectionString.cs | 217 ++++++++++++
plc4net/spi/drivers/DriverBase.cs | 294 ++++++++++++++++
plc4net/spi/drivers/MessageCodecBase.cs | 206 +++++++++++
plc4net/spi/drivers/ScanExtensions.cs | 117 +++++++
.../drivers/functions/PlcReader.cs} | 15 +-
.../drivers/functions/PlcWriter.cs} | 15 +-
.../spi/drivers/messages/DefaultPlcReadRequest.cs | 61 ++++
.../messages/DefaultPlcReadRequestBuilder.cs | 68 ++++
.../spi/drivers/messages/DefaultPlcReadResponse.cs | 73 ++++
.../spi/drivers/messages/DefaultPlcWriteRequest.cs | 69 ++++
.../messages/DefaultPlcWriteRequestBuilder.cs | 143 ++++++++
.../drivers/messages/DefaultPlcWriteResponse.cs | 54 +++
.../messages/items/DefaultPlcResponseItem.cs} | 16 +-
.../messages/items/DefaultPlcTagErrorItem.cs} | 16 +-
.../drivers/messages/items/DefaultPlcTagItem.cs} | 16 +-
.../messages/items/DefaultPlcTagValueItem.cs} | 19 +-
.../drivers/messages/items/PlcResponseItem.cs} | 13 +-
.../drivers/messages/items/PlcTagItem.cs} | 13 +-
.../drivers/messages/items/PlcTagValueItem.cs} | 12 +-
.../drivers/tags/PlcTagHandler.cs} | 27 +-
plc4net/spi/generation/BitReader.cs | 146 ++++++++
plc4net/spi/generation/BitWriter.cs | 93 +++++
plc4net/spi/{spi => }/generation/ByteOrder.cs | 2 +-
.../spi/{spi => }/generation/EvaluationHelper.cs | 17 +-
.../ParseException.cs} | 29 +-
plc4net/spi/generation/ReadBuffer.cs | 222 ++++++++++++
plc4net/spi/generation/WriteBuffer.cs | 236 +++++++++++++
.../values/PlcWORD.cs => model/values/PlcBOOL.cs} | 66 ++--
.../values/PlcNULL.cs => model/values/PlcBYTE.cs} | 17 +-
plc4net/spi/model/values/PlcBitString.cs | 188 ++++++++++
plc4net/spi/{spi => }/model/values/PlcCHAR.cs | 18 +-
plc4net/spi/{spi => }/model/values/PlcDATE.cs | 45 ++-
plc4net/spi/model/values/PlcDATE_AND_LTIME.cs | 127 +++++++
.../spi/{spi => }/model/values/PlcDATE_AND_TIME.cs | 41 ++-
plc4net/spi/{spi => }/model/values/PlcDINT.cs | 0
.../values/PlcDWORD.cs} | 20 +-
plc4net/spi/{spi => }/model/values/PlcINT.cs | 0
plc4net/spi/{spi => }/model/values/PlcLINT.cs | 0
plc4net/spi/{spi => }/model/values/PlcLREAL.cs | 0
plc4net/spi/{spi => }/model/values/PlcLTIME.cs | 44 ++-
plc4net/spi/model/values/PlcLTIME_OF_DAY.cs | 89 +++++
.../values/PlcBOOL.cs => model/values/PlcLWORD.cs} | 20 +-
.../PlcPlcList.cs => model/values/PlcList.cs} | 43 ++-
.../values/PlcNULL.cs} | 40 ++-
plc4net/spi/{spi => }/model/values/PlcREAL.cs | 0
.../PlcBYTE.cs => model/values/PlcRawByteArray.cs} | 54 ++-
plc4net/spi/{spi => }/model/values/PlcSINT.cs | 0
plc4net/spi/{spi => }/model/values/PlcSTRING.cs | 18 +-
.../model/values/PlcSimpleNumericValueAdapter.cs | 253 ++++++++++++++
.../model/values/PlcSimpleValueAdapter.cs | 8 +-
plc4net/spi/model/values/PlcStruct.cs | 91 +++++
plc4net/spi/{spi => }/model/values/PlcTIME.cs | 24 +-
.../values/PlcTIME_OF_DAY.cs} | 39 ++-
plc4net/spi/{spi => }/model/values/PlcUDINT.cs | 0
plc4net/spi/{spi => }/model/values/PlcUINT.cs | 0
plc4net/spi/{spi => }/model/values/PlcULINT.cs | 0
plc4net/spi/{spi => }/model/values/PlcUSINT.cs | 0
plc4net/spi/model/values/PlcValueAdapter.cs | 281 +++++++++++++++
plc4net/spi/{spi => }/model/values/PlcWCHAR.cs | 18 +-
.../values/PlcNULL.cs => model/values/PlcWORD.cs} | 17 +-
plc4net/spi/{spi => }/model/values/PlcWSTRING.cs | 18 +-
plc4net/{api/plc4net-api.csproj => spi/spi.csproj} | 17 +-
plc4net/spi/spi/generation/ParseException.cs | 26 --
plc4net/spi/spi/generation/ReadBuffer.cs | 206 -----------
plc4net/spi/spi/generation/WriteBuffer.cs | 178 ----------
plc4net/spi/spi/model/values/PlcDWORD.cs | 90 -----
plc4net/spi/spi/model/values/PlcLWORD.cs | 106 ------
.../model/values/PlcSimpleNumericValueAdapter.cs | 212 ------------
plc4net/spi/spi/model/values/PlcStruct.cs | 54 ---
plc4net/spi/spi/model/values/PlcValueAdapter.cs | 242 -------------
plc4net/spi/transports/BaseTransportInstance.cs | 96 +++++
plc4net/spi/transports/ITransport.cs | 108 ++++++
plc4net/spi/transports/ITransportInstance.cs | 95 +++++
plc4net/spi/transports/RingBuffer.cs | 153 ++++++++
.../TransportException.cs} | 25 +-
.../test/spi-test/drivers/ConnectionStringTests.cs | 196 +++++++++++
plc4net/test/spi-test/drivers/DriverBaseTests.cs | 385 +++++++++++++++++++++
.../test/spi-test/drivers/MessageCodecBaseTests.cs | 176 ++++++++++
plc4net/test/spi-test/drivers/MessagesTests.cs | 126 +++++++
.../test/spi-test/drivers/PlcDriverManagerTests.cs | 128 +++++++
plc4net/test/spi-test/generation/BufferTests.cs | 230 ++++++++++++
.../test/spi-test/model/values/PlcValueTests.cs | 256 ++++++++++++++
.../spi-test/model/values/PlcValueTypeTests.cs | 113 ++++++
.../spi-test/spi-test.csproj} | 31 +-
.../test/spi-test/transports/RingBufferTests.cs | 162 +++++++++
.../test/spi-test/transports/TestTransportTests.cs | 143 ++++++++
plc4net/transports/test/TestTransport.cs | 72 ++++
.../test/TestTransportConfiguration.cs} | 36 +-
plc4net/transports/test/TestTransportInstance.cs | 238 +++++++++++++
.../test/test.csproj} | 30 +-
137 files changed, 7132 insertions(+), 2018 deletions(-)
diff --git a/.github/workflows/dotnet-platform.yml b/.github/workflows/dotnet-platform.yml
new file mode 100644
index 0000000000..a6d3906a26
--- /dev/null
+++ b/.github/workflows/dotnet-platform.yml
@@ -0,0 +1,53 @@
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# ----------------------------------------------------------------------------
+name: .NET Platform Compatibility
+
+on:
+ push:
+ branches: [develop, plc4net]
+ paths: [plc4net/**, .github/workflows/dotnet-platform.yml]
+ pull_request:
+ branches: [develop, plc4net]
+ paths: [plc4net/**, .github/workflows/dotnet-platform.yml]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ name: Build and test (${{ matrix.os }})
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, macos-latest, windows-latest]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - uses: actions/checkout@v7
+ - uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: 8.0.x
+ - name: Restore
+ run: dotnet restore plc4net/plc4net.sln
+ - name: Build
+ run: dotnet build plc4net/plc4net.sln --configuration Debug --no-restore --no-incremental
+ - name: Test
+ run: dotnet test plc4net/plc4net.sln --configuration Debug --no-build
+ - name: Check vulnerable dependencies
+ run: dotnet list plc4net/plc4net.sln package --vulnerable --include-transitive
diff --git a/plc4net/Directory.Build.props b/plc4net/Directory.Build.props
new file mode 100644
index 0000000000..87945b8953
--- /dev/null
+++ b/plc4net/Directory.Build.props
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+<Project>
+
+ <!--
+ Properties shared by every PLC4Net project. Keeping them here means a
+ framework or version bump is a one-line change instead of a sweep over
+ every .csproj.
+ -->
+ <PropertyGroup>
+ <!--
+ net8.0 (LTS) replaces the previous net452 target. .NET Framework 4.5.2
+ went out of support in April 2022 and cannot be built on the Linux and
+ macOS CI runners, which is what kept PLC4Net off the CI matrix.
+ -->
+ <TargetFramework>net8.0</TargetFramework>
+ <LangVersion>latest</LangVersion>
+ <!-- Full nullable analysis: warnings stay on so a false non-null contract
+ cannot ship. Public members whose documented contract allows null
+ (e.g. feature-gated request builders, unknown transport codes) must
+ annotate that with `?`. -->
+ <Nullable>enable</Nullable>
+ <!-- The module builds warning-clean today; keep it that way in CI so a
+ later change cannot quietly regress it. -->
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ <RootNamespace>org.apache.plc4net</RootNamespace>
+ </PropertyGroup>
+
+ <PropertyGroup>
+ <Authors>Apache PLC4X</Authors>
+ <Copyright>The Apache Software Foundation</Copyright>
+ <PackageProjectUrl>https://plc4x.apache.org</PackageProjectUrl>
+ <!-- PackageLicenseUrl is deprecated (NU5125); the SPDX expression replaces it. -->
+ <PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
+ <!-- Overridden by the Maven build via -p:Version=${project.version}. -->
+ <Version>1.1.0-SNAPSHOT</Version>
+ <!--
+ AssemblyVersion is pinned to MAJOR.0.0.0 so it stays stable across every
+ 1.x build; the NuGet package version and FileVersion still track -p:Version.
+ Without this a consumer that restored two packages built at different
+ -p:Version values gets a binding mismatch (driver-s7 refs plc4net-spi
+ 1.1.0.0, the packed plc4net-spi.dll is 0.0.1.0).
+ -->
+ <AssemblyVersion>1.0.0.0</AssemblyVersion>
+ <!-- Per-project <Title> stays specific; this is the fallback prose NuGet wants. -->
+ <Description>Apache PLC4X for .NET (plc4net): API, SPI, and transport foundations for industrial protocol drivers.</Description>
+ <PackageReadmeFile>PACKAGE.md</PackageReadmeFile>
+ <RepositoryUrl>https://github.com/apache/plc4x</RepositoryUrl>
+ <RepositoryType>git</RepositoryType>
+ <!-- Embed the PDB in the assembly so a symbol server is not needed and every
+ feed (GitHub Packages does not take .snupkg) carries usable symbols. -->
+ <DebugType>embedded</DebugType>
+ </PropertyGroup>
+
+ <!-- Ship the package readme in every packable project. -->
+ <ItemGroup Condition="'$(IsPackable)' != 'false'">
+ <None Include="$(MSBuildThisFileDirectory)PACKAGE.md" Pack="true" PackagePath="\" Visible="false" />
+ </ItemGroup>
+
+</Project>
diff --git a/plc4net/PACKAGE.md b/plc4net/PACKAGE.md
new file mode 100644
index 0000000000..a066055ac2
--- /dev/null
+++ b/plc4net/PACKAGE.md
@@ -0,0 +1,40 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+ -->
+# Apache PLC4X for .NET (plc4net)
+
+The .NET implementation of [Apache PLC4X](https://plc4x.apache.org). This
+package set currently provides the API, SPI runtime, and a loopback test
+transport. Protocol drivers and production transports are separate follow-up
+contributions.
+
+## Packages
+
+| Package | Contents |
+|---|---|
+| `plc4net-api` | connection / driver / value interfaces |
+| `plc4net-spi` | driver runtime, value model, buffers, `ConnectionString` |
+| `plc4net-transports-test` | in-memory loopback transport for tests |
+
+## Status
+
+This is the first buildable slice of the plc4net revival described in
+[apache/plc4x#2656](https://github.com/apache/plc4x/pull/2656). It does not
+yet include a production transport or a protocol driver. Pre-release builds only.
+
+Licensed under the Apache License 2.0.
diff --git a/plc4net/api/IPlcConnection.cs b/plc4net/api/IPlcConnection.cs
new file mode 100644
index 0000000000..0e19e3830b
--- /dev/null
+++ b/plc4net/api/IPlcConnection.cs
@@ -0,0 +1,59 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+using System;
+using org.apache.plc4net.api.authentication;
+using org.apache.plc4net.api.metadata;
+using org.apache.plc4net.messages;
+using org.apache.plc4net.model;
+
+namespace org.apache.plc4net.api
+{
+ /// <summary>
+ /// A live connection to a device.
+ /// </summary>
+ public interface IPlcConnection : IDisposable
+ {
+ bool IsConnected { get; }
+
+ void Close();
+
+ IPlcConnectionMetadata PlcConnectionMetadata { get; }
+
+ /// <summary>
+ /// Parses a protocol-specific tag string, e.g. "%DB1.DBB0:INT".
+ /// </summary>
+ IPlcTag Parse(string tagQuery);
+
+ /// <summary>null when reading is not supported.</summary>
+ IPlcReadRequestBuilder? ReadRequestBuilder { get; }
+
+ /// <summary>null when writing is not supported.</summary>
+ IPlcWriteRequestBuilder? WriteRequestBuilder { get; }
+
+ /// <summary>null when subscriptions are not supported.</summary>
+ IPlcSubscriptionRequestBuilder? SubscriptionRequestBuilder { get; }
+
+ /// <summary>null when subscriptions are not supported.</summary>
+ IPlcUnsubscriptionRequestBuilder? UnsubscriptionRequestBuilder { get; }
+
+ /// <summary>null when the connection was established without authentication.</summary>
+ IPlcAuthentication? Authentication { get; }
+ }
+}
\ No newline at end of file
diff --git a/plc4net/api/IPlcDriver.cs b/plc4net/api/IPlcDriver.cs
new file mode 100644
index 0000000000..48cfcf0ede
--- /dev/null
+++ b/plc4net/api/IPlcDriver.cs
@@ -0,0 +1,47 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+using org.apache.plc4net.api.authentication;
+using org.apache.plc4net.exceptions;
+
+namespace org.apache.plc4net.api
+{
+ /// <summary>
+ /// A PLC protocol driver.
+ /// </summary>
+ /// <remarks>
+ /// This is the SPI3-aligned contract: drivers are synchronous factories that
+ /// open connections. The transport selection and connection-string parsing
+ /// is handled by <see cref="org.apache.plc4net.spi.drivers.DriverBase"/>.
+ /// </remarks>
+ public interface IPlcDriver
+ {
+ string ProtocolCode { get; }
+
+ string ProtocolName { get; }
+
+ /// <summary>
+ /// Opens a connection to the device described by <paramref name="connectionString"/>.
+ /// </summary>
+ /// <exception cref="PlcConnectionException">on connection failure</exception>
+ IPlcConnection Connect(string connectionString);
+
+ IPlcConnection Connect(string connectionString, IPlcAuthentication? authentication);
+ }
+}
\ No newline at end of file
diff --git a/plc4net/api/PlcDriverManager.cs b/plc4net/api/PlcDriverManager.cs
index 294cbb1c1e..367c74ba8e 100644
--- a/plc4net/api/PlcDriverManager.cs
+++ b/plc4net/api/PlcDriverManager.cs
@@ -1,25 +1,25 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
-using System.Threading.Tasks;
using org.apache.plc4net.api;
using org.apache.plc4net.api.authentication;
using org.apache.plc4net.exceptions;
@@ -27,78 +27,89 @@ using org.apache.plc4net.exceptions;
namespace org.apache.plc4net
{
/// <summary>
- /// Manages connections to PLCs
+ /// Registry of PLC protocol drivers. A driver registers itself here;
+ /// connection requests are dispatched to the driver whose protocol code
+ /// matches the connection string scheme.
/// </summary>
+ /// <remarks>
+ /// All read and write paths through the driver dictionary are safe for
+ /// concurrent use so that a background initialisation task can register
+ /// a driver without racing against an incoming connection request.
+ /// </remarks>
public class PlcDriverManager
{
- /// <summary>
- /// Singleton instance of the manager
- /// </summary>
- private static PlcDriverManager _instance;
+ // Static initializer is thread-safe; the CLR guarantees it runs exactly once.
+ private static readonly PlcDriverManager _instance = new PlcDriverManager();
- /// <summary>
- /// Get the singleton instance
- /// </summary>
- public static PlcDriverManager Instance => _instance ?? (_instance = new PlcDriverManager());
+ // Protocol codes are matched case-insensitively, the same way
+ // DefaultTransportManager matches transport codes.
+ private readonly ConcurrentDictionary<string, IPlcDriver> _drivers
+ = new ConcurrentDictionary<string, IPlcDriver>(StringComparer.OrdinalIgnoreCase);
- /// <summary>
- /// Dictionary for the drivers
- /// </summary>
- private readonly Dictionary<string, IPlcDriver> _drivers;
-
- /// <summary>
- /// Private constructor for the singleton driver manager.
- /// </summary>
private PlcDriverManager()
{
- _drivers = new Dictionary<string, IPlcDriver>();
-
- /*
- * TODO: Implement some mechanism to provide drivers -> MEF?
- */
}
- /// <summary>
- /// Get the connection to the a PLC identified by the URL
- /// </summary>
- /// <param name="url">URL including the schema to connect to the PLC</param>
- /// <param name="authentication">Authentication to use</param>
- /// <returns>Created PLC connection</returns>
- public async Task<IPlcConnection> GetConnection(string url, IPlcAuthentication authentication)
+ public static PlcDriverManager Instance => _instance;
+
+ public void RegisterDriver(IPlcDriver driver)
{
- var plcDriver = GetDriver(url);
- var connection = await plcDriver.ConnectAsync(url, authentication);
-
- //TODO: Does the driver method already connect or is a separate connect needed?
- //TODO: Should we do it like this?
- if (!connection.IsConnected)
+ if (driver == null)
{
- await connection.ConnectAsync();
+ throw new ArgumentNullException(nameof(driver));
}
-
- return connection;
+ _drivers[driver.ProtocolCode] = driver;
}
public IPlcDriver GetDriver(string url)
{
- try
- {
- Uri plcUri = new Uri(url);
- var proto = plcUri.Scheme;
+ // Extract the protocol code from the connection string without relying
+ // on System.Uri, which may reject or misinterpret non-standard schemes
+ // like "s7:cotp". The pattern is always {protocol}(:{transport})?://...
+ if (string.IsNullOrWhiteSpace(url))
+ throw new PlcConnectionException("Connection string must not be null or empty.");
- _drivers.TryGetValue(proto, out var plcDriver);
+ var schemeEnd = url.IndexOf("://", StringComparison.Ordinal);
+ if (schemeEnd < 0)
+ throw new PlcConnectionException(
+ $"Invalid connection string '{url}': no :// separator found.");
- if (plcDriver == null)
- {
- throw new PlcConnectionException($"Unknown driver for protocol '{proto}'");
- }
+ var scheme = schemeEnd > 0 ? url.Substring(0, schemeEnd) : string.Empty;
+ if (scheme.Length == 0)
+ throw new PlcConnectionException(
+ $"Invalid connection string '{url}': scheme is empty.");
- return plcDriver;
- }
- catch (UriFormatException invalidUriException)
+ // For two-scheme forms like s7:cotp://host, take the protocol
+ // portion before the colon (s7), matching what Java's
+ // URI.getScheme() returns for the same input.
+ var colon = scheme.IndexOf(':');
+ if (colon >= 0)
+ scheme = scheme.Substring(0, colon);
+
+ return GetDriverByCode(scheme);
+ }
+
+ public IPlcDriver GetDriverByCode(string? protocolCode)
+ {
+ if (protocolCode != null
+ && _drivers.TryGetValue(protocolCode, out var driver)
+ && driver != null)
{
- throw new PlcConnectionException($"Provided connection string '{url}' is invalid", invalidUriException);
+ return driver;
}
+ throw new PlcConnectionException(
+ $"No driver registered for protocol '{protocolCode}'.");
+ }
+
+ public IPlcConnection GetConnection(string connectionString)
+ {
+ return GetConnection(connectionString, null);
+ }
+
+ public IPlcConnection GetConnection(string connectionString, IPlcAuthentication? authentication)
+ {
+ var driver = GetDriver(connectionString);
+ return driver.Connect(connectionString, authentication);
}
}
}
\ No newline at end of file
diff --git a/plc4net/api/plc4net-api.csproj b/plc4net/api/api.csproj
similarity index 57%
copy from plc4net/api/plc4net-api.csproj
copy to plc4net/api/api.csproj
index ee7f65ff08..43d813fa51 100644
--- a/plc4net/api/plc4net-api.csproj
+++ b/plc4net/api/api.csproj
@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="utf-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
@@ -18,18 +18,12 @@
under the License.
-->
<Project Sdk="Microsoft.NET.Sdk">
-
- <PropertyGroup>
- <TargetFramework>net452</TargetFramework>
- <RootNamespace>org.apache.plc4net</RootNamespace>
- <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
- <Authors>Apache PLC4X</Authors>
- <Copyright>The Apache Software Foundation</Copyright>
- <PackageProjectUrl>https://plc4x.apache.org</PackageProjectUrl>
- <Title>PLC4Net: API</Title>
- <PackageLicenseUrl>https://www.apache.org/licenses/LICENSE-2.0</PackageLicenseUrl>
- <PackageVersion>0.8.0-SNAPSHOT</PackageVersion>
- <LangVersion>11.0</LangVersion>
+ <PropertyGroup>
+ <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
+ <AssemblyName>plc4net-api</AssemblyName>
+ <Title>PLC4Net: API</Title>
</PropertyGroup>
-
-</Project>
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
+ </ItemGroup>
+</Project>
\ No newline at end of file
diff --git a/plc4net/api/api/IPlcConnection.cs b/plc4net/api/api/IPlcConnection.cs
deleted file mode 100644
index cbbacf86df..0000000000
--- a/plc4net/api/api/IPlcConnection.cs
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-using System;
-using System.Threading.Tasks;
-using org.apache.plc4net.api.metadata;
-using org.apache.plc4net.messages;
-using org.apache.plc4net.model;
-
-namespace org.apache.plc4net.api
-{
- /// <summary>
- /// Interface for generalized PLC connections providing
- /// functionality for basic operations like connect / disconnect etc.
- /// </summary>
- public interface IPlcConnection: IDisposable
- {
- /// <summary>
- /// Connect to the PLC asynchronously
- /// </summary>
- /// <returns>Awaitable task</returns>
- /// <exception cref="org.apache.plc4net.exceptions.PlcConnectionException">Thrown if the connection to the PLC fails</exception>
- Task ConnectAsync();
-
- /// <summary>
- /// Indicates the connection state
- /// </summary>
- bool IsConnected { get; }
-
- /// <summary>
- /// Close the PLC connection asynchronously
- /// </summary>
- /// <returns>Awaitable task</returns>
- Task CloseAsync();
-
- /// <summary>
- /// Get the metadata for the connection
- /// </summary>
- IPlcConnectionMetadata PlcConnectionMetadata { get; }
-
- /// <summary>
- /// Parse the given field query
- /// </summary>
- /// <param name="fieldQuery">Query for the field</param>
- /// <returns>Field parsed from the query string</returns>
- /// <exception cref="org.apache.plc4net.exceptions.PlcInvalidFieldException">Thrown when the query can not be parsed</exception>
- IPlcField Parse(string fieldQuery);
-
- /// <summary>
- /// Request builder for constructing read requests
- /// </summary>
- /// <returns>null if the connection does not support reading</returns>
- IPlcReadRequestBuilder ReadRequestBuilder { get; }
-
- /// <summary>
- /// Request builder for constructing write requests
- /// </summary>
- /// <returns>null if the connection does not support writing</returns>
- IPlcWriteRequestBuilder WriteRequestBuilder { get; }
-
- /// <summary>
- /// Request builder for constructing subscription requests
- /// </summary>
- /// <returns>null if the connection does not support subscriptions</returns>
- IPlcSubscriptionRequestBuilder SubscriptionRequestBuilder { get; }
-
- /// <summary>
- /// Request builder for unsubscribing
- /// </summary>
- /// <returns>null if the connection does not support subscriptions</returns>
- IPlcUnsubscriptionRequestBuilder UnsubscriptionRequestBuilder { get; }
- }
-}
\ No newline at end of file
diff --git a/plc4net/api/api/IPlcDriver.cs b/plc4net/api/api/IPlcDriver.cs
deleted file mode 100644
index aef20bdd21..0000000000
--- a/plc4net/api/api/IPlcDriver.cs
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-using System.Threading.Tasks;
-using org.apache.plc4net.api.authentication;
-
-namespace org.apache.plc4net.api
-{
- /// <summary>
- /// Interface for PLC drivers to be implemented
- /// </summary>
- public interface IPlcDriver
- {
- /// <summary>
- /// Get the code of the implemented protocol
- /// </summary>
- string ProtocolCode { get; }
-
- /// <summary>
- /// Full Name of the implemented protocol
- /// </summary>
- string ProtocolName { get; }
-
- /// <summary>
- /// Connects to the PLC identified by the connection string
- /// </summary>
- /// <param name="connectionString">Connection string identifying the PLC to connect to</param>
- /// <returns>Awaitable task returning the <see cref="IPlcConnection"/> to which the connection was established</returns>
- /// <exception cref="org.apache.plc4net.exceptions.PlcConnectionException">Thrown on connection failure</exception>
- Task<IPlcConnection> ConnectAsync(string connectionString);
-
- /// <summary>
- /// Connects to the PLC identified by the connection string and using the
- /// </summary>
- /// <param name="connectionString"></param>
- /// <param name="authentication"></param>
- /// <returns>Awaitable task returning the <see cref="IPlcConnection"/> to which the connection was established</returns>
- /// <exception cref="org.apache.plc4net.exceptions.PlcConnectionException">Thrown on connection failure</exception>
- Task<IPlcConnection> ConnectAsync(string connectionString, IPlcAuthentication authentication);
- }
-}
\ No newline at end of file
diff --git a/plc4net/api/api/model/IPlcField.cs b/plc4net/api/api/model/IPlcField.cs
deleted file mode 100644
index e880a38eda..0000000000
--- a/plc4net/api/api/model/IPlcField.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-
-namespace org.apache.plc4net.model
-{
- /// <summary>
- /// Base type for all field types.
- /// Typically every driver provides an implementation of this interface in order
- /// to be able to describe the fields of a resource.As this is completely tied to
- /// the implemented protocol, this base interface makes absolutely no assumption to
- /// any information it should provide.
-
- /// In order to stay platform and protocol independent every driver connection implementation
- /// provides a prepareField(String) method that is able to parse a string representation of
- /// a resource into it's individual field type. Manually constructing PlcField objects
- /// manually makes the solution less independent from the protocol, but might be faster.
- /// </summary>
- public interface IPlcField
- {
-
- }
-}
\ No newline at end of file
diff --git a/plc4net/api/api/types/PlcValueType.cs b/plc4net/api/api/types/PlcValueType.cs
deleted file mode 100644
index 9e2b9ebc67..0000000000
--- a/plc4net/api/api/types/PlcValueType.cs
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-namespace org.apache.plc4net.types
-{
- using System;
- using System.Collections.Generic;
-
- /// <summary>
- /// The supported types of PlcValues.
- /// </summary>
- public class PlcValueType
-{
- public static readonly PlcValueType NULL = new PlcValueType(0x00, null);
- public static readonly PlcValueType BOOL = new PlcValueType(0x01, typeof(bool));
- public static readonly PlcValueType BYTE = new PlcValueType(0x02, typeof(byte));
- public static readonly PlcValueType WORD = new PlcValueType(0x03, typeof(short));
- public static readonly PlcValueType DWORD = new PlcValueType(0x04, typeof(int));
- public static readonly PlcValueType LWORD = new PlcValueType(0x05, typeof(long));
- public static readonly PlcValueType USINT = new PlcValueType(0x11, typeof(ushort));
- public static readonly PlcValueType UINT = new PlcValueType(0x12, typeof(uint));
- public static readonly PlcValueType UDINT = new PlcValueType(0x13, typeof(ulong));
- public static readonly PlcValueType ULINT = new PlcValueType(0x14, typeof(System.Numerics.BigInteger));
- public static readonly PlcValueType SINT = new PlcValueType(0x21, typeof(sbyte));
- public static readonly PlcValueType INT = new PlcValueType(0x22, typeof(short));
- public static readonly PlcValueType DINT = new PlcValueType(0x23, typeof(int));
- public static readonly PlcValueType LINT = new PlcValueType(0x24, typeof(long));
- public static readonly PlcValueType REAL = new PlcValueType(0x31, typeof(float));
- public static readonly PlcValueType LREAL = new PlcValueType(0x32, typeof(double));
- public static readonly PlcValueType CHAR = new PlcValueType(0x41, typeof(char));
- public static readonly PlcValueType WCHAR = new PlcValueType(0x42, typeof(short));
- public static readonly PlcValueType STRING = new PlcValueType(0x43, typeof(string));
- public static readonly PlcValueType WSTRING = new PlcValueType(0x44, typeof(string));
- public static readonly PlcValueType TIME = new PlcValueType(0x51, typeof(TimeSpan));
- public static readonly PlcValueType LTIME = new PlcValueType(0x52, typeof(TimeSpan));
- public static readonly PlcValueType DATE = new PlcValueType(0x53, typeof(DateTime));
- public static readonly PlcValueType LDATE = new PlcValueType(0x54, typeof(DateTime));
- public static readonly PlcValueType TIME_OF_DAY = new PlcValueType(0x55, typeof(TimeSpan));
- public static readonly PlcValueType LTIME_OF_DAY = new PlcValueType(0x56, typeof(TimeSpan));
- public static readonly PlcValueType DATE_AND_TIME = new PlcValueType(0x57, typeof(DateTime));
- public static readonly PlcValueType DATE_AND_LTIME = new PlcValueType(0x58, typeof(DateTime));
- public static readonly PlcValueType LDATE_AND_TIME = new PlcValueType(0x59, typeof(DateTime));
- public static readonly PlcValueType Struct = new PlcValueType(0x61, typeof(Dictionary<string, object>));
- public static readonly PlcValueType List = new PlcValueType(0x62, typeof(List<object>));
- public static readonly PlcValueType RAW_BYTE_ARRAY = new PlcValueType(0x71, typeof(byte[]));
-
- private static readonly Dictionary<short, PlcValueType> map = new Dictionary<short, PlcValueType>();
-
- static PlcValueType()
- {
- foreach (var field in typeof(PlcValueType).GetFields(System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public))
- {
- if (field.GetValue(null) is PlcValueType value)
- {
- map[value.Value] = value;
- }
- }
- }
-
- public short Value { get; }
- public Type DefaultDotNetType { get; }
-
- private PlcValueType(short value, Type defaultDotNetType)
- {
- Value = value;
- DefaultDotNetType = defaultDotNetType;
- }
-
- public static PlcValueType EnumForValue(short value)
- {
- map.TryGetValue(value, out var result);
- return result;
- }
-
- public static bool IsDefined(short value)
- {
- return map.ContainsKey(value);
- }
-
- public override string ToString() => $"PlcValueType(0x{Value:X}, {DefaultDotNetType?.Name ?? "null"})";
-}
-}
\ No newline at end of file
diff --git a/plc4net/api/api/authentication/IPlcAuthentication.cs b/plc4net/api/authentication/IPlcAuthentication.cs
similarity index 100%
rename from plc4net/api/api/authentication/IPlcAuthentication.cs
rename to plc4net/api/authentication/IPlcAuthentication.cs
diff --git a/plc4net/api/api/authentication/PlcUsernamePasswordAuthentication.cs b/plc4net/api/authentication/PlcUsernamePasswordAuthentication.cs
similarity index 100%
rename from plc4net/api/api/authentication/PlcUsernamePasswordAuthentication.cs
rename to plc4net/api/authentication/PlcUsernamePasswordAuthentication.cs
diff --git a/plc4net/api/api/exceptions/PlcConnectionException.cs b/plc4net/api/exceptions/PlcConnectionException.cs
similarity index 85%
copy from plc4net/api/api/exceptions/PlcConnectionException.cs
copy to plc4net/api/exceptions/PlcConnectionException.cs
index 491275af10..463bd90713 100644
--- a/plc4net/api/api/exceptions/PlcConnectionException.cs
+++ b/plc4net/api/exceptions/PlcConnectionException.cs
@@ -18,7 +18,6 @@
*/
using System;
-using System.Runtime.Serialization;
namespace org.apache.plc4net.exceptions
{
@@ -26,7 +25,6 @@ namespace org.apache.plc4net.exceptions
/// Exception raised when the connection to a
/// PLC fails
/// </summary>
- [Serializable]
public class PlcConnectionException : PlcException
{
public PlcConnectionException()
@@ -40,11 +38,5 @@ namespace org.apache.plc4net.exceptions
public PlcConnectionException(string message, Exception inner) : base(message, inner)
{
}
-
- protected PlcConnectionException(
- SerializationInfo info,
- StreamingContext context) : base(info, context)
- {
- }
}
}
\ No newline at end of file
diff --git a/plc4net/api/api/exceptions/PlcException.cs b/plc4net/api/exceptions/PlcException.cs
similarity index 84%
rename from plc4net/api/api/exceptions/PlcException.cs
rename to plc4net/api/exceptions/PlcException.cs
index c631347ffe..0307cf8ffe 100644
--- a/plc4net/api/api/exceptions/PlcException.cs
+++ b/plc4net/api/exceptions/PlcException.cs
@@ -18,7 +18,6 @@
*/
using System;
-using System.Runtime.Serialization;
namespace org.apache.plc4net.exceptions
{
@@ -26,9 +25,8 @@ namespace org.apache.plc4net.exceptions
/// General exception that gets thrown when errors while communicating
/// with PLC
/// </summary>
- [Serializable]
public class PlcException : Exception
- {
+ {
public PlcException()
{
}
@@ -40,11 +38,5 @@ namespace org.apache.plc4net.exceptions
public PlcException(string message, Exception inner) : base(message, inner)
{
}
-
- protected PlcException(
- SerializationInfo info,
- StreamingContext context) : base(info, context)
- {
- }
- }
+ }
}
\ No newline at end of file
diff --git a/plc4net/api/api/exceptions/PlcConnectionException.cs b/plc4net/api/exceptions/PlcIncompatibleDatatypeException.cs
similarity index 62%
rename from plc4net/api/api/exceptions/PlcConnectionException.cs
rename to plc4net/api/exceptions/PlcIncompatibleDatatypeException.cs
index 491275af10..a67fbbe682 100644
--- a/plc4net/api/api/exceptions/PlcConnectionException.cs
+++ b/plc4net/api/exceptions/PlcIncompatibleDatatypeException.cs
@@ -18,32 +18,26 @@
*/
using System;
-using System.Runtime.Serialization;
namespace org.apache.plc4net.exceptions
{
/// <summary>
- /// Exception raised when the connection to a
- /// PLC fails
+ /// Thrown when a <c>PlcValue</c> accessor is used for a type the value
+ /// cannot represent, e.g. calling <c>GetString()</c> on a numeric value.
+ /// Mirrors the Java SPI3 <c>PlcIncompatibleDatatypeException</c>: check the
+ /// <c>IsXxx()</c> guards instead of catching this.
/// </summary>
- [Serializable]
- public class PlcConnectionException : PlcException
+ public class PlcIncompatibleDatatypeException : PlcException
{
- public PlcConnectionException()
+ public PlcIncompatibleDatatypeException()
{
}
- public PlcConnectionException(string message) : base(message)
+ public PlcIncompatibleDatatypeException(string message) : base(message)
{
}
- public PlcConnectionException(string message, Exception inner) : base(message, inner)
- {
- }
-
- protected PlcConnectionException(
- SerializationInfo info,
- StreamingContext context) : base(info, context)
+ public PlcIncompatibleDatatypeException(string message, Exception inner) : base(message, inner)
{
}
}
diff --git a/plc4net/api/api/exceptions/PlcInvalidFieldException.cs b/plc4net/api/exceptions/PlcInvalidFieldException.cs
similarity index 83%
rename from plc4net/api/api/exceptions/PlcInvalidFieldException.cs
rename to plc4net/api/exceptions/PlcInvalidFieldException.cs
index 1ead422ff5..bf3837f30c 100644
--- a/plc4net/api/api/exceptions/PlcInvalidFieldException.cs
+++ b/plc4net/api/exceptions/PlcInvalidFieldException.cs
@@ -18,13 +18,11 @@
*/
using System;
-using System.Runtime.Serialization;
namespace org.apache.plc4net.exceptions
{
- [Serializable]
public class PlcInvalidFieldException : PlcException
- {
+ {
public PlcInvalidFieldException()
{
}
@@ -36,11 +34,5 @@ namespace org.apache.plc4net.exceptions
public PlcInvalidFieldException(string message, Exception inner) : base(message, inner)
{
}
-
- protected PlcInvalidFieldException(
- SerializationInfo info,
- StreamingContext context) : base(info, context)
- {
- }
}
}
\ No newline at end of file
diff --git a/plc4net/spi/spi/model/values/PlcLTIME.cs b/plc4net/api/extensions/Plc4NetServiceExtensions.cs
similarity index 51%
copy from plc4net/spi/spi/model/values/PlcLTIME.cs
copy to plc4net/api/extensions/Plc4NetServiceExtensions.cs
index 7a425d3abd..9edfb5c31e 100644
--- a/plc4net/spi/spi/model/values/PlcLTIME.cs
+++ b/plc4net/api/extensions/Plc4NetServiceExtensions.cs
@@ -18,36 +18,25 @@
//
using System;
-using org.apache.plc4net.api.value;
-namespace org.apache.plc4net.spi.model.values
+namespace org.apache.plc4net
{
- public class PlcLTIME : PlcSimpleValueAdapter
+ /// <summary>
+ /// Extension methods for registering PLC4Net drivers via dependency injection.
+ /// Assembly-scanning helpers live in the SPI layer (see
+ /// <c>org.apache.plc4net.spi.drivers.Plc4NetScanExtensions</c>).
+ /// </summary>
+ public static class Plc4NetServiceExtensions
{
- private DateTime value;
-
- public PlcLTIME(DateTime value)
+ /// <summary>
+ /// Registers a driver with the global <see cref="PlcDriverManager"/>.
+ /// </summary>
+ public static PlcDriverManager RegisterDriver(this PlcDriverManager manager, api.IPlcDriver driver)
{
- this.value = value;
+ if (manager == null) throw new ArgumentNullException(nameof(manager));
+ if (driver == null) throw new ArgumentNullException(nameof(driver));
+ manager.RegisterDriver(driver);
+ return manager;
}
-
- protected bool Equals(PlcLTIME other)
- {
- return value.Equals(other.value);
- }
-
- public override bool Equals(object obj)
- {
- if (ReferenceEquals(null, obj)) return false;
- if (ReferenceEquals(this, obj)) return true;
- if (obj.GetType() != this.GetType()) return false;
- return Equals((PlcLTIME) obj);
- }
-
- public override int GetHashCode()
- {
- return value.GetHashCode();
- }
-
}
}
\ No newline at end of file
diff --git a/plc4net/api/api/messages/IPlcMessage.cs b/plc4net/api/messages/IPlcMessage.cs
similarity index 98%
rename from plc4net/api/api/messages/IPlcMessage.cs
rename to plc4net/api/messages/IPlcMessage.cs
index 8604c34c57..6b72f761e9 100644
--- a/plc4net/api/api/messages/IPlcMessage.cs
+++ b/plc4net/api/messages/IPlcMessage.cs
@@ -24,6 +24,6 @@ namespace org.apache.plc4net.messages
/// </summary>
public interface IPlcMessage
{
-
+
}
}
\ No newline at end of file
diff --git a/plc4net/api/api/messages/IPlcReadRequest.cs b/plc4net/api/messages/IPlcReadRequest.cs
similarity index 94%
rename from plc4net/api/api/messages/IPlcReadRequest.cs
rename to plc4net/api/messages/IPlcReadRequest.cs
index 5ba4243b2b..eb41a737a1 100644
--- a/plc4net/api/api/messages/IPlcReadRequest.cs
+++ b/plc4net/api/messages/IPlcReadRequest.cs
@@ -22,8 +22,8 @@ namespace org.apache.plc4net.messages
/// <summary>
/// Interface for requests reading data from a PLC
/// </summary>
- public interface IPlcReadRequest: IPlcRequest
+ public interface IPlcReadRequest : IPlcRequest
{
-
+
}
}
\ No newline at end of file
diff --git a/plc4net/api/api/messages/IPlcReadRequestBuilder.cs b/plc4net/api/messages/IPlcReadRequestBuilder.cs
similarity index 75%
rename from plc4net/api/api/messages/IPlcReadRequestBuilder.cs
rename to plc4net/api/messages/IPlcReadRequestBuilder.cs
index d9bda46b8d..723bebb463 100644
--- a/plc4net/api/api/messages/IPlcReadRequestBuilder.cs
+++ b/plc4net/api/messages/IPlcReadRequestBuilder.cs
@@ -22,14 +22,14 @@ namespace org.apache.plc4net.messages
/// <summary>
/// Builds requests for reading values from a PLC
/// </summary>
- public interface IPlcReadRequestBuilder: IPlcRequestBuilder<IPlcReadRequest>
+ public interface IPlcReadRequestBuilder : IPlcRequestBuilder<IPlcReadRequest>
{
/// <summary>
- /// Add an item to the request
+ /// Add a tag address to the read request.
/// </summary>
- /// <param name="name">Name of the field to add</param>
- /// <param name="fieldQuery">Query string for the field parameter</param>
+ /// <param name="name">Name of the tag to read</param>
+ /// <param name="tagAddress">Address string for the tag</param>
/// <returns>Request builder to allow fluent API calls</returns>
- IPlcReadRequestBuilder AddItem(string name, string fieldQuery);
+ IPlcReadRequestBuilder AddTagAddress(string name, string tagAddress);
}
}
\ No newline at end of file
diff --git a/plc4net/api/api/messages/IPlcReadResponse.cs b/plc4net/api/messages/IPlcReadResponse.cs
similarity index 100%
copy from plc4net/api/api/messages/IPlcReadResponse.cs
copy to plc4net/api/messages/IPlcReadResponse.cs
diff --git a/plc4net/api/api/messages/IPlcRequest.cs b/plc4net/api/messages/IPlcRequest.cs
similarity index 100%
rename from plc4net/api/api/messages/IPlcRequest.cs
rename to plc4net/api/messages/IPlcRequest.cs
diff --git a/plc4net/api/api/messages/IPlcRequestBuilder.cs b/plc4net/api/messages/IPlcRequestBuilder.cs
similarity index 100%
rename from plc4net/api/api/messages/IPlcRequestBuilder.cs
rename to plc4net/api/messages/IPlcRequestBuilder.cs
diff --git a/plc4net/api/api/messages/IPlcResponse.cs b/plc4net/api/messages/IPlcResponse.cs
similarity index 95%
rename from plc4net/api/api/messages/IPlcResponse.cs
rename to plc4net/api/messages/IPlcResponse.cs
index 2665bef47e..0a650c0965 100644
--- a/plc4net/api/api/messages/IPlcResponse.cs
+++ b/plc4net/api/messages/IPlcResponse.cs
@@ -23,7 +23,7 @@ namespace org.apache.plc4net.messages
/// Interface for responses received from
/// PLCs
/// </summary>
- public interface IPlcResponse: IPlcMessage
+ public interface IPlcResponse : IPlcMessage
{
/// <summary>
/// Get the request that generated the response
diff --git a/plc4net/api/api/messages/IPlcSubscriptionEventArgs.cs b/plc4net/api/messages/IPlcSubscriptionEventArgs.cs
similarity index 100%
rename from plc4net/api/api/messages/IPlcSubscriptionEventArgs.cs
rename to plc4net/api/messages/IPlcSubscriptionEventArgs.cs
diff --git a/plc4net/api/api/messages/IPlcSubscriptionRequest.cs b/plc4net/api/messages/IPlcSubscriptionRequest.cs
similarity index 93%
rename from plc4net/api/api/messages/IPlcSubscriptionRequest.cs
rename to plc4net/api/messages/IPlcSubscriptionRequest.cs
index b7b7d51256..d6f65cf38a 100644
--- a/plc4net/api/api/messages/IPlcSubscriptionRequest.cs
+++ b/plc4net/api/messages/IPlcSubscriptionRequest.cs
@@ -22,7 +22,7 @@ namespace org.apache.plc4net.messages
/// <summary>
/// Interface for requests to subscribe to value changes
/// </summary>
- public interface IPlcSubscriptionRequest: IPlcFieldRequest
+ public interface IPlcSubscriptionRequest : IPlcTagRequest
{
}
diff --git a/plc4net/api/api/messages/IPlcSubscriptionRequestBuilder.cs b/plc4net/api/messages/IPlcSubscriptionRequestBuilder.cs
similarity index 95%
rename from plc4net/api/api/messages/IPlcSubscriptionRequestBuilder.cs
rename to plc4net/api/messages/IPlcSubscriptionRequestBuilder.cs
index bf5745950e..93ffdd8674 100644
--- a/plc4net/api/api/messages/IPlcSubscriptionRequestBuilder.cs
+++ b/plc4net/api/messages/IPlcSubscriptionRequestBuilder.cs
@@ -24,7 +24,7 @@ namespace org.apache.plc4net.messages
/// <summary>
/// Interface for classes building requests for subscriptions
/// </summary>
- public interface IPlcSubscriptionRequestBuilder: IPlcRequestBuilder<IPlcSubscriptionRequest>
+ public interface IPlcSubscriptionRequestBuilder : IPlcRequestBuilder<IPlcSubscriptionRequest>
{
/// <summary>
/// Add a field that gets polled cyclically
diff --git a/plc4net/api/api/messages/IPlcSubscriptionResponse.cs b/plc4net/api/messages/IPlcSubscriptionResponse.cs
similarity index 95%
copy from plc4net/api/api/messages/IPlcSubscriptionResponse.cs
copy to plc4net/api/messages/IPlcSubscriptionResponse.cs
index c7ff66a34d..213ebd28cf 100644
--- a/plc4net/api/api/messages/IPlcSubscriptionResponse.cs
+++ b/plc4net/api/messages/IPlcSubscriptionResponse.cs
@@ -25,7 +25,7 @@ namespace org.apache.plc4net.messages
/// <summary>
/// Response to a <see cref="IPlcSubscriptionRequest"/>
/// </summary>
- public interface IPlcSubscriptionResponse: IPlcFieldResponse
+ public interface IPlcSubscriptionResponse : IPlcTagResponse
{
/// <summary>
/// Get the handle for the subscription with the given name
diff --git a/plc4net/api/api/messages/IPlcFieldRequest.cs b/plc4net/api/messages/IPlcTagRequest.cs
similarity index 65%
rename from plc4net/api/api/messages/IPlcFieldRequest.cs
rename to plc4net/api/messages/IPlcTagRequest.cs
index a4f65be694..ec0f11ef07 100644
--- a/plc4net/api/api/messages/IPlcFieldRequest.cs
+++ b/plc4net/api/messages/IPlcTagRequest.cs
@@ -23,30 +23,30 @@ using org.apache.plc4net.model;
namespace org.apache.plc4net.messages
{
/// <summary>
- /// Request for certain fields inside the PLC
+ /// Request for certain tags inside the PLC
/// </summary>
- public interface IPlcFieldRequest: IPlcRequest
+ public interface IPlcTagRequest : IPlcRequest
{
/// <summary>
- /// Number of fields in the request
+ /// Number of tags in the request
/// </summary>
- int FieldCount { get; }
+ int TagCount { get; }
/// <summary>
- /// Enumeration of field names
+ /// Enumeration of tag names
/// </summary>
- IEnumerable<string> FieldNames { get; }
+ IEnumerable<string> TagNames { get; }
/// <summary>
- /// Get a field inside the request by its name
+ /// Get a tag inside the request by its name
/// </summary>
- /// <param name="name">Name of the PLC field to return</param>
- /// <returns></returns>
- IPlcField GetFieldByName(string name);
+ /// <param name="name">Name of the PLC tag to return</param>
+ /// <returns>null when no tag with that name is in the request</returns>
+ IPlcTag? GetTagByName(string name);
/// <summary>
- /// Returns all fields inside the request
+ /// Returns all tags inside the request
/// </summary>
- IEnumerable<IPlcField> Fields { get; }
+ IEnumerable<IPlcTag> Tags { get; }
}
}
\ No newline at end of file
diff --git a/plc4net/api/api/messages/IPlcFieldResponse.cs b/plc4net/api/messages/IPlcTagResponse.cs
similarity index 73%
rename from plc4net/api/api/messages/IPlcFieldResponse.cs
rename to plc4net/api/messages/IPlcTagResponse.cs
index 3dcfa0e68e..b662ef52b7 100644
--- a/plc4net/api/api/messages/IPlcFieldResponse.cs
+++ b/plc4net/api/messages/IPlcTagResponse.cs
@@ -24,22 +24,22 @@ using org.apache.plc4net.types;
namespace org.apache.plc4net.messages
{
/// <summary>
- /// Interface for responses to requests realted to
- /// a specific PLC field
+ /// Interface for responses to requests related to
+ /// a specific PLC tag
/// </summary>
- public interface IPlcFieldResponse: IPlcResponse
+ public interface IPlcTagResponse : IPlcResponse
{
/// <summary>
- /// Enumeration of fields in the response
+ /// Enumeration of tags in the response
/// </summary>
- IEnumerable<string> FieldNames { get; }
+ IEnumerable<string> TagNames { get; }
/// <summary>
- /// Get a field by name
+ /// Get a tag by name
/// </summary>
- /// <param name="name">Name of the field to retrieve</param>
- /// <returns>Field with the given name</returns>
- IPlcField GetFieldByName(string name);
+ /// <param name="name">Name of the tag to retrieve</param>
+ /// <returns>Tag with the given name, or null when absent</returns>
+ IPlcTag? GetTagByName(string name);
/// <summary>
/// Get the response code from the PLC
diff --git a/plc4net/api/api/messages/IPlcUnsubscriptionRequest.cs b/plc4net/api/messages/IPlcUnsubscriptionRequest.cs
similarity index 93%
rename from plc4net/api/api/messages/IPlcUnsubscriptionRequest.cs
rename to plc4net/api/messages/IPlcUnsubscriptionRequest.cs
index 921e6edff4..5409755c77 100644
--- a/plc4net/api/api/messages/IPlcUnsubscriptionRequest.cs
+++ b/plc4net/api/messages/IPlcUnsubscriptionRequest.cs
@@ -22,8 +22,8 @@ namespace org.apache.plc4net.messages
/// <summary>
/// Request for releasing a subscription
/// </summary>
- public interface IPlcUnsubscriptionRequest: IPlcRequest
+ public interface IPlcUnsubscriptionRequest : IPlcRequest
{
-
+
}
}
\ No newline at end of file
diff --git a/plc4net/api/api/messages/IPlcUnsubscriptionRequestBuilder.cs b/plc4net/api/messages/IPlcUnsubscriptionRequestBuilder.cs
similarity index 96%
rename from plc4net/api/api/messages/IPlcUnsubscriptionRequestBuilder.cs
rename to plc4net/api/messages/IPlcUnsubscriptionRequestBuilder.cs
index c053cfe31f..43869151bf 100644
--- a/plc4net/api/api/messages/IPlcUnsubscriptionRequestBuilder.cs
+++ b/plc4net/api/messages/IPlcUnsubscriptionRequestBuilder.cs
@@ -25,7 +25,7 @@ namespace org.apache.plc4net.messages
/// <summary>
/// Constructs requests for unsubscribing from PLC fields
/// </summary>
- public interface IPlcUnsubscriptionRequestBuilder:
+ public interface IPlcUnsubscriptionRequestBuilder :
IPlcRequestBuilder<IPlcUnsubscriptionRequest>
{
/// <summary>
diff --git a/plc4net/api/api/messages/IPlcUnsubscriptionResponse.cs b/plc4net/api/messages/IPlcUnsubscriptionResponse.cs
similarity index 92%
rename from plc4net/api/api/messages/IPlcUnsubscriptionResponse.cs
rename to plc4net/api/messages/IPlcUnsubscriptionResponse.cs
index 6478a15a5a..c3ff196792 100644
--- a/plc4net/api/api/messages/IPlcUnsubscriptionResponse.cs
+++ b/plc4net/api/messages/IPlcUnsubscriptionResponse.cs
@@ -22,8 +22,8 @@ namespace org.apache.plc4net.messages
/// <summary>
/// Interface for requests to release a subscription
/// </summary>
- public interface IPlcUnsubscriptionResponse: IPlcResponse
+ public interface IPlcUnsubscriptionResponse : IPlcResponse
{
-
+
}
}
\ No newline at end of file
diff --git a/plc4net/api/api/messages/IPlcWriteRequest.cs b/plc4net/api/messages/IPlcWriteRequest.cs
similarity index 95%
rename from plc4net/api/api/messages/IPlcWriteRequest.cs
rename to plc4net/api/messages/IPlcWriteRequest.cs
index df0f372ce6..8097aa419d 100644
--- a/plc4net/api/api/messages/IPlcWriteRequest.cs
+++ b/plc4net/api/messages/IPlcWriteRequest.cs
@@ -22,7 +22,7 @@ namespace org.apache.plc4net.messages
/// <summary>
/// Interface for writing values to PLCs
/// </summary>
- public interface IPlcWriteRequest: IPlcRequest
+ public interface IPlcWriteRequest : IPlcRequest
{
/// <summary>
/// Number of values in the request
diff --git a/plc4net/api/api/messages/IPlcWriteRequestBuilder.cs b/plc4net/api/messages/IPlcWriteRequestBuilder.cs
similarity index 56%
rename from plc4net/api/api/messages/IPlcWriteRequestBuilder.cs
rename to plc4net/api/messages/IPlcWriteRequestBuilder.cs
index aa176b4f47..161e423bf0 100644
--- a/plc4net/api/api/messages/IPlcWriteRequestBuilder.cs
+++ b/plc4net/api/messages/IPlcWriteRequestBuilder.cs
@@ -30,105 +30,105 @@ namespace org.apache.plc4net.messages
/// <summary>
/// Interface for a builder that constructs write requests
/// </summary>
- public interface IPlcWriteRequestBuilder: IPlcRequestBuilder<IPlcWriteRequest>
+ public interface IPlcWriteRequestBuilder : IPlcRequestBuilder<IPlcWriteRequest>
{
/// <summary>
/// Add item with bool value
/// </summary>
- /// <param name="name">Identifying name of the field</param>
- /// <param name="fieldQuery">Query string for the field</param>
+ /// <param name="name">Identifying name of the tag</param>
+ /// <param name="fieldQuery">Query string for the tag</param>
/// <param name="values">Values to set</param>
/// <returns>Own instance to allow Fluid API calls</returns>
- IPlcWriteRequestBuilder AddItem(string name, string fieldQuery, params bool[] values);
-
+ IPlcWriteRequestBuilder AddTag(string name, string tagAddress, params bool[] values);
+
/// <summary>
/// Add item with byte value
/// </summary>
- /// <param name="name">Identifying name of the field</param>
- /// <param name="fieldQuery">Query string for the field</param>
+ /// <param name="name">Identifying name of the tag</param>
+ /// <param name="fieldQuery">Query string for the tag</param>
/// <param name="values">Values to set</param>
/// <returns>Own instance to allow Fluid API calls</returns>
- IPlcWriteRequestBuilder AddItem(string name, string fieldQuery, params byte[] values);
+ IPlcWriteRequestBuilder AddTag(string name, string tagAddress, params byte[] values);
/// <summary>
/// Add item with short value
/// </summary>
- /// <param name="name">Identifying name of the field</param>
- /// <param name="fieldQuery">Query string for the field</param>
+ /// <param name="name">Identifying name of the tag</param>
+ /// <param name="fieldQuery">Query string for the tag</param>
/// <param name="values">Values to set</param>
/// <returns>Own instance to allow Fluid API calls</returns>
- IPlcWriteRequestBuilder AddItem(string name, string fieldQuery, params short[] values);
-
+ IPlcWriteRequestBuilder AddTag(string name, string tagAddress, params short[] values);
+
/// <summary>
/// Add item with int value
/// </summary>
- /// <param name="name">Identifying name of the field</param>
- /// <param name="fieldQuery">Query string for the field</param>
+ /// <param name="name">Identifying name of the tag</param>
+ /// <param name="fieldQuery">Query string for the tag</param>
/// <param name="values">Values to set</param>
/// <returns>Own instance to allow Fluid API calls</returns>
- IPlcWriteRequestBuilder AddItem(string name, string fieldQuery, params int[] values);
+ IPlcWriteRequestBuilder AddTag(string name, string tagAddress, params int[] values);
/// <summary>
/// Add item with long value
/// </summary>
- /// <param name="name">Identifying name of the field</param>
- /// <param name="fieldQuery">Query string for the field</param>
+ /// <param name="name">Identifying name of the tag</param>
+ /// <param name="fieldQuery">Query string for the tag</param>
/// <param name="values">Values to set</param>
/// <returns>Own instance to allow Fluid API calls</returns>
- IPlcWriteRequestBuilder AddItem(string name, string fieldQuery, params long[] values);
+ IPlcWriteRequestBuilder AddTag(string name, string tagAddress, params long[] values);
/// <summary>
/// Add item with float value
/// </summary>
- /// <param name="name">Identifying name of the field</param>
- /// <param name="fieldQuery">Query string for the field</param>
+ /// <param name="name">Identifying name of the tag</param>
+ /// <param name="fieldQuery">Query string for the tag</param>
/// <param name="values">Values to set</param>
/// <returns>Own instance to allow Fluid API calls</returns>
- IPlcWriteRequestBuilder AddItem(string name, string fieldQuery, params float[] values);
+ IPlcWriteRequestBuilder AddTag(string name, string tagAddress, params float[] values);
/// <summary>
/// Add item with double value
/// </summary>
- /// <param name="name">Identifying name of the field</param>
- /// <param name="fieldQuery">Query string for the field</param>
+ /// <param name="name">Identifying name of the tag</param>
+ /// <param name="fieldQuery">Query string for the tag</param>
/// <param name="values">Values to set</param>
/// <returns>Own instance to allow Fluid API calls</returns>
- IPlcWriteRequestBuilder AddItem(string name, string fieldQuery, params double[] values);
+ IPlcWriteRequestBuilder AddTag(string name, string tagAddress, params double[] values);
/// <summary>
/// Add item with Decimal value
/// </summary>
- /// <param name="name">Identifying name of the field</param>
- /// <param name="fieldQuery">Query string for the field</param>
+ /// <param name="name">Identifying name of the tag</param>
+ /// <param name="fieldQuery">Query string for the tag</param>
/// <param name="values">Values to set</param>
/// <returns>Own instance to allow Fluid API calls</returns>
- IPlcWriteRequestBuilder AddItem(string name, string fieldQuery, params Decimal[] values);
+ IPlcWriteRequestBuilder AddTag(string name, string tagAddress, params Decimal[] values);
/// <summary>
/// Add item with string value
/// </summary>
- /// <param name="name">Identifying name of the field</param>
- /// <param name="fieldQuery">Query string for the field</param>
+ /// <param name="name">Identifying name of the tag</param>
+ /// <param name="fieldQuery">Query string for the tag</param>
/// <param name="values">Values to set</param>
/// <returns>Own instance to allow Fluid API calls</returns>
- IPlcWriteRequestBuilder AddItem(string name, string fieldQuery, params string[] values);
+ IPlcWriteRequestBuilder AddTag(string name, string tagAddress, params string[] values);
/// <summary>
/// Add item with DateTime value
/// </summary>
- /// <param name="name">Identifying name of the field</param>
- /// <param name="fieldQuery">Query string for the field</param>
+ /// <param name="name">Identifying name of the tag</param>
+ /// <param name="fieldQuery">Query string for the tag</param>
/// <param name="values">Values to set</param>
/// <returns>Own instance to allow Fluid API calls</returns>
- IPlcWriteRequestBuilder AddItem(string name, string fieldQuery, params DateTime[] values);
+ IPlcWriteRequestBuilder AddTag(string name, string tagAddress, params DateTime[] values);
/// <summary>
/// Add item with value of generic type
/// </summary>
- /// <param name="name">Identifying name of the field</param>
- /// <param name="fieldQuery">Query string for the field</param>
+ /// <param name="name">Identifying name of the tag</param>
+ /// <param name="fieldQuery">Query string for the tag</param>
/// <param name="values">Values to set</param>
/// <returns>Own instance to allow Fluid API calls</returns>
- IPlcWriteRequestBuilder AddItem<T>(string name, string fieldQuery, params T[] values);
+ IPlcWriteRequestBuilder AddTag<T>(string name, string tagAddress, params T[] values);
}
}
\ No newline at end of file
diff --git a/plc4net/api/api/messages/IPlcReadResponse.cs b/plc4net/api/messages/IPlcWriteResponse.cs
similarity index 88%
copy from plc4net/api/api/messages/IPlcReadResponse.cs
copy to plc4net/api/messages/IPlcWriteResponse.cs
index c1d1719c8d..8c7668255f 100644
--- a/plc4net/api/api/messages/IPlcReadResponse.cs
+++ b/plc4net/api/messages/IPlcWriteResponse.cs
@@ -20,10 +20,9 @@
namespace org.apache.plc4net.messages
{
/// <summary>
- /// Response for read requests <see cref="IPlcReadRequest"/>
+ /// Response for write requests <see cref="IPlcWriteRequest"/>
/// </summary>
- public interface IPlcReadResponse : IPlcResponse
+ public interface IPlcWriteResponse : IPlcResponse
{
-
}
}
\ No newline at end of file
diff --git a/plc4net/spi/spi/model/values/PlcBitString.cs b/plc4net/api/metadata/DefaultPlcConnectionMetadata.cs
similarity index 56%
copy from plc4net/spi/spi/model/values/PlcBitString.cs
copy to plc4net/api/metadata/DefaultPlcConnectionMetadata.cs
index 42beac3bd3..97aab6ecb4 100644
--- a/plc4net/spi/spi/model/values/PlcBitString.cs
+++ b/plc4net/api/metadata/DefaultPlcConnectionMetadata.cs
@@ -17,29 +17,21 @@
// under the License.
//
-namespace org.apache.plc4net.spi.model.values
+namespace org.apache.plc4net.api.metadata
{
- public class PlcBitString : PlcValueAdapter
+ /// <summary>
+ /// Default implementation of <see cref="IPlcConnectionMetadata"/>
+ /// so drivers don't return null.
+ /// </summary>
+ public class DefaultPlcConnectionMetadata : IPlcConnectionMetadata
{
- public PlcBitString()
- {
- }
+ /// <summary>Whether the connection supports reading values.</summary>
+ public bool CanRead { get; set; }
- public PlcBitString(byte value)
- {
- }
-
- public PlcBitString(ushort value)
- {
- }
-
- public PlcBitString(uint value)
- {
- }
-
- public PlcBitString(ulong value)
- {
- }
+ /// <summary>Whether the connection supports writing values.</summary>
+ public bool CanWrite { get; set; }
+ /// <summary>Whether the connection supports subscriptions.</summary>
+ public bool CanSubscribe { get; set; }
}
}
\ No newline at end of file
diff --git a/plc4net/api/api/metadata/IPlcConnectionMetadata.cs b/plc4net/api/metadata/IPlcConnectionMetadata.cs
similarity index 100%
rename from plc4net/api/api/metadata/IPlcConnectionMetadata.cs
rename to plc4net/api/metadata/IPlcConnectionMetadata.cs
diff --git a/plc4net/api/api/model/IPlcSubscriptionHandle.cs b/plc4net/api/model/IPlcSubscriptionHandle.cs
similarity index 100%
copy from plc4net/api/api/model/IPlcSubscriptionHandle.cs
copy to plc4net/api/model/IPlcSubscriptionHandle.cs
diff --git a/plc4net/spi/spi/model/values/PlcDINT.cs b/plc4net/api/model/IPlcTag.cs
similarity index 64%
copy from plc4net/spi/spi/model/values/PlcDINT.cs
copy to plc4net/api/model/IPlcTag.cs
index 3e3f6beecc..143b518f79 100644
--- a/plc4net/spi/spi/model/values/PlcDINT.cs
+++ b/plc4net/api/model/IPlcTag.cs
@@ -7,7 +7,7 @@
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
-// https://www.apache.org/licenses/LICENSE-2.0
+// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
@@ -17,14 +17,15 @@
// under the License.
//
-using org.apache.plc4net.api.value;
-
-namespace org.apache.plc4net.spi.model.values
+namespace org.apache.plc4net.model
{
- public class PlcDINT : SimpleNumericValueAdapter<int>
+ /// <summary>
+ /// An addressable item on a device. Matches the SPI3 convention: plc4j uses
+ /// <c>PlcTag</c>, and the Field→Tag rename happened during the SPI3 refactoring.
+ /// The legacy <c>IPlcField</c> interface has been removed.
+ /// during the SPI3 refactoring.
+ /// </summary>
+ public interface IPlcTag
{
- public PlcDINT(int value) : base(value)
- {
- }
}
}
\ No newline at end of file
diff --git a/plc4net/api/api/types/PlcResponseCode.cs b/plc4net/api/types/PlcResponseCode.cs
similarity index 79%
rename from plc4net/api/api/types/PlcResponseCode.cs
rename to plc4net/api/types/PlcResponseCode.cs
index 42dee4cdf9..1f5c74bd14 100644
--- a/plc4net/api/api/types/PlcResponseCode.cs
+++ b/plc4net/api/types/PlcResponseCode.cs
@@ -30,6 +30,13 @@ namespace org.apache.plc4net.types
InvalidAddress,
InvalidDatatype,
InternalError,
- ResponsePending
+ ResponsePending,
+
+ /// <summary>The device did not answer within the request timeout.</summary>
+ RequestTimeout,
+
+ /// <summary>The device rejected the request as unsupported (e.g. an
+ /// illegal function code).</summary>
+ Unsupported
}
}
\ No newline at end of file
diff --git a/plc4net/api/types/PlcValueType.cs b/plc4net/api/types/PlcValueType.cs
new file mode 100644
index 0000000000..cb723f945c
--- /dev/null
+++ b/plc4net/api/types/PlcValueType.cs
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+namespace org.apache.plc4net.types
+{
+ using System;
+ using System.Collections.Generic;
+
+ /// <summary>
+ /// The supported types of PlcValues.
+ /// </summary>
+ public class PlcValueType
+ {
+ public static readonly PlcValueType NULL = new PlcValueType(0x00, null);
+ public static readonly PlcValueType BOOL = new PlcValueType(0x01, typeof(bool));
+ public static readonly PlcValueType BYTE = new PlcValueType(0x02, typeof(byte));
+ public static readonly PlcValueType WORD = new PlcValueType(0x03, typeof(ushort));
+ public static readonly PlcValueType DWORD = new PlcValueType(0x04, typeof(uint));
+ public static readonly PlcValueType LWORD = new PlcValueType(0x05, typeof(ulong));
+ public static readonly PlcValueType USINT = new PlcValueType(0x11, typeof(byte));
+ public static readonly PlcValueType UINT = new PlcValueType(0x12, typeof(ushort));
+ public static readonly PlcValueType UDINT = new PlcValueType(0x13, typeof(uint));
+ public static readonly PlcValueType ULINT = new PlcValueType(0x14, typeof(System.Numerics.BigInteger));
+ public static readonly PlcValueType SINT = new PlcValueType(0x21, typeof(sbyte));
+ public static readonly PlcValueType INT = new PlcValueType(0x22, typeof(short));
+ public static readonly PlcValueType DINT = new PlcValueType(0x23, typeof(int));
+ public static readonly PlcValueType LINT = new PlcValueType(0x24, typeof(long));
+ public static readonly PlcValueType REAL = new PlcValueType(0x31, typeof(float));
+ public static readonly PlcValueType LREAL = new PlcValueType(0x32, typeof(double));
+ public static readonly PlcValueType CHAR = new PlcValueType(0x41, typeof(char));
+ public static readonly PlcValueType WCHAR = new PlcValueType(0x42, typeof(char));
+ public static readonly PlcValueType STRING = new PlcValueType(0x43, typeof(string));
+ public static readonly PlcValueType WSTRING = new PlcValueType(0x44, typeof(string));
+ public static readonly PlcValueType TIME = new PlcValueType(0x51, typeof(TimeSpan));
+ public static readonly PlcValueType LTIME = new PlcValueType(0x52, typeof(TimeSpan));
+ public static readonly PlcValueType DATE = new PlcValueType(0x53, typeof(DateOnly));
+ public static readonly PlcValueType LDATE = new PlcValueType(0x54, typeof(DateOnly));
+ public static readonly PlcValueType TIME_OF_DAY = new PlcValueType(0x55, typeof(TimeOnly));
+ public static readonly PlcValueType LTIME_OF_DAY = new PlcValueType(0x56, typeof(TimeOnly));
+ public static readonly PlcValueType DATE_AND_TIME = new PlcValueType(0x57, typeof(DateTime));
+ public static readonly PlcValueType DATE_AND_LTIME = new PlcValueType(0x58, typeof(DateTime));
+ public static readonly PlcValueType LDATE_AND_TIME = new PlcValueType(0x59, typeof(DateTime));
+ public static readonly PlcValueType Struct = new PlcValueType(0x61, typeof(Dictionary<string, object>));
+ public static readonly PlcValueType List = new PlcValueType(0x62, typeof(List<object>));
+ public static readonly PlcValueType RAW_BYTE_ARRAY = new PlcValueType(0x71, typeof(byte[]));
+
+ private static readonly Dictionary<short, PlcValueType> map = new Dictionary<short, PlcValueType>();
+
+ static PlcValueType()
+ {
+ foreach (var field in typeof(PlcValueType).GetFields(System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public))
+ {
+ if (field.GetValue(null) is PlcValueType value)
+ {
+ map[value.Value] = value;
+ }
+ }
+ }
+
+ public short Value { get; }
+ public Type? DefaultDotNetType { get; }
+
+ private PlcValueType(short value, Type? defaultDotNetType)
+ {
+ Value = value;
+ DefaultDotNetType = defaultDotNetType;
+ }
+
+ public static PlcValueType? EnumForValue(short value)
+ {
+ map.TryGetValue(value, out var result);
+ return result;
+ }
+
+ public static bool IsDefined(short value)
+ {
+ return map.ContainsKey(value);
+ }
+
+ public override string ToString() => $"PlcValueType(0x{Value:X}, {DefaultDotNetType?.Name ?? "null"})";
+ }
+}
\ No newline at end of file
diff --git a/plc4net/api/api/value/IPlcValue.cs b/plc4net/api/value/IPlcValue.cs
similarity index 80%
rename from plc4net/api/api/value/IPlcValue.cs
rename to plc4net/api/value/IPlcValue.cs
index bd0ef73f74..76dae3d967 100644
--- a/plc4net/api/api/value/IPlcValue.cs
+++ b/plc4net/api/value/IPlcValue.cs
@@ -64,7 +64,14 @@ namespace org.apache.plc4net.api.value
bool IsString();
string GetString();
- // Time
+ // Temporal — mirrors the java.time split plc4j uses: a bare duration,
+ // a date with no time, a time-of-day with no date, and a full instant.
+ bool IsDuration();
+ TimeSpan GetDuration();
+ bool IsDate();
+ DateOnly GetDate();
+ bool IsTime();
+ TimeOnly GetTime();
bool IsDateTime();
DateTime GetDateTime();
@@ -81,7 +88,9 @@ namespace org.apache.plc4net.api.value
bool IsStruct();
string[] GetKeys();
bool HasKey(string key);
- IPlcValue GetValue(string key);
+ /// <summary>null when no value is stored under <paramref name="key"/>
+ /// (mirrors Java's <c>PlcStruct.getValue</c> returning <c>map.get(key)</c>).</summary>
+ IPlcValue? GetValue(string key);
Dictionary<string, IPlcValue> GetStruct();
}
}
\ No newline at end of file
diff --git a/plc4net/plc4net.sln b/plc4net/plc4net.sln
index 464f265d05..700e338a62 100644
--- a/plc4net/plc4net.sln
+++ b/plc4net/plc4net.sln
@@ -1,41 +1,82 @@
-Microsoft Visual Studio Solution File, Format Version 12.00
-
-#
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-#
-
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "plc4net-api", "api\plc4net-api.csproj", "{7906988A-C7CA-4886-A343-EBC17C0BAB59}"
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "api", "api\api.csproj", "{006BAC0D-D969-4CE8-A6D8-73EDAB8CEA9C}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "plc4net-spi", "spi\plc4net-spi.csproj", "{A92DD9F9-1688-4E70-A95E-709E12DBB8C5}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "spi", "spi\spi.csproj", "{B5604A88-3345-4E14-8D47-C6B0501B4753}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "transports", "transports", "{6D81D0D1-7A60-1ADD-C689-B1528D5CD9EE}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "test", "transports\test\test.csproj", "{D6401677-CDF1-493D-8B37-0B53C4085FC4}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "spi-test", "test\spi-test\spi-test.csproj", "{5CDFBE07-1721-4D36-8F1A-B5533B6E28AD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {7906988A-C7CA-4886-A343-EBC17C0BAB59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {7906988A-C7CA-4886-A343-EBC17C0BAB59}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {7906988A-C7CA-4886-A343-EBC17C0BAB59}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {7906988A-C7CA-4886-A343-EBC17C0BAB59}.Release|Any CPU.Build.0 = Release|Any CPU
- {A92DD9F9-1688-4E70-A95E-709E12DBB8C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {A92DD9F9-1688-4E70-A95E-709E12DBB8C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {A92DD9F9-1688-4E70-A95E-709E12DBB8C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {A92DD9F9-1688-4E70-A95E-709E12DBB8C5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {006BAC0D-D969-4CE8-A6D8-73EDAB8CEA9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {006BAC0D-D969-4CE8-A6D8-73EDAB8CEA9C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {006BAC0D-D969-4CE8-A6D8-73EDAB8CEA9C}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {006BAC0D-D969-4CE8-A6D8-73EDAB8CEA9C}.Debug|x64.Build.0 = Debug|Any CPU
+ {006BAC0D-D969-4CE8-A6D8-73EDAB8CEA9C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {006BAC0D-D969-4CE8-A6D8-73EDAB8CEA9C}.Debug|x86.Build.0 = Debug|Any CPU
+ {006BAC0D-D969-4CE8-A6D8-73EDAB8CEA9C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {006BAC0D-D969-4CE8-A6D8-73EDAB8CEA9C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {006BAC0D-D969-4CE8-A6D8-73EDAB8CEA9C}.Release|x64.ActiveCfg = Release|Any CPU
+ {006BAC0D-D969-4CE8-A6D8-73EDAB8CEA9C}.Release|x64.Build.0 = Release|Any CPU
+ {006BAC0D-D969-4CE8-A6D8-73EDAB8CEA9C}.Release|x86.ActiveCfg = Release|Any CPU
+ {006BAC0D-D969-4CE8-A6D8-73EDAB8CEA9C}.Release|x86.Build.0 = Release|Any CPU
+ {B5604A88-3345-4E14-8D47-C6B0501B4753}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B5604A88-3345-4E14-8D47-C6B0501B4753}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B5604A88-3345-4E14-8D47-C6B0501B4753}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {B5604A88-3345-4E14-8D47-C6B0501B4753}.Debug|x64.Build.0 = Debug|Any CPU
+ {B5604A88-3345-4E14-8D47-C6B0501B4753}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B5604A88-3345-4E14-8D47-C6B0501B4753}.Debug|x86.Build.0 = Debug|Any CPU
+ {B5604A88-3345-4E14-8D47-C6B0501B4753}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B5604A88-3345-4E14-8D47-C6B0501B4753}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B5604A88-3345-4E14-8D47-C6B0501B4753}.Release|x64.ActiveCfg = Release|Any CPU
+ {B5604A88-3345-4E14-8D47-C6B0501B4753}.Release|x64.Build.0 = Release|Any CPU
+ {B5604A88-3345-4E14-8D47-C6B0501B4753}.Release|x86.ActiveCfg = Release|Any CPU
+ {B5604A88-3345-4E14-8D47-C6B0501B4753}.Release|x86.Build.0 = Release|Any CPU
+ {D6401677-CDF1-493D-8B37-0B53C4085FC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D6401677-CDF1-493D-8B37-0B53C4085FC4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D6401677-CDF1-493D-8B37-0B53C4085FC4}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {D6401677-CDF1-493D-8B37-0B53C4085FC4}.Debug|x64.Build.0 = Debug|Any CPU
+ {D6401677-CDF1-493D-8B37-0B53C4085FC4}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {D6401677-CDF1-493D-8B37-0B53C4085FC4}.Debug|x86.Build.0 = Debug|Any CPU
+ {D6401677-CDF1-493D-8B37-0B53C4085FC4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D6401677-CDF1-493D-8B37-0B53C4085FC4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D6401677-CDF1-493D-8B37-0B53C4085FC4}.Release|x64.ActiveCfg = Release|Any CPU
+ {D6401677-CDF1-493D-8B37-0B53C4085FC4}.Release|x64.Build.0 = Release|Any CPU
+ {D6401677-CDF1-493D-8B37-0B53C4085FC4}.Release|x86.ActiveCfg = Release|Any CPU
+ {D6401677-CDF1-493D-8B37-0B53C4085FC4}.Release|x86.Build.0 = Release|Any CPU
+ {5CDFBE07-1721-4D36-8F1A-B5533B6E28AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5CDFBE07-1721-4D36-8F1A-B5533B6E28AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5CDFBE07-1721-4D36-8F1A-B5533B6E28AD}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {5CDFBE07-1721-4D36-8F1A-B5533B6E28AD}.Debug|x64.Build.0 = Debug|Any CPU
+ {5CDFBE07-1721-4D36-8F1A-B5533B6E28AD}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {5CDFBE07-1721-4D36-8F1A-B5533B6E28AD}.Debug|x86.Build.0 = Debug|Any CPU
+ {5CDFBE07-1721-4D36-8F1A-B5533B6E28AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5CDFBE07-1721-4D36-8F1A-B5533B6E28AD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5CDFBE07-1721-4D36-8F1A-B5533B6E28AD}.Release|x64.ActiveCfg = Release|Any CPU
+ {5CDFBE07-1721-4D36-8F1A-B5533B6E28AD}.Release|x64.Build.0 = Release|Any CPU
+ {5CDFBE07-1721-4D36-8F1A-B5533B6E28AD}.Release|x86.ActiveCfg = Release|Any CPU
+ {5CDFBE07-1721-4D36-8F1A-B5533B6E28AD}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {D6401677-CDF1-493D-8B37-0B53C4085FC4} = {6D81D0D1-7A60-1ADD-C689-B1528D5CD9EE}
+ {5CDFBE07-1721-4D36-8F1A-B5533B6E28AD} = {6D81D0D1-7A60-1ADD-C689-B1528D5CD9EE}
EndGlobalSection
EndGlobal
diff --git a/plc4net/pom.xml b/plc4net/pom.xml
index e8102eef5a..c73a8ece23 100644
--- a/plc4net/pom.xml
+++ b/plc4net/pom.xml
@@ -42,6 +42,16 @@
<build>
<plugins>
+ <plugin>
+ <groupId>org.apache.rat</groupId>
+ <artifactId>apache-rat-plugin</artifactId>
+ <configuration>
+ <inputExcludes combine.children="append">
+ <!-- A Visual Studio solution cannot carry a license header. -->
+ <inputExclude>**/*.sln</inputExclude>
+ </inputExcludes>
+ </configuration>
+ </plugin>
<!-- Build the project -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
@@ -60,14 +70,30 @@
<argument>build</argument>
<argument>plc4net.sln</argument>
<argument>--configuration=Debug</argument>
- <argument>--output=${project.build.directory}/build</argument>
<argument>-p:Version=${project.version}</argument>
</arguments>
</configuration>
</execution>
+ <execution>
+ <id>dotnet-test</id>
+ <phase>test</phase>
+ <goals>
+ <goal>exec</goal>
+ </goals>
+ <configuration>
+ <skip>${skipTests}</skip>
+ <executable>dotnet</executable>
+ <workingDirectory>${project.basedir}</workingDirectory>
+ <arguments>
+ <argument>test</argument>
+ <argument>plc4net.sln</argument>
+ <argument>--configuration=Debug</argument>
+ </arguments>
+ </configuration>
+ </execution>
</executions>
</plugin>
</plugins>
</build>
-</project>
\ No newline at end of file
+</project>
diff --git a/plc4net/spi/drivers/ConnectionBase.cs b/plc4net/spi/drivers/ConnectionBase.cs
new file mode 100644
index 0000000000..ad5b102d55
--- /dev/null
+++ b/plc4net/spi/drivers/ConnectionBase.cs
@@ -0,0 +1,148 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+using System;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using org.apache.plc4net.api;
+using org.apache.plc4net.api.authentication;
+using org.apache.plc4net.api.metadata;
+using org.apache.plc4net.messages;
+using org.apache.plc4net.model;
+using org.apache.plc4net.spi.transports;
+
+namespace org.apache.plc4net.spi.drivers
+{
+ /// <summary>
+ /// A connection that wraps a transport instance.
+ /// </summary>
+ /// <remarks>
+ /// Mirrors the Java SPI3 <c>ConnectionBase</c>. Concrete driver connections extend
+ /// this and wire a <see cref="MessageCodecBase{TMessage}"/> over the transport.
+ /// </remarks>
+ public abstract class ConnectionBase : IPlcConnection
+ {
+ private readonly ITransportInstance _transportInstance;
+ private readonly ConnectionString _connectionString;
+ private IPlcAuthentication? _authentication;
+ private ILogger _logger = NullLogger.Instance;
+
+ protected ConnectionBase(ConnectionString connectionString, ITransportInstance transportInstance)
+ {
+ _connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
+ _transportInstance = transportInstance ?? throw new ArgumentNullException(nameof(transportInstance));
+ }
+
+ /// <summary>
+ /// Optional logger. Assigning a real <c>ILogger<T></c> (for example
+ /// via DI injection) adds diagnostic output to Read/Write error paths.
+ /// Defaults to <c>NullLogger.Instance</c> (zero overhead when unset).
+ /// </summary>
+ public ILogger Logger
+ {
+ get => _logger;
+ set => _logger = value ?? NullLogger.Instance;
+ }
+
+ protected ITransportInstance TransportInstance => _transportInstance;
+
+ protected ConnectionString ConnectionInfo => _connectionString;
+
+ public IPlcAuthentication? Authentication
+ {
+ get => _authentication;
+ set => _authentication = value;
+ }
+
+ public bool IsConnected => _transportInstance.IsOpen;
+
+ /// <summary>
+ /// Establishes the protocol-level session on top of the already-open transport.
+ /// Called by <see cref="DriverBase.Connect(string, IPlcAuthentication)"/> after the
+ /// connection object has been constructed, so that construction itself stays free of
+ /// blocking I/O. Mirrors the Java SPI3 split between <c>getConnection()</c> and
+ /// <c>connect()</c>: the constructor only records configuration, and every network
+ /// round-trip a protocol needs before it can carry payload happens here.
+ /// </summary>
+ public void Connect()
+ {
+ OnConnect();
+ }
+
+ /// <summary>
+ /// Async equivalent of <see cref="Connect"/>. The default implementation
+ /// delegates to the synchronous hook; drivers whose handshake performs I/O
+ /// (COTP CR/CC) may override this to run it on a background thread.
+ /// </summary>
+ public virtual async System.Threading.Tasks.Task ConnectAsync()
+ {
+ await System.Threading.Tasks.Task.Run(() => OnConnect())
+ .ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Protocol-specific connection setup. The default implementation does nothing, which
+ /// is correct for protocols that carry payload directly over the raw transport
+ /// (Modbus TCP). Protocols requiring a handshake before their first PDU (S7 over COTP)
+ /// override this. Mirrors the Java SPI3 <c>ConnectionBase.onConnect()</c> hook.
+ /// </summary>
+ protected virtual void OnConnect()
+ {
+ }
+
+ public virtual void Close()
+ {
+ _transportInstance.Close();
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!disposing)
+ {
+ return;
+ }
+ try
+ {
+ Close();
+ }
+ catch (TransportException)
+ {
+ // Dispose must not throw.
+ }
+ }
+
+ public abstract IPlcConnectionMetadata PlcConnectionMetadata { get; }
+
+ public abstract IPlcTag Parse(string tagQuery);
+
+ public abstract IPlcReadRequestBuilder? ReadRequestBuilder { get; }
+
+ public abstract IPlcWriteRequestBuilder? WriteRequestBuilder { get; }
+
+ public abstract IPlcSubscriptionRequestBuilder? SubscriptionRequestBuilder { get; }
+
+ public abstract IPlcUnsubscriptionRequestBuilder? UnsubscriptionRequestBuilder { get; }
+ }
+}
\ No newline at end of file
diff --git a/plc4net/spi/drivers/ConnectionString.cs b/plc4net/spi/drivers/ConnectionString.cs
new file mode 100644
index 0000000000..3b533eb4ba
--- /dev/null
+++ b/plc4net/spi/drivers/ConnectionString.cs
@@ -0,0 +1,217 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Net;
+using System.Text.RegularExpressions;
+using org.apache.plc4net.exceptions;
+
+namespace org.apache.plc4net.spi.drivers
+{
+ /// <summary>
+ /// A parsed PLC4X connection string:
+ /// <c>{protocol-code}(:{transport-code})?://{transport-config}(?{parameter-string})?</c>
+ /// </summary>
+ /// <remarks>
+ /// The grammar is kept identical to the Java SPI3 <c>DriverBase.URI_PATTERN</c> so the
+ /// same connection string addresses the same device from either language. Examples:
+ /// <c>s7://192.168.0.1</c>, <c>s7:cotp://10.0.0.5:102?remote-rack=0&remote-slot=1</c>,
+ /// <c>modbus-tcp://10.0.0.9:502?unit-identifier=1</c>.
+ /// </remarks>
+ public sealed class ConnectionString
+ {
+ private static readonly Regex UriPattern = new Regex(
+ @"^(?<protocolCode>[a-z0-9\-]*)(:(?<transportCode>[a-z0-9\-]*))?://(?<transportConfig>[^?]*)(\?(?<paramString>.*))?$",
+ RegexOptions.Compiled);
+
+ // Masks the value of any query parameter whose name looks like it carries a
+ // credential, so connection strings can be logged safely.
+ private static readonly Regex SecretParamPattern = new Regex(
+ @"([?&][^=&]*(?:password|passwd|secret|token)[^=&]*=)[^&]*",
+ RegexOptions.Compiled | RegexOptions.IgnoreCase);
+
+ private ConnectionString(
+ string protocolCode,
+ string? transportCode,
+ string transportConfig,
+ string paramString,
+ IReadOnlyDictionary<string, string> parameters)
+ {
+ ProtocolCode = protocolCode;
+ TransportCode = transportCode;
+ TransportConfig = transportConfig;
+ ParamString = paramString;
+ Parameters = parameters;
+ }
+
+ /// <summary>e.g. "s7", "modbus-tcp".</summary>
+ public string ProtocolCode { get; }
+
+ /// <summary>e.g. "tcp", "cotp". Null when the string relies on the driver's default.</summary>
+ public string? TransportCode { get; }
+
+ /// <summary>The address the transport consumes, e.g. "192.168.0.1:102".</summary>
+ public string TransportConfig { get; }
+
+ /// <summary>The raw query string, without the leading '?'. Empty when absent.</summary>
+ public string ParamString { get; }
+
+ /// <summary>Query parameters, parsed. Keys are case-insensitive.</summary>
+ public IReadOnlyDictionary<string, string> Parameters { get; }
+
+ public static ConnectionString Parse(string? connectionString)
+ {
+ if (string.IsNullOrWhiteSpace(connectionString))
+ {
+ throw new PlcConnectionException("Connection string must not be empty.");
+ }
+
+ var match = UriPattern.Match(connectionString);
+ if (!match.Success)
+ {
+ throw new PlcConnectionException(
+ "Connection string doesn't match the format " +
+ "'{protocol-code}(:{transport-code})?://{transport-config}(?{parameter-string})?'");
+ }
+
+ var protocolCode = match.Groups["protocolCode"].Value;
+ if (string.IsNullOrEmpty(protocolCode))
+ {
+ throw new PlcConnectionException("Connection string is missing the protocol code.");
+ }
+
+ var transportGroup = match.Groups["transportCode"];
+ var transportCode = transportGroup.Success && transportGroup.Value.Length > 0
+ ? transportGroup.Value
+ : null;
+
+ var paramString = match.Groups["paramString"].Success
+ ? match.Groups["paramString"].Value
+ : string.Empty;
+
+ return new ConnectionString(
+ protocolCode,
+ transportCode,
+ match.Groups["transportConfig"].Value,
+ paramString,
+ ParseParameters(paramString));
+ }
+
+ private static IReadOnlyDictionary<string, string> ParseParameters(string paramString)
+ {
+ var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ if (string.IsNullOrEmpty(paramString))
+ {
+ return result;
+ }
+
+ foreach (var pair in paramString.Split('&'))
+ {
+ if (pair.Length == 0)
+ {
+ continue;
+ }
+ var separator = pair.IndexOf('=');
+ if (separator < 0)
+ {
+ // A bare flag, e.g. "?verbose" — treat as present-and-true.
+ result[Decode(pair)] = "true";
+ continue;
+ }
+ var key = Decode(pair.Substring(0, separator));
+ var value = Decode(pair.Substring(separator + 1));
+ result[key] = value;
+ }
+ return result;
+ }
+
+ /// <summary>
+ /// Decodes one parameter name or value.
+ /// </summary>
+ /// <remarks>
+ /// Java decodes parameter values with <c>URLDecoder.decode(value, UTF_8)</c>, which
+ /// is the application/x-www-form-urlencoded decoder: it turns '+' into a space as
+ /// well as resolving %XX escapes. <c>Uri.UnescapeDataString</c> resolves the escapes
+ /// but leaves '+' alone, so "?name=a+b" would mean different things in each
+ /// language. <c>WebUtility.UrlDecode</c> is the .NET equivalent of Java's decoder.
+ /// </remarks>
+ private static string Decode(string raw)
+ {
+ return WebUtility.UrlDecode(raw);
+ }
+
+ /// <summary>
+ /// Looks up a parameter, returning <paramref name="defaultValue"/> when absent.
+ /// </summary>
+ public string? GetParameter(string name, string? defaultValue = null)
+ {
+ return Parameters.TryGetValue(name, out var value) ? value : defaultValue;
+ }
+
+ public int GetIntParameter(string name, int defaultValue)
+ {
+ var raw = GetParameter(name);
+ // Invariant culture: a connection string means the same thing regardless of
+ // the machine's locale, matching Java's Integer.parseInt.
+ return int.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)
+ ? value
+ : defaultValue;
+ }
+
+ public bool GetBoolParameter(string name, bool defaultValue)
+ {
+ var raw = GetParameter(name);
+ if (string.IsNullOrEmpty(raw))
+ return defaultValue;
+ // Accept .NET bool literals + numeric and on/off forms for
+ // compatibility with common connection-string conventions.
+ if (raw == "1" || raw.Equals("on", StringComparison.OrdinalIgnoreCase)
+ || raw.Equals("yes", StringComparison.OrdinalIgnoreCase))
+ return true;
+ if (raw == "0" || raw.Equals("off", StringComparison.OrdinalIgnoreCase)
+ || raw.Equals("no", StringComparison.OrdinalIgnoreCase))
+ return false;
+ return bool.TryParse(raw, out var value) ? value : defaultValue;
+ }
+
+ /// <summary>
+ /// Replaces credential-bearing parameter values with '***' so a connection string
+ /// can be written to a log.
+ /// </summary>
+ public static string? RedactSecrets(string? connectionString)
+ {
+ if (connectionString == null)
+ {
+ return null;
+ }
+ return SecretParamPattern.Replace(connectionString, "$1***");
+ }
+
+ public override string ToString()
+ {
+ var transport = TransportCode == null ? string.Empty : ":" + TransportCode;
+ var parameters = ParamString.Length == 0 ? string.Empty : "?" + ParamString;
+ // The argument is an interpolated string, so RedactSecrets cannot
+ // return null here; the coalesce only satisfies its nullable return.
+ return RedactSecrets($"{ProtocolCode}{transport}://{TransportConfig}{parameters}") ?? string.Empty;
+ }
+ }
+}
\ No newline at end of file
diff --git a/plc4net/spi/drivers/DriverBase.cs b/plc4net/spi/drivers/DriverBase.cs
new file mode 100644
index 0000000000..954ac6a139
--- /dev/null
+++ b/plc4net/spi/drivers/DriverBase.cs
@@ -0,0 +1,294 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+using org.apache.plc4net.api;
+using org.apache.plc4net.api.authentication;
+using org.apache.plc4net.exceptions;
+using org.apache.plc4net.spi.transports;
+
+namespace org.apache.plc4net.spi.drivers
+{
+ /// <summary>
+ /// Shared logic for every driver: connection-string dispatch, transport selection,
+ /// configuration parsing, supported-transport enforcement.
+ /// </summary>
+ /// <remarks>
+ /// Mirrors the Java SPI3 <c>DriverBase</c>. A concrete driver implements
+ /// <see cref="GetProtocolCode"/>, <see cref="GetProtocolName"/>, and
+ /// <see cref="CreateConnection"/>; everything else is handled here.
+ /// </remarks>
+ public abstract class DriverBase : IPlcDriver
+ {
+ private readonly ITransportManager _transportManager;
+
+ protected DriverBase(ITransportManager transportManager)
+ {
+ _transportManager = transportManager ?? throw new ArgumentNullException(nameof(transportManager));
+ }
+
+ protected void RegisterTransport(ITransport transport)
+ {
+ if (_transportManager is DefaultTransportManager defaultManager)
+ {
+ defaultManager.Register(transport);
+ }
+ }
+
+ // ----- abstract members a driver must implement -----
+
+ /// <summary>e.g. "s7", "modbus-tcp".</summary>
+ public abstract string ProtocolCode { get; }
+
+ /// <summary>Human-readable name, e.g. "Siemens S7".</summary>
+ public abstract string ProtocolName { get; }
+
+ /// <summary>
+ /// Optional default transport when the connection string omits one
+ /// (e.g. "s7" defaults to "cotp").
+ /// </summary>
+ public abstract string DefaultTransportCode { get; }
+
+ /// <summary>
+ /// The transports this driver supports. A connection string that requests a
+ /// different transport is rejected unless <c>allow-unsupported-transport=true</c>
+ /// is set.
+ /// </summary>
+ protected abstract string[] SupportedTransportCodes { get; }
+
+ /// <summary>
+ /// Constructs a protocol-specific connection. Called after the transport has
+ /// been opened and validated. Implementations record configuration only; the
+ /// protocol handshake belongs in <see cref="ConnectionBase.OnConnect"/>, which
+ /// <see cref="Connect(string, IPlcAuthentication)"/> invokes on the result.
+ /// </summary>
+ protected abstract ConnectionBase CreateConnection(
+ ConnectionString connectionString,
+ ITransportInstance transportInstance,
+ IPlcAuthentication? authentication);
+
+ // ----- public driver API -----
+
+ public IPlcConnection Connect(string connectionString)
+ {
+ return Connect(connectionString, null);
+ }
+
+ public IPlcConnection Connect(string connectionString, IPlcAuthentication? authentication)
+ {
+ if (string.IsNullOrWhiteSpace(connectionString))
+ {
+ throw new PlcConnectionException("Connection string must not be empty.");
+ }
+
+ var parsed = ConnectionString.Parse(connectionString);
+
+ if (!string.Equals(parsed.ProtocolCode, ProtocolCode, StringComparison.OrdinalIgnoreCase))
+ {
+ throw new PlcConnectionException(
+ $"This driver ({ProtocolCode}) is not suited for protocol '{parsed.ProtocolCode}'.");
+ }
+
+ var transportCode = ResolveTransportCode(parsed);
+
+ var transport = _transportManager.GetTransport(transportCode);
+ if (transport == null)
+ {
+ throw new PlcConnectionException(
+ $"No transport registered for code '{transportCode}'. " +
+ $"Registered transports: {string.Join(", ", _transportManager.GetTransportCodes())}");
+ }
+
+ var rawTransportConfig = parsed.TransportConfig;
+ // Per SPI3, the driver-config portion of the URL is the part after the
+ // transport's parsed host:port. The TCP transport returns it as /milo in
+ // opcua:tcp://localhost:4840/milo; for drivers that don't consume one it
+ // stays empty.
+ var transportConfig = transport.CreateConfiguration(parsed.Parameters);
+ var transportInstance = transport.CreateTransportInstance(rawTransportConfig, transportConfig);
+
+ // The transport is already open by this point, so a driver constructor that
+ // throws (e.g. parameter validation) must not leak it: nothing else holds a
+ // reference. Closing the instance is chain-safe — a wrapper transport (COTP
+ // over TCP) closes its inner one.
+ ConnectionBase connection;
+ try
+ {
+ connection = CreateConnection(parsed, transportInstance, authentication);
+ }
+ catch
+ {
+ try
+ {
+ transportInstance.Close();
+ }
+ catch
+ {
+ // Preserve the original failure; cleanup errors must not mask it.
+ }
+ throw;
+ }
+
+ // Establish the protocol-level session. Construction above only records
+ // configuration; anything that talks to the device happens here, so that a
+ // driver's constructor never blocks on I/O. If setup fails the connection is
+ // closed before the exception escapes — the caller never receives a reference,
+ // so nobody else can release the socket.
+ //
+ // Closing goes through the connection rather than the transport instance
+ // created above: a driver may wrap it (S7 puts COTP on top of TCP) and it is
+ // the wrapper the connection holds. Closing the inner one directly would leave
+ // the wrapper's own state untouched.
+ try
+ {
+ connection.Connect();
+ }
+ catch (PlcConnectionException)
+ {
+ try
+ {
+ connection.Close();
+ }
+ catch
+ {
+ // Preserve the original failure; cleanup errors must not mask it.
+ }
+ throw;
+ }
+ catch (Exception ex)
+ {
+ try
+ {
+ connection.Close();
+ }
+ catch
+ {
+ // Preserve the original failure; cleanup errors must not mask it.
+ }
+
+ // Every other Connect() failure path reports PlcConnectionException;
+ // a handshake failure that surfaces as TransportException must not
+ // escape that contract. Java's SPI3 wraps both getConnection() and
+ // connect() failures the same way.
+ throw new PlcConnectionException(
+ $"Failed to establish the connection: {ex.Message}", ex);
+ }
+
+ return connection;
+ }
+
+ /// <summary>
+ /// Async equivalent of <see cref="Connect(string, IPlcAuthentication)"/>.
+ /// Runs the transport creation and protocol handshake on a background thread
+ /// so that UI threads and ASP.NET request threads are not blocked.
+ /// </summary>
+ public async System.Threading.Tasks.Task<IPlcConnection> ConnectAsync(
+ string connectionString,
+ IPlcAuthentication? authentication = null)
+ {
+ var parsed = ConnectionString.Parse(connectionString);
+
+ if (!string.Equals(parsed.ProtocolCode, ProtocolCode, StringComparison.OrdinalIgnoreCase))
+ {
+ throw new PlcConnectionException(
+ $"This driver ({ProtocolCode}) is not suited for protocol '{parsed.ProtocolCode}'.");
+ }
+
+ var transportCode = ResolveTransportCode(parsed);
+
+ var transport = _transportManager.GetTransport(transportCode);
+ if (transport == null)
+ {
+ throw new PlcConnectionException(
+ $"No transport registered for code '{transportCode}'. " +
+ $"Registered transports: {string.Join(", ", _transportManager.GetTransportCodes())}");
+ }
+
+ var transportConfig = transport.CreateConfiguration(parsed.Parameters);
+ // Opening a socket can block, so keep this step off the caller's thread too.
+ var transportInstance = await System.Threading.Tasks.Task.Run(() =>
+ transport.CreateTransportInstance(parsed.TransportConfig, transportConfig))
+ .ConfigureAwait(false);
+
+ ConnectionBase connection;
+ try
+ {
+ connection = CreateConnection(parsed, transportInstance, authentication);
+ }
+ catch
+ {
+ try { transportInstance.Close(); }
+ catch { }
+ throw;
+ }
+
+ try
+ {
+ await connection.ConnectAsync().ConfigureAwait(false);
+ }
+ catch (PlcConnectionException)
+ {
+ try { connection.Close(); }
+ catch { }
+ throw;
+ }
+ catch (Exception ex)
+ {
+ try { connection.Close(); }
+ catch { }
+ throw new PlcConnectionException(
+ $"Failed to establish the connection: {ex.Message}", ex);
+ }
+
+ return connection;
+ }
+
+ private string ResolveTransportCode(ConnectionString parsed)
+ {
+ if (!string.IsNullOrWhiteSpace(parsed.TransportCode))
+ {
+ var requested = parsed.TransportCode;
+ if (!SupportedTransportCodes.Contains(requested, StringComparer.OrdinalIgnoreCase))
+ {
+ var allowUnsupported = parsed.GetBoolParameter("allow-unsupported-transport", false);
+ if (!allowUnsupported)
+ {
+ throw new PlcConnectionException(
+ $"Transport '{requested}' is not supported by driver '{ProtocolCode}'. " +
+ $"Supported transports: {string.Join(", ", SupportedTransportCodes)}. " +
+ "Set 'allow-unsupported-transport=true' to bypass this check.",
+ new Exception("Unsupported transport"));
+ }
+ }
+ return requested;
+ }
+
+ if (!string.IsNullOrWhiteSpace(DefaultTransportCode))
+ {
+ return DefaultTransportCode;
+ }
+
+ throw new PlcConnectionException(
+ $"Driver '{ProtocolCode}' has no default transport and the connection string did not specify one.");
+ }
+ }
+}
\ No newline at end of file
diff --git a/plc4net/spi/drivers/MessageCodecBase.cs b/plc4net/spi/drivers/MessageCodecBase.cs
new file mode 100644
index 0000000000..b06a81204c
--- /dev/null
+++ b/plc4net/spi/drivers/MessageCodecBase.cs
@@ -0,0 +1,206 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+using System;
+using org.apache.plc4net.spi.generation;
+using org.apache.plc4net.spi.transports;
+
+namespace org.apache.plc4net.spi.drivers
+{
+ /// <summary>
+ /// Raised when a protocol message cannot be encoded, decoded or transmitted.
+ /// </summary>
+ public class MessageCodecException : Exception
+ {
+ public MessageCodecException(string message) : base(message)
+ {
+ }
+
+ public MessageCodecException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+ }
+
+ /// <summary>
+ /// A protocol message that can report its own wire length and serialize itself.
+ /// </summary>
+ /// <remarks>
+ /// Generated message classes are intended to implement this. Note that the C#
+ /// code generator does not yet emit serialization logic (see the port notes in
+ /// the PLC4Net README), so today this is implemented by hand-written messages
+ /// and by test fixtures.
+ /// </remarks>
+ public interface IMessage
+ {
+ /// <summary>Length of this message once serialized, in bytes.</summary>
+ int GetLengthInBytes();
+
+ /// <summary>Writes this message into the supplied buffer.</summary>
+ void Serialize(WriteBuffer writeBuffer);
+ }
+
+ /// <summary>
+ /// Turns a transport's byte stream into whole protocol messages and back.
+ /// </summary>
+ /// <remarks>
+ /// Mirrors the Java SPI3 <c>MessageCodecBase</c>. The framing contract is the
+ /// three abstract members: a driver states the smallest header it can act on,
+ /// derives the total frame length from that header, and parses a complete frame.
+ /// Everything else — partial reads, several messages arriving in one packet,
+ /// waiting for the rest of a frame — is handled here.
+ /// </remarks>
+ /// <typeparam name="TMessage">the message type this codec handles</typeparam>
+ public abstract class MessageCodecBase<TMessage> where TMessage : class
+ {
+ private readonly string _protocolName;
+ private readonly ITransportInstance _transportInstance;
+ private readonly Action<TMessage> _messageHandler;
+
+ protected MessageCodecBase(
+ string protocolName,
+ ITransportInstance transportInstance,
+ Action<TMessage> messageHandler)
+ {
+ _protocolName = protocolName ?? throw new ArgumentNullException(nameof(protocolName));
+ _transportInstance = transportInstance ?? throw new ArgumentNullException(nameof(transportInstance));
+ _messageHandler = messageHandler ?? throw new ArgumentNullException(nameof(messageHandler));
+ }
+
+ protected ITransportInstance TransportInstance => _transportInstance;
+
+ /// <summary>
+ /// Smallest number of bytes needed before <see cref="CalculateTotalMessageSize"/>
+ /// can say anything useful.
+ /// </summary>
+ protected abstract int GetMinimumHeaderSize();
+
+ /// <summary>
+ /// Total frame length in bytes, derived from the peeked header. Return a negative
+ /// value to signal "cannot tell yet, wait for more bytes".
+ /// </summary>
+ protected abstract int CalculateTotalMessageSize(byte[] header, int availableBytes);
+
+ /// <summary>Parses exactly one complete frame.</summary>
+ protected abstract TMessage ParseMessage(ReadBuffer readBuffer);
+
+ public void Send(TMessage message)
+ {
+ if (message == null)
+ {
+ throw new ArgumentNullException(nameof(message));
+ }
+ if (!(message is IMessage serializable))
+ {
+ throw new MessageCodecException(
+ $"{typeof(TMessage).Name} does not implement IMessage and cannot be serialized.");
+ }
+
+ try
+ {
+ var writeBuffer = new WriteBuffer();
+ serializable.Serialize(writeBuffer);
+ _transportInstance.Write(writeBuffer.GetBytes());
+ }
+ catch (TransportException e)
+ {
+ throw new MessageCodecException($"Failed to send {_protocolName} message", e);
+ }
+ catch (ParseException e)
+ {
+ throw new MessageCodecException($"Failed to serialize {_protocolName} message", e);
+ }
+ }
+
+ /// <summary>
+ /// Drains as many complete messages as the buffer currently holds, handing each
+ /// to the message handler. Returns without consuming anything when the next
+ /// frame has not fully arrived.
+ /// </summary>
+ public void ProcessIncomingData()
+ {
+ try
+ {
+ while (true)
+ {
+ var minimumHeaderSize = GetMinimumHeaderSize();
+
+ var availableBytes = _transportInstance.GetNumBytesAvailable();
+ if (availableBytes < minimumHeaderSize)
+ {
+ return;
+ }
+
+ var header = _transportInstance.PeekReadableBytes(minimumHeaderSize);
+
+ int totalMessageSize;
+ try
+ {
+ totalMessageSize = CalculateTotalMessageSize(header, availableBytes);
+ }
+ catch (MessageCodecException)
+ {
+ // The header does not describe a valid frame - the byte stream
+ // is misaligned (a datagram dropped mid-frame, a truncated or
+ // malformed frame). Discard one byte and resync on the next
+ // pass rather than wedging on the same bad header forever.
+ _transportInstance.Read(1);
+ continue;
+ }
+ if (totalMessageSize < 0)
+ {
+ return;
+ }
+
+ if (availableBytes < totalMessageSize)
+ {
+ // The frame is still in flight; leave the bytes buffered.
+ return;
+ }
+
+ var messageBytes = _transportInstance.Read(totalMessageSize);
+ var message = ParseMessage(new ReadBuffer(messageBytes));
+ _messageHandler(message);
+ }
+ }
+ catch (TransportException e)
+ {
+ throw new MessageCodecException($"Failed to receive {_protocolName} message", e);
+ }
+ catch (ParseException e)
+ {
+ throw new MessageCodecException($"Failed to parse {_protocolName} message", e);
+ }
+ }
+
+ public bool IsOpen => _transportInstance.IsOpen;
+
+ public void Close()
+ {
+ try
+ {
+ _transportInstance.Close();
+ }
+ catch (TransportException e)
+ {
+ throw new MessageCodecException("Failed to close transport", e);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/plc4net/spi/drivers/ScanExtensions.cs b/plc4net/spi/drivers/ScanExtensions.cs
new file mode 100644
index 0000000000..7689377fe9
--- /dev/null
+++ b/plc4net/spi/drivers/ScanExtensions.cs
@@ -0,0 +1,117 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+using System;
+using System.Reflection;
+using org.apache.plc4net.api;
+using org.apache.plc4net.spi.transports;
+
+namespace org.apache.plc4net.spi.drivers
+{
+ /// <summary>
+ /// Assembly scanning helpers for auto-registering drivers. These live
+ /// in the SPI layer (not api) so they have access to <c>ITransportManager</c>
+ /// without creating a circular dependency.
+ /// </summary>
+ public static class Plc4NetScanExtensions
+ {
+ /// <summary>
+ /// Scans <paramref name="assembly"/> for types extending <c>DriverBase</c>,
+ /// instantiates each one with the given <paramref name="transportManager"/>,
+ /// and registers the resulting driver with the global <c>PlcDriverManager</c>.
+ /// </summary>
+ public static int ScanAndRegisterDrivers(
+ this PlcDriverManager manager,
+ Assembly assembly,
+ ITransportManager transportManager)
+ {
+ if (manager == null) throw new ArgumentNullException(nameof(manager));
+ if (assembly == null) throw new ArgumentNullException(nameof(assembly));
+ if (transportManager == null) throw new ArgumentNullException(nameof(transportManager));
+
+ var count = 0;
+ foreach (var type in assembly.GetExportedTypes())
+ {
+ if (type.IsAbstract || type.IsInterface) continue;
+
+ if (!IsDriverBase(type)) continue;
+
+ var ctors = type.GetConstructors(
+ BindingFlags.Public | BindingFlags.Instance);
+ if (ctors.Length == 0) continue;
+
+ foreach (var ctor in ctors)
+ {
+ var parameters = ctor.GetParameters();
+ var args = new object[parameters.Length];
+ var matched = true;
+ for (int i = 0; i < parameters.Length; i++)
+ {
+ if (typeof(ITransportManager)
+ .IsAssignableFrom(parameters[i].ParameterType))
+ {
+ args[i] = transportManager;
+ }
+ else if (parameters[i].HasDefaultValue)
+ {
+ args[i] = parameters[i].DefaultValue!;
+ }
+ else if (parameters[i].ParameterType.IsValueType)
+ {
+ args[i] = Activator.CreateInstance(
+ parameters[i].ParameterType)!;
+ }
+ else
+ {
+ matched = false;
+ break;
+ }
+ }
+ if (!matched) continue;
+
+ try
+ {
+ var driver = (IPlcDriver)ctor.Invoke(args);
+ manager.RegisterDriver(driver);
+ count++;
+ break;
+ }
+ catch
+ {
+ // Try the next constructor.
+ }
+ }
+ }
+ return count;
+ }
+
+ private static bool IsDriverBase(Type type)
+ {
+ var baseType = type.BaseType;
+ while (baseType != null)
+ {
+ if (baseType.FullName ==
+ "org.apache.plc4net.spi.drivers.DriverBase")
+ return true;
+ baseType = baseType.BaseType;
+ }
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/plc4net/api/api/model/IPlcSubscriptionHandle.cs b/plc4net/spi/drivers/functions/PlcReader.cs
similarity index 66%
copy from plc4net/api/api/model/IPlcSubscriptionHandle.cs
copy to plc4net/spi/drivers/functions/PlcReader.cs
index e605d29083..2008ea448c 100644
--- a/plc4net/api/api/model/IPlcSubscriptionHandle.cs
+++ b/plc4net/spi/drivers/functions/PlcReader.cs
@@ -17,19 +17,22 @@
* under the License.
*/
-using System;
+using System.Threading;
+using System.Threading.Tasks;
using org.apache.plc4net.messages;
+using org.apache.plc4net.spi.drivers.messages;
-namespace org.apache.plc4net.model
+namespace org.apache.plc4net.spi.drivers.functions
{
/// <summary>
- /// Handle to a subscription
+ /// The interface a driver implements to execute a read request.
+ /// Mirrors Java SPI3 <c>PlcReader</c>.
/// </summary>
- public interface IPlcSubscriptionHandle
+ public interface PlcReader
{
/// <summary>
- /// Event that can be subscribed to handle events raised by the subscription
+ /// Executes a read request, returning the response.
/// </summary>
- event EventHandler<IPlcSubscriptionEventArgs> PlcSubscriptionEvent;
+ Task<IPlcReadResponse> Read(DefaultPlcReadRequest request, CancellationToken cancellationToken = default);
}
}
\ No newline at end of file
diff --git a/plc4net/api/api/model/IPlcSubscriptionHandle.cs b/plc4net/spi/drivers/functions/PlcWriter.cs
similarity index 66%
rename from plc4net/api/api/model/IPlcSubscriptionHandle.cs
rename to plc4net/spi/drivers/functions/PlcWriter.cs
index e605d29083..8bb38d78db 100644
--- a/plc4net/api/api/model/IPlcSubscriptionHandle.cs
+++ b/plc4net/spi/drivers/functions/PlcWriter.cs
@@ -17,19 +17,22 @@
* under the License.
*/
-using System;
+using System.Threading;
+using System.Threading.Tasks;
using org.apache.plc4net.messages;
+using org.apache.plc4net.spi.drivers.messages;
-namespace org.apache.plc4net.model
+namespace org.apache.plc4net.spi.drivers.functions
{
/// <summary>
- /// Handle to a subscription
+ /// The interface a driver implements to execute a write request.
+ /// Mirrors Java SPI3 <c>PlcWriter</c>.
/// </summary>
- public interface IPlcSubscriptionHandle
+ public interface PlcWriter
{
/// <summary>
- /// Event that can be subscribed to handle events raised by the subscription
+ /// Executes a write request, returning the response.
/// </summary>
- event EventHandler<IPlcSubscriptionEventArgs> PlcSubscriptionEvent;
+ Task<IPlcWriteResponse> Write(DefaultPlcWriteRequest request, CancellationToken cancellationToken = default);
}
}
\ No newline at end of file
diff --git a/plc4net/spi/drivers/messages/DefaultPlcReadRequest.cs b/plc4net/spi/drivers/messages/DefaultPlcReadRequest.cs
new file mode 100644
index 0000000000..0e4924c965
--- /dev/null
+++ b/plc4net/spi/drivers/messages/DefaultPlcReadRequest.cs
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using org.apache.plc4net.messages;
+using org.apache.plc4net.model;
+using org.apache.plc4net.spi.drivers.functions;
+using org.apache.plc4net.spi.drivers.messages.items;
+
+namespace org.apache.plc4net.spi.drivers.messages
+{
+ /// <summary>
+ /// Default implementation of a read request.
+ /// Mirrors the Java SPI3 <c>DefaultPlcReadRequest</c>.
+ /// </summary>
+ public class DefaultPlcReadRequest : IPlcReadRequest, IPlcTagRequest
+ {
+ private readonly PlcReader _reader;
+ private readonly Dictionary<string, PlcTagItem<IPlcTag>> _tags;
+
+ public DefaultPlcReadRequest(PlcReader reader, Dictionary<string, PlcTagItem<IPlcTag>> tags)
+ {
+ _reader = reader;
+ _tags = tags ?? new Dictionary<string, PlcTagItem<IPlcTag>>();
+ }
+
+ public int TagCount => _tags.Count;
+
+ public IEnumerable<string> TagNames => _tags.Keys;
+
+ public IPlcTag? GetTagByName(string name)
+ {
+ return _tags.TryGetValue(name, out var item) ? item.Tag : null;
+ }
+
+ public IEnumerable<IPlcTag> Tags => _tags.Values.Select(v => v.Tag);
+
+ async Task<IPlcResponse> IPlcRequest.ExecuteAsync()
+ {
+ return (IPlcResponse)await _reader.Read(this).ConfigureAwait(false);
+ }
+ }
+}
\ No newline at end of file
diff --git a/plc4net/spi/drivers/messages/DefaultPlcReadRequestBuilder.cs b/plc4net/spi/drivers/messages/DefaultPlcReadRequestBuilder.cs
new file mode 100644
index 0000000000..ac22bfa3e9
--- /dev/null
+++ b/plc4net/spi/drivers/messages/DefaultPlcReadRequestBuilder.cs
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+using System;
+using System.Collections.Generic;
+using org.apache.plc4net.messages;
+using org.apache.plc4net.model;
+using org.apache.plc4net.spi.drivers.functions;
+using org.apache.plc4net.spi.drivers.messages.items;
+
+namespace org.apache.plc4net.spi.drivers.messages
+{
+ /// <summary>
+ /// Default builder for read requests.
+ /// </summary>
+ public class DefaultPlcReadRequestBuilder : IPlcReadRequestBuilder
+ {
+ private readonly PlcReader _reader;
+ private readonly Func<string, IPlcTag> _tagParser;
+ private readonly Dictionary<string, PlcTagItem<IPlcTag>> _tags
+ = new Dictionary<string, PlcTagItem<IPlcTag>>();
+
+ public DefaultPlcReadRequestBuilder(PlcReader reader, Func<string, IPlcTag>? tagParser)
+ {
+ _reader = reader;
+ _tagParser = tagParser ?? (s => new GenericTag(s));
+ }
+
+ public IPlcReadRequestBuilder AddTagAddress(string name, string tagAddress)
+ {
+ var tag = _tagParser(tagAddress);
+ _tags[name] = new DefaultPlcTagItem<IPlcTag>(name, tag);
+ return this;
+ }
+
+ public IPlcReadRequest Build()
+ {
+ return new DefaultPlcReadRequest(_reader, _tags);
+ }
+ }
+
+ /// <summary>
+ /// Fallback tag carrying an address string — used when no protocol-specific
+ /// parser is available.
+ /// </summary>
+ internal class GenericTag : IPlcTag
+ {
+ public GenericTag(string address) { Address = address; }
+ public string Address { get; }
+ public override string ToString() => Address;
+ }
+}
\ No newline at end of file
diff --git a/plc4net/spi/drivers/messages/DefaultPlcReadResponse.cs b/plc4net/spi/drivers/messages/DefaultPlcReadResponse.cs
new file mode 100644
index 0000000000..59f64bb79f
--- /dev/null
+++ b/plc4net/spi/drivers/messages/DefaultPlcReadResponse.cs
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+using System.Collections.Generic;
+using org.apache.plc4net.api.value;
+using org.apache.plc4net.messages;
+using org.apache.plc4net.spi.drivers.messages.items;
+using org.apache.plc4net.types;
+
+namespace org.apache.plc4net.spi.drivers.messages
+{
+ /// <summary>
+ /// Default implementation of a read response.
+ /// Mirrors the Java SPI3 <c>DefaultPlcReadResponse</c>.
+ /// </summary>
+ public class DefaultPlcReadResponse : IPlcReadResponse
+ {
+ private readonly IPlcReadRequest _request;
+ private readonly Dictionary<string, PlcResponseItem<IPlcValue>> _values;
+
+ public DefaultPlcReadResponse(
+ IPlcReadRequest request,
+ Dictionary<string, PlcResponseItem<IPlcValue>> values)
+ {
+ _request = request;
+ _values = values ?? new Dictionary<string, PlcResponseItem<IPlcValue>>();
+ }
+
+ // Explicit interface implementation: IPlcResponse.Request returns IPlcRequest.
+ IPlcRequest IPlcResponse.Request => _request;
+
+ /// <summary>The originating read request.</summary>
+ public IPlcReadRequest Request => _request;
+
+ /// <summary>
+ /// Returns the response code for a named tag.
+ /// </summary>
+ public PlcResponseCode GetResponseCode(string name)
+ {
+ return _values.TryGetValue(name, out var item)
+ ? item.Code
+ : PlcResponseCode.NotFound;
+ }
+
+ /// <summary>
+ /// Returns the value for a named tag, or null if the tag is absent or in error.
+ /// </summary>
+ public IPlcValue? GetValue(string name)
+ {
+ return _values.TryGetValue(name, out var item) && item.Code == PlcResponseCode.Ok
+ ? item.Value
+ : null;
+ }
+
+ public IEnumerable<string> TagNames => _values.Keys;
+ }
+}
\ No newline at end of file
diff --git a/plc4net/spi/drivers/messages/DefaultPlcWriteRequest.cs b/plc4net/spi/drivers/messages/DefaultPlcWriteRequest.cs
new file mode 100644
index 0000000000..ea9083df14
--- /dev/null
+++ b/plc4net/spi/drivers/messages/DefaultPlcWriteRequest.cs
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using org.apache.plc4net.messages;
+using org.apache.plc4net.model;
+using org.apache.plc4net.spi.drivers.functions;
+using org.apache.plc4net.spi.drivers.messages.items;
+
+namespace org.apache.plc4net.spi.drivers.messages
+{
+ /// <summary>
+ /// Default implementation of a write request.
+ /// Mirrors the Java SPI3 <c>DefaultPlcWriteRequest</c>.
+ /// </summary>
+ public class DefaultPlcWriteRequest : IPlcWriteRequest, IPlcTagRequest
+ {
+ private readonly PlcWriter _writer;
+ private readonly Dictionary<string, PlcTagValueItem<IPlcTag>> _tags;
+
+ public DefaultPlcWriteRequest(PlcWriter writer, Dictionary<string, PlcTagValueItem<IPlcTag>> tags)
+ {
+ _writer = writer;
+ _tags = tags ?? new Dictionary<string, PlcTagValueItem<IPlcTag>>();
+ }
+
+ public int NumberOfValues => _tags.Count;
+
+ public int TagCount => _tags.Count;
+
+ public IEnumerable<string> TagNames => _tags.Keys;
+
+ public IPlcTag? GetTagByName(string name)
+ {
+ return _tags.TryGetValue(name, out var item) ? item.Tag : null;
+ }
+
+ public IEnumerable<IPlcTag> Tags => _tags.Values.Select(v => v.Tag);
+
+ /// <summary>Returns the value to write to the named tag, or null.</summary>
+ public object? GetValue(string name)
+ {
+ return _tags.TryGetValue(name, out var item) ? item.Value : null;
+ }
+
+ async Task<IPlcResponse> IPlcRequest.ExecuteAsync()
+ {
+ return (IPlcResponse)await _writer.Write(this).ConfigureAwait(false);
+ }
+ }
+}
\ No newline at end of file
diff --git a/plc4net/spi/drivers/messages/DefaultPlcWriteRequestBuilder.cs b/plc4net/spi/drivers/messages/DefaultPlcWriteRequestBuilder.cs
new file mode 100644
index 0000000000..02385981bd
--- /dev/null
+++ b/plc4net/spi/drivers/messages/DefaultPlcWriteRequestBuilder.cs
@@ -0,0 +1,143 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+using System;
+using System.Collections.Generic;
+using org.apache.plc4net.messages;
+using org.apache.plc4net.model;
+using org.apache.plc4net.spi.drivers.functions;
+using org.apache.plc4net.spi.drivers.messages.items;
+
+namespace org.apache.plc4net.spi.drivers.messages
+{
+ /// <summary>
+ /// Default builder for write requests.
+ /// </summary>
+ public class DefaultPlcWriteRequestBuilder : IPlcWriteRequestBuilder
+ {
+ private readonly PlcWriter _writer;
+ private readonly Func<string, IPlcTag> _tagParser;
+ private readonly Dictionary<string, PlcTagValueItem<IPlcTag>> _tags
+ = new Dictionary<string, PlcTagValueItem<IPlcTag>>();
+
+ public DefaultPlcWriteRequestBuilder(PlcWriter writer, Func<string, IPlcTag>? tagParser)
+ {
+ _writer = writer;
+ _tagParser = tagParser ?? (s => new GenericTag(s));
+ }
+
+ // For each typed overload, unwrap a single-element params array so the
+ // driver's Write method receives a scalar value rather than an array.
+
+ public IPlcWriteRequestBuilder AddTag(string name, string tagAddress, params bool[] values)
+ {
+ _tags[name] = new DefaultPlcTagValueItem<IPlcTag>(
+ name, _tagParser(tagAddress), Unwrap(values));
+ return this;
+ }
+
+ public IPlcWriteRequestBuilder AddTag(string name, string tagAddress, params byte[] values)
+ {
+ _tags[name] = new DefaultPlcTagValueItem<IPlcTag>(
+ name, _tagParser(tagAddress), Unwrap(values));
+ return this;
+ }
+
+ public IPlcWriteRequestBuilder AddTag(string name, string tagAddress, params short[] values)
+ {
+ _tags[name] = new DefaultPlcTagValueItem<IPlcTag>(
+ name, _tagParser(tagAddress), Unwrap(values));
+ return this;
+ }
+
+ public IPlcWriteRequestBuilder AddTag(string name, string tagAddress, params int[] values)
+ {
+ _tags[name] = new DefaultPlcTagValueItem<IPlcTag>(
+ name, _tagParser(tagAddress), Unwrap(values));
+ return this;
+ }
+
+ public IPlcWriteRequestBuilder AddTag(string name, string tagAddress, params long[] values)
+ {
+ _tags[name] = new DefaultPlcTagValueItem<IPlcTag>(
+ name, _tagParser(tagAddress), Unwrap(values));
+ return this;
+ }
+
+ public IPlcWriteRequestBuilder AddTag(string name, string tagAddress, params float[] values)
+ {
+ _tags[name] = new DefaultPlcTagValueItem<IPlcTag>(
+ name, _tagParser(tagAddress), Unwrap(values));
+ return this;
+ }
+
+ public IPlcWriteRequestBuilder AddTag(string name, string tagAddress, params double[] values)
+ {
+ _tags[name] = new DefaultPlcTagValueItem<IPlcTag>(
+ name, _tagParser(tagAddress), Unwrap(values));
+ return this;
+ }
+
+ public IPlcWriteRequestBuilder AddTag(string name, string tagAddress, params Decimal[] values)
+ {
+ _tags[name] = new DefaultPlcTagValueItem<IPlcTag>(
+ name, _tagParser(tagAddress), Unwrap(values));
+ return this;
+ }
+
+ public IPlcWriteRequestBuilder AddTag(string name, string tagAddress, params string[] values)
+ {
+ _tags[name] = new DefaultPlcTagValueItem<IPlcTag>(
+ name, _tagParser(tagAddress), Unwrap(values));
+ return this;
+ }
+
+ public IPlcWriteRequestBuilder AddTag(string name, string tagAddress, params DateTime[] values)
+ {
+ _tags[name] = new DefaultPlcTagValueItem<IPlcTag>(
+ name, _tagParser(tagAddress), Unwrap(values));
+ return this;
+ }
+
+ public IPlcWriteRequestBuilder AddTag<T>(string name, string tagAddress, params T[] values)
+ {
+ _tags[name] = new DefaultPlcTagValueItem<IPlcTag>(
+ name, _tagParser(tagAddress), Unwrap(values));
+ return this;
+ }
+
+ public IPlcWriteRequest Build()
+ {
+ return new DefaultPlcWriteRequest(_writer, _tags);
+ }
+
+ /// <summary>
+ /// If the array has one element, return it directly; if more than one,
+ /// throw because multi-value writes are not yet supported.
+ /// </summary>
+ private static object? Unwrap<T>(T[] values)
+ {
+ if (values == null || values.Length == 0) return null;
+ if (values.Length == 1) return values[0];
+ throw new NotSupportedException(
+ $"Multi-value writes (passed {values.Length} values of type {typeof(T).Name}) " +
+ "are not yet supported. Write each tag individually.");
+ }
+ }
+}
\ No newline at end of file
diff --git a/plc4net/spi/drivers/messages/DefaultPlcWriteResponse.cs b/plc4net/spi/drivers/messages/DefaultPlcWriteResponse.cs
new file mode 100644
index 0000000000..f95c2ca805
--- /dev/null
+++ b/plc4net/spi/drivers/messages/DefaultPlcWriteResponse.cs
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+using System.Collections.Generic;
+using org.apache.plc4net.messages;
+using org.apache.plc4net.types;
+
+namespace org.apache.plc4net.spi.drivers.messages
+{
+ /// <summary>
+ /// Default implementation of a write response.
+ /// </summary>
+ public class DefaultPlcWriteResponse : IPlcWriteResponse
+ {
+ private readonly IPlcWriteRequest _request;
+ private readonly Dictionary<string, PlcResponseCode> _codes;
+
+ public DefaultPlcWriteResponse(IPlcWriteRequest request, Dictionary<string, PlcResponseCode> codes)
+ {
+ _request = request;
+ _codes = codes ?? new Dictionary<string, PlcResponseCode>();
+ }
+
+ // Explicit interface implementation: IPlcResponse.Request returns IPlcRequest.
+ IPlcRequest IPlcResponse.Request => _request;
+
+ /// <summary>The originating write request.</summary>
+ public IPlcWriteRequest Request => _request;
+
+ /// <summary>Returns the response code for a named tag.</summary>
+ public PlcResponseCode GetResponseCode(string name)
+ {
+ return _codes.TryGetValue(name, out var code) ? code : PlcResponseCode.NotFound;
+ }
+
+ public IEnumerable<string> TagNames => _codes.Keys;
+ }
+}
\ No newline at end of file
diff --git a/plc4net/api/api/messages/IPlcReadResponse.cs b/plc4net/spi/drivers/messages/items/DefaultPlcResponseItem.cs
similarity index 66%
copy from plc4net/api/api/messages/IPlcReadResponse.cs
copy to plc4net/spi/drivers/messages/items/DefaultPlcResponseItem.cs
index c1d1719c8d..8501ba9f59 100644
--- a/plc4net/api/api/messages/IPlcReadResponse.cs
+++ b/plc4net/spi/drivers/messages/items/DefaultPlcResponseItem.cs
@@ -17,13 +17,23 @@
* under the License.
*/
-namespace org.apache.plc4net.messages
+using org.apache.plc4net.types;
+
+namespace org.apache.plc4net.spi.drivers.messages.items
{
/// <summary>
- /// Response for read requests <see cref="IPlcReadRequest"/>
+ /// Default implementation of <see cref="PlcResponseItem{T}"/>.
/// </summary>
- public interface IPlcReadResponse : IPlcResponse
+ public class DefaultPlcResponseItem<T> : PlcResponseItem<T>
{
+ public DefaultPlcResponseItem(PlcResponseCode code, T? value)
+ {
+ Code = code;
+ Value = value;
+ }
+
+ public PlcResponseCode Code { get; }
+ public T? Value { get; }
}
}
\ No newline at end of file
diff --git a/plc4net/api/api/messages/IPlcReadResponse.cs b/plc4net/spi/drivers/messages/items/DefaultPlcTagErrorItem.cs
similarity index 66%
copy from plc4net/api/api/messages/IPlcReadResponse.cs
copy to plc4net/spi/drivers/messages/items/DefaultPlcTagErrorItem.cs
index c1d1719c8d..a7b6f6bd97 100644
--- a/plc4net/api/api/messages/IPlcReadResponse.cs
+++ b/plc4net/spi/drivers/messages/items/DefaultPlcTagErrorItem.cs
@@ -17,13 +17,23 @@
* under the License.
*/
-namespace org.apache.plc4net.messages
+using org.apache.plc4net.types;
+
+namespace org.apache.plc4net.spi.drivers.messages.items
{
/// <summary>
- /// Response for read requests <see cref="IPlcReadRequest"/>
+ /// A response item that represents an error for a specific tag.
/// </summary>
- public interface IPlcReadResponse : IPlcResponse
+ public class DefaultPlcTagErrorItem<T> : PlcResponseItem<T>
{
+ public DefaultPlcTagErrorItem(PlcResponseCode code)
+ {
+ Code = code;
+ Value = default;
+ }
+
+ public PlcResponseCode Code { get; }
+ public T? Value { get; }
}
}
\ No newline at end of file
diff --git a/plc4net/api/api/messages/IPlcReadResponse.cs b/plc4net/spi/drivers/messages/items/DefaultPlcTagItem.cs
similarity index 66%
copy from plc4net/api/api/messages/IPlcReadResponse.cs
copy to plc4net/spi/drivers/messages/items/DefaultPlcTagItem.cs
index c1d1719c8d..c79abc6103 100644
--- a/plc4net/api/api/messages/IPlcReadResponse.cs
+++ b/plc4net/spi/drivers/messages/items/DefaultPlcTagItem.cs
@@ -17,13 +17,23 @@
* under the License.
*/
-namespace org.apache.plc4net.messages
+using org.apache.plc4net.model;
+
+namespace org.apache.plc4net.spi.drivers.messages.items
{
/// <summary>
- /// Response for read requests <see cref="IPlcReadRequest"/>
+ /// Default implementation of <see cref="PlcTagItem{T}"/> for read requests.
/// </summary>
- public interface IPlcReadResponse : IPlcResponse
+ public class DefaultPlcTagItem<T> : PlcTagItem<T> where T : IPlcTag
{
+ public DefaultPlcTagItem(string name, T tag)
+ {
+ Name = name;
+ Tag = tag;
+ }
+
+ public string Name { get; }
+ public T Tag { get; }
}
}
\ No newline at end of file
diff --git a/plc4net/api/api/messages/IPlcReadResponse.cs b/plc4net/spi/drivers/messages/items/DefaultPlcTagValueItem.cs
similarity index 61%
copy from plc4net/api/api/messages/IPlcReadResponse.cs
copy to plc4net/spi/drivers/messages/items/DefaultPlcTagValueItem.cs
index c1d1719c8d..8e98943b67 100644
--- a/plc4net/api/api/messages/IPlcReadResponse.cs
+++ b/plc4net/spi/drivers/messages/items/DefaultPlcTagValueItem.cs
@@ -17,13 +17,26 @@
* under the License.
*/
-namespace org.apache.plc4net.messages
+using org.apache.plc4net.model;
+
+namespace org.apache.plc4net.spi.drivers.messages.items
{
/// <summary>
- /// Response for read requests <see cref="IPlcReadRequest"/>
+ /// Default implementation of <see cref="PlcTagValueItem{T}"/> for write requests.
/// </summary>
- public interface IPlcReadResponse : IPlcResponse
+ public class DefaultPlcTagValueItem<T> : PlcTagValueItem<T> where T : IPlcTag
{
+ public DefaultPlcTagValueItem(string name, T tag, object? value)
+ {
+ Name = name;
+ Tag = tag;
+ Value = value;
+ }
+
+ public string Name { get; }
+
+ public T Tag { get; }
+ public object? Value { get; }
}
}
\ No newline at end of file
diff --git a/plc4net/api/api/messages/IPlcReadResponse.cs b/plc4net/spi/drivers/messages/items/PlcResponseItem.cs
similarity index 63%
copy from plc4net/api/api/messages/IPlcReadResponse.cs
copy to plc4net/spi/drivers/messages/items/PlcResponseItem.cs
index c1d1719c8d..39441b8140 100644
--- a/plc4net/api/api/messages/IPlcReadResponse.cs
+++ b/plc4net/spi/drivers/messages/items/PlcResponseItem.cs
@@ -17,13 +17,20 @@
* under the License.
*/
-namespace org.apache.plc4net.messages
+using org.apache.plc4net.types;
+
+namespace org.apache.plc4net.spi.drivers.messages.items
{
/// <summary>
- /// Response for read requests <see cref="IPlcReadRequest"/>
+ /// An item inside a response — either a value or an error.
+ /// Mirrors Java SPI3 <c>PlcResponseItem<T></c>.
/// </summary>
- public interface IPlcReadResponse : IPlcResponse
+ public interface PlcResponseItem<T>
{
+ /// <summary>The response code for this item.</summary>
+ PlcResponseCode Code { get; }
+ /// <summary>The value, valid only when <see cref="Code"/> is <see cref="PlcResponseCode.OK"/>.</summary>
+ T? Value { get; }
}
}
\ No newline at end of file
diff --git a/plc4net/api/api/messages/IPlcReadResponse.cs b/plc4net/spi/drivers/messages/items/PlcTagItem.cs
similarity index 64%
copy from plc4net/api/api/messages/IPlcReadResponse.cs
copy to plc4net/spi/drivers/messages/items/PlcTagItem.cs
index c1d1719c8d..4afd2c36d7 100644
--- a/plc4net/api/api/messages/IPlcReadResponse.cs
+++ b/plc4net/spi/drivers/messages/items/PlcTagItem.cs
@@ -17,13 +17,20 @@
* under the License.
*/
-namespace org.apache.plc4net.messages
+using org.apache.plc4net.model;
+
+namespace org.apache.plc4net.spi.drivers.messages.items
{
/// <summary>
- /// Response for read requests <see cref="IPlcReadRequest"/>
+ /// A tag item in a request — a named pair of a tag address.
+ /// Mirrors Java SPI3 <c>PlcTagItem<T extends PlcTag></c>.
/// </summary>
- public interface IPlcReadResponse : IPlcResponse
+ public interface PlcTagItem<T> where T : IPlcTag
{
+ /// <summary>Name the caller gave this tag.</summary>
+ string Name { get; }
+ /// <summary>The tag with its protocol-specific address.</summary>
+ T Tag { get; }
}
}
\ No newline at end of file
diff --git a/plc4net/api/api/messages/IPlcReadResponse.cs b/plc4net/spi/drivers/messages/items/PlcTagValueItem.cs
similarity index 68%
rename from plc4net/api/api/messages/IPlcReadResponse.cs
rename to plc4net/spi/drivers/messages/items/PlcTagValueItem.cs
index c1d1719c8d..e95c29f802 100644
--- a/plc4net/api/api/messages/IPlcReadResponse.cs
+++ b/plc4net/spi/drivers/messages/items/PlcTagValueItem.cs
@@ -17,13 +17,17 @@
* under the License.
*/
-namespace org.apache.plc4net.messages
+using org.apache.plc4net.model;
+
+namespace org.apache.plc4net.spi.drivers.messages.items
{
/// <summary>
- /// Response for read requests <see cref="IPlcReadRequest"/>
+ /// A tag item with a value — used by write requests.
+ /// Mirrors Java SPI3 <c>PlcTagValueItem<T extends PlcTag></c>.
/// </summary>
- public interface IPlcReadResponse : IPlcResponse
+ public interface PlcTagValueItem<T> : PlcTagItem<T> where T : IPlcTag
{
-
+ /// <summary>The value to write to this tag.</summary>
+ object? Value { get; }
}
}
\ No newline at end of file
diff --git a/plc4net/api/api/messages/IPlcSubscriptionResponse.cs b/plc4net/spi/drivers/tags/PlcTagHandler.cs
similarity index 55%
rename from plc4net/api/api/messages/IPlcSubscriptionResponse.cs
rename to plc4net/spi/drivers/tags/PlcTagHandler.cs
index c7ff66a34d..8691c987b3 100644
--- a/plc4net/api/api/messages/IPlcSubscriptionResponse.cs
+++ b/plc4net/spi/drivers/tags/PlcTagHandler.cs
@@ -17,26 +17,27 @@
* under the License.
*/
-using System.Collections.Generic;
using org.apache.plc4net.model;
-namespace org.apache.plc4net.messages
+namespace org.apache.plc4net.spi.drivers.tags
{
/// <summary>
- /// Response to a <see cref="IPlcSubscriptionRequest"/>
+ /// A driver implements this to parse its protocol-specific tag address syntax.
+ /// Mirrors Java SPI3 <c>PlcTagHandler</c>.
/// </summary>
- public interface IPlcSubscriptionResponse: IPlcFieldResponse
+ /// <remarks>
+ /// Each driver registers its own handler. The framework calls
+ /// <see cref="ParseTag"/> when the user writes something like
+ /// <c>builder.AddTagAddress("myTag", "%DB1.DBW10")</c>.
+ /// </remarks>
+ public interface PlcTagHandler
{
/// <summary>
- /// Get the handle for the subscription with the given name
+ /// Parses a tag address string into the driver's concrete tag type.
/// </summary>
- /// <param name="name">Name of the subscription</param>
- /// <returns>Subscription handle</returns>
- IPlcSubscriptionHandle GetSubscriptionHandle(string name);
-
- /// <summary>
- /// Get all subscription handles
- /// </summary>
- IEnumerable<IPlcSubscriptionHandle> SubscriptionHandles { get; }
+ /// <param name="tagAddress">The address string, e.g. "%DB1.DBW10" for S7
+ /// or "coil:0" for Modbus.</param>
+ /// <returns>A protocol-specific tag.</returns>
+ IPlcTag ParseTag(string tagAddress);
}
}
\ No newline at end of file
diff --git a/plc4net/spi/generation/BitReader.cs b/plc4net/spi/generation/BitReader.cs
new file mode 100644
index 0000000000..d3e5637bae
--- /dev/null
+++ b/plc4net/spi/generation/BitReader.cs
@@ -0,0 +1,146 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+using System;
+
+namespace org.apache.plc4net.spi.generation
+{
+ /// <summary>
+ /// Reads big-endian (MSB-first) bit fields out of a byte array.
+ /// </summary>
+ /// <remarks>
+ /// This replaces the third-party Ayx.BitIO package, which is a .NET Framework
+ /// only assembly, unmaintained since 2016, and which threw
+ /// ArgumentOutOfRangeException when asked for a 32 bit field because it
+ /// round-tripped every read through a StringBuilder of '0'/'1' characters.
+ ///
+ /// MSB-first is the order every mspec-described protocol uses: the first bit of
+ /// a frame is the high bit of byte 0.
+ /// </remarks>
+ public class BitReader
+ {
+ private readonly byte[] _data;
+ private int _bitPosition;
+
+ public BitReader(byte[] data)
+ {
+ _data = data ?? throw new ArgumentNullException(nameof(data));
+ }
+
+ /// <summary>Current read offset, in bits.</summary>
+ public int Position
+ {
+ get => _bitPosition;
+ set
+ {
+ if (value < 0 || value > TotalBits)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value));
+ }
+ _bitPosition = value;
+ }
+ }
+
+ public int TotalBits => _data.Length * 8;
+
+ /// <summary>Number of bits not yet consumed.</summary>
+ public int Remaining => TotalBits - _bitPosition;
+
+ public bool ReadBool()
+ {
+ return ReadBits(1) != 0;
+ }
+
+ /// <summary>
+ /// Reads <paramref name="bitCount"/> bits and returns them right-aligned.
+ /// </summary>
+ public ulong ReadBits(int bitCount)
+ {
+ if (bitCount < 0 || bitCount > 64)
+ {
+ throw new ArgumentOutOfRangeException(
+ nameof(bitCount), bitCount, "Can read between 0 and 64 bits at a time.");
+ }
+ if (bitCount > Remaining)
+ {
+ throw new ParseException(
+ $"Not enough data: tried to read {bitCount} bits, {Remaining} available.");
+ }
+
+ ulong result = 0;
+ for (var i = 0; i < bitCount; i++)
+ {
+ var byteIndex = _bitPosition >> 3;
+ var bitOffset = 7 - (_bitPosition & 7); // MSB-first within the byte
+ var bit = (ulong)((_data[byteIndex] >> bitOffset) & 1);
+ result = (result << 1) | bit;
+ _bitPosition++;
+ }
+ return result;
+ }
+
+ /// <summary>
+ /// Reads <paramref name="bitCount"/> bits and sign-extends them from that width.
+ /// </summary>
+ public long ReadSignedBits(int bitCount)
+ {
+ var raw = ReadBits(bitCount);
+ if (bitCount == 0 || bitCount == 64)
+ {
+ return unchecked((long)raw);
+ }
+ var signMask = 1UL << (bitCount - 1);
+ if ((raw & signMask) != 0)
+ {
+ // Set every bit above the field width so the value stays negative.
+ raw |= ulong.MaxValue << bitCount;
+ }
+ return unchecked((long)raw);
+ }
+
+ /// <summary>Reads whole bytes. Requires the reader to be byte aligned.</summary>
+ public byte[] ReadBytes(int byteCount)
+ {
+ if (byteCount < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(byteCount));
+ }
+ if (byteCount * 8 > Remaining)
+ {
+ throw new ParseException(
+ $"Not enough data: tried to read {byteCount} bytes, {Remaining / 8} available.");
+ }
+
+ var result = new byte[byteCount];
+ if ((_bitPosition & 7) == 0)
+ {
+ Array.Copy(_data, _bitPosition >> 3, result, 0, byteCount);
+ _bitPosition += byteCount * 8;
+ }
+ else
+ {
+ for (var i = 0; i < byteCount; i++)
+ {
+ result[i] = (byte)ReadBits(8);
+ }
+ }
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/plc4net/spi/generation/BitWriter.cs b/plc4net/spi/generation/BitWriter.cs
new file mode 100644
index 0000000000..809058ffc9
--- /dev/null
+++ b/plc4net/spi/generation/BitWriter.cs
@@ -0,0 +1,93 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+using System;
+using System.Collections.Generic;
+
+namespace org.apache.plc4net.spi.generation
+{
+ /// <summary>
+ /// Writes big-endian (MSB-first) bit fields into a growing byte buffer.
+ /// Mirror image of <see cref="BitReader"/>; see that type for why the
+ /// third-party bit codec was dropped.
+ /// </summary>
+ public class BitWriter
+ {
+ private readonly List<byte> _data = new List<byte>();
+ private int _bitPosition;
+
+ /// <summary>Number of bits written so far.</summary>
+ public int Length => _bitPosition;
+
+ public void WriteBool(bool value)
+ {
+ WriteBits(value ? 1UL : 0UL, 1);
+ }
+
+ /// <summary>
+ /// Writes the low <paramref name="bitCount"/> bits of <paramref name="value"/>,
+ /// most significant bit first.
+ /// </summary>
+ public void WriteBits(ulong value, int bitCount)
+ {
+ if (bitCount < 0 || bitCount > 64)
+ {
+ throw new ArgumentOutOfRangeException(
+ nameof(bitCount), bitCount, "Can write between 0 and 64 bits at a time.");
+ }
+
+ for (var i = bitCount - 1; i >= 0; i--)
+ {
+ var bit = (value >> i) & 1;
+
+ if ((_bitPosition & 7) == 0)
+ {
+ _data.Add(0);
+ }
+ if (bit != 0)
+ {
+ var byteIndex = _bitPosition >> 3;
+ var bitOffset = 7 - (_bitPosition & 7);
+ _data[byteIndex] |= (byte)(1 << bitOffset);
+ }
+ _bitPosition++;
+ }
+ }
+
+ public void WriteBytes(byte[] value)
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+ foreach (var b in value)
+ {
+ WriteBits(b, 8);
+ }
+ }
+
+ /// <summary>
+ /// The bytes written so far. A trailing partial byte is zero padded.
+ /// </summary>
+ public byte[] GetBytes()
+ {
+ return _data.ToArray();
+ }
+ }
+}
\ No newline at end of file
diff --git a/plc4net/spi/spi/generation/ByteOrder.cs b/plc4net/spi/generation/ByteOrder.cs
similarity index 99%
rename from plc4net/spi/spi/generation/ByteOrder.cs
rename to plc4net/spi/generation/ByteOrder.cs
index 1f65cd93ca..9295914023 100644
--- a/plc4net/spi/spi/generation/ByteOrder.cs
+++ b/plc4net/spi/generation/ByteOrder.cs
@@ -1,4 +1,4 @@
-/*
+/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
diff --git a/plc4net/spi/spi/generation/EvaluationHelper.cs b/plc4net/spi/generation/EvaluationHelper.cs
similarity index 90%
rename from plc4net/spi/spi/generation/EvaluationHelper.cs
rename to plc4net/spi/generation/EvaluationHelper.cs
index 30e3c78e4d..de9ae414fd 100644
--- a/plc4net/spi/spi/generation/EvaluationHelper.cs
+++ b/plc4net/spi/generation/EvaluationHelper.cs
@@ -1,4 +1,4 @@
-/*
+/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
@@ -51,23 +51,28 @@ namespace org.apache.plc4net.spi.generation
return false;
}*/
- public static bool notEquals(Object val1, Object val2) {
+ public static bool notEquals(Object val1, Object val2)
+ {
return true;
}
- public static bool greater(Object val1, Object val2) {
+ public static bool greater(Object val1, Object val2)
+ {
return true;
}
- public static bool greaterEquals(Object val1, Object val2) {
+ public static bool greaterEquals(Object val1, Object val2)
+ {
return true;
}
- public static bool smaller(Object val1, Object val2) {
+ public static bool smaller(Object val1, Object val2)
+ {
return true;
}
- public static bool smallerEquals(Object val1, Object val2) {
+ public static bool smallerEquals(Object val1, Object val2)
+ {
return true;
}
}
diff --git a/plc4net/spi/spi/model/values/PlcBitString.cs b/plc4net/spi/generation/ParseException.cs
similarity index 58%
copy from plc4net/spi/spi/model/values/PlcBitString.cs
copy to plc4net/spi/generation/ParseException.cs
index 42beac3bd3..33ab1b8792 100644
--- a/plc4net/spi/spi/model/values/PlcBitString.cs
+++ b/plc4net/spi/generation/ParseException.cs
@@ -17,29 +17,30 @@
// under the License.
//
-namespace org.apache.plc4net.spi.model.values
+using System;
+
+namespace org.apache.plc4net.spi.generation
{
- public class PlcBitString : PlcValueAdapter
+ /// <summary>
+ /// Raised when a byte stream cannot be interpreted as the expected message.
+ /// </summary>
+ /// <remarks>
+ /// This used to be an empty class that did not derive from Exception, so it
+ /// could not be thrown or caught and no parsing error was ever reportable.
+ /// </remarks>
+ public class ParseException : Exception
{
- public PlcBitString()
- {
- }
-
- public PlcBitString(byte value)
+ public ParseException()
{
}
- public PlcBitString(ushort value)
+ public ParseException(string message) : base(message)
{
}
- public PlcBitString(uint value)
+ public ParseException(string message, Exception innerException)
+ : base(message, innerException)
{
}
-
- public PlcBitString(ulong value)
- {
- }
-
}
}
\ No newline at end of file
diff --git a/plc4net/spi/generation/ReadBuffer.cs b/plc4net/spi/generation/ReadBuffer.cs
new file mode 100644
index 0000000000..092708a163
--- /dev/null
+++ b/plc4net/spi/generation/ReadBuffer.cs
@@ -0,0 +1,222 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+using System;
+using System.Text;
+
+namespace org.apache.plc4net.spi.generation
+{
+
+ public class ReadBuffer
+ {
+ private readonly byte[] _data;
+ private readonly BitReader _reader;
+
+ public ReadBuffer(byte[] data)
+ {
+ this._data = data;
+ _reader = new BitReader(data);
+ }
+
+ public void Reset()
+ {
+ _reader.Position = 0;
+ }
+
+ public int GetPos()
+ {
+ return _reader.Position;
+ }
+
+ public byte[] GetBytes()
+ {
+ return _data;
+ }
+
+ public int GetTotalBytes()
+ {
+ return _data.Length;
+ }
+
+ public bool HasMore(int bitLength)
+ {
+ // A negative bit length is nonsensical and should not silently return true
+ // just because the comparison below would hold for any non-negative remaining.
+ if (bitLength < 0)
+ {
+ return false;
+ }
+ // Inclusive: a buffer with exactly bitLength bits left can still serve
+ // a read of that size.
+ return bitLength <= _reader.Remaining;
+ }
+
+ public byte PeekByte(int offset)
+ {
+ return _data[(_reader.Position / 8) + offset];
+ }
+
+ public bool ReadBit(String logicalName)
+ {
+ return _reader.ReadBool();
+ }
+
+ public byte ReadByte(String logicalName, int bitLength)
+ {
+ if ((bitLength < 0) || (bitLength > 8))
+ {
+ throw new ArgumentOutOfRangeException(nameof(bitLength));
+ }
+ return (byte)_reader.ReadBits(bitLength);
+ }
+
+ public ushort ReadUshort(String logicalName, int bitLength)
+ {
+ if ((bitLength < 0) || (bitLength > 16))
+ {
+ throw new ArgumentOutOfRangeException(nameof(bitLength));
+ }
+ return (ushort)_reader.ReadBits(bitLength);
+ }
+
+ public uint ReadUint(String logicalName, int bitLength)
+ {
+ if ((bitLength < 0) || (bitLength > 32))
+ {
+ throw new ArgumentOutOfRangeException(nameof(bitLength));
+ }
+ return (uint)_reader.ReadBits(bitLength);
+ }
+
+ public ulong ReadUlong(String logicalName, int bitLength)
+ {
+ if ((bitLength < 0) || (bitLength > 64))
+ {
+ throw new ArgumentOutOfRangeException(nameof(bitLength));
+ }
+ // The previous implementation read (bitLength - 32) bits and then a
+ // further bitLength bits, consuming 2*bitLength-32 bits instead of
+ // bitLength, so it never round-tripped against WriteBuffer.WriteUlong.
+ return _reader.ReadBits(bitLength);
+ }
+
+ public sbyte ReadSbyte(String logicalName, int bitLength)
+ {
+ if ((bitLength < 0) || (bitLength > 8))
+ {
+ throw new ArgumentOutOfRangeException(nameof(bitLength));
+ }
+ return (sbyte)_reader.ReadSignedBits(bitLength);
+ }
+
+ public short ReadShort(String logicalName, int bitLength)
+ {
+ if ((bitLength < 0) || (bitLength > 16))
+ {
+ throw new ArgumentOutOfRangeException(nameof(bitLength));
+ }
+ return (short)_reader.ReadSignedBits(bitLength);
+ }
+
+ public int ReadInt(String logicalName, int bitLength)
+ {
+ if ((bitLength < 0) || (bitLength > 32))
+ {
+ throw new ArgumentOutOfRangeException(nameof(bitLength));
+ }
+ return (int)_reader.ReadSignedBits(bitLength);
+ }
+
+ public long ReadLong(String logicalName, int bitLength)
+ {
+ if ((bitLength < 0) || (bitLength > 64))
+ {
+ throw new ArgumentOutOfRangeException(nameof(bitLength));
+ }
+ // Same over-read as ReadUlong had.
+ return _reader.ReadSignedBits(bitLength);
+ }
+
+ public float ReadFloat(String logicalName, int bitLength)
+ {
+ if (bitLength == 32)
+ {
+ // Reinterpret the 32 raw bits as IEEE-754. Going via
+ // BitConverter.GetBytes would reorder them on a little-endian host.
+ return BitConverter.Int32BitsToSingle((int)_reader.ReadBits(32));
+ }
+ // KNX DPT 9.x: a 16 bit fixed-point form, not IEEE-754 half precision.
+ if (bitLength == 16)
+ {
+ var sign = _reader.ReadBool();
+ var exp = (int)_reader.ReadBits(4);
+ var mantissa = (int)_reader.ReadBits(11);
+ if (sign)
+ {
+ // Negative values are stored as a two's complement mantissa.
+ mantissa -= 2048;
+ }
+ return (float)(0.01 * mantissa * Math.Pow(2, exp));
+ }
+ throw new NotImplementedException("This encoding is currently not supported");
+ }
+
+ public double ReadDouble(String logicalName, int bitLength)
+ {
+ if (bitLength == 32)
+ {
+ // Was BitConverter.ToDouble over a 4 byte array, which always threw.
+ return ReadFloat(logicalName, 32);
+ }
+ if (bitLength == 64)
+ {
+ return BitConverter.Int64BitsToDouble((long)_reader.ReadBits(64));
+ }
+ throw new NotImplementedException("This encoding is currently not supported");
+ }
+
+ public string ReadString(String logicalName, int bitLength, Encoding encoding)
+ {
+ if (encoding == null)
+ {
+ throw new ArgumentNullException(nameof(encoding));
+ }
+ if (bitLength < 0 || bitLength % 8 != 0)
+ {
+ throw new ArgumentOutOfRangeException(
+ nameof(bitLength), bitLength, "String lengths must be a whole number of bytes.");
+ }
+
+ var raw = _reader.ReadBytes(bitLength / 8);
+ // PLC strings are commonly padded to a fixed field width with NULs.
+ return encoding.GetString(raw).TrimEnd('\0');
+ }
+
+ public byte[] ReadByteArray(String logicalName, int bitLength)
+ {
+ if (bitLength < 0 || bitLength % 8 != 0)
+ {
+ throw new ArgumentOutOfRangeException(
+ nameof(bitLength), bitLength, "Byte arrays must be a whole number of bytes.");
+ }
+ return _reader.ReadBytes(bitLength / 8);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/plc4net/spi/generation/WriteBuffer.cs b/plc4net/spi/generation/WriteBuffer.cs
new file mode 100644
index 0000000000..e7dff34b53
--- /dev/null
+++ b/plc4net/spi/generation/WriteBuffer.cs
@@ -0,0 +1,236 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+using System;
+using System.Text;
+
+namespace org.apache.plc4net.spi.generation
+{
+ public class WriteBuffer
+ {
+ private readonly BitWriter _writer;
+
+ public WriteBuffer()
+ {
+ _writer = new BitWriter();
+ }
+
+ public int GetPos()
+ {
+ return _writer.Length;
+ }
+
+ public byte[] GetBytes()
+ {
+ return _writer.GetBytes();
+ }
+
+ public int GetTotalBytes()
+ {
+ return _writer.Length / 8 + (_writer.Length % 8 != 0 ? 1 : 0);
+ }
+
+ public void WriteBit(String logicalName, bool value)
+ {
+ _writer.WriteBool(value);
+ }
+
+ public void WriteByte(String logicalName, int bitLength, byte value)
+ {
+ if ((bitLength < 0) || (bitLength > 8))
+ {
+ throw new ArgumentOutOfRangeException(nameof(bitLength));
+ }
+ _writer.WriteBits(value, bitLength);
+ }
+
+ public void WriteUshort(String logicalName, int bitLength, ushort value)
+ {
+ if ((bitLength < 0) || (bitLength > 16))
+ {
+ throw new ArgumentOutOfRangeException(nameof(bitLength));
+ }
+ _writer.WriteBits(value, bitLength);
+ }
+
+ public void WriteUint(String logicalName, int bitLength, uint value)
+ {
+ if ((bitLength < 0) || (bitLength > 32))
+ {
+ throw new ArgumentOutOfRangeException(nameof(bitLength));
+ }
+ _writer.WriteBits(value, bitLength);
+ }
+
+ public void WriteUlong(String logicalName, int bitLength, ulong value)
+ {
+ if ((bitLength < 0) || (bitLength > 64))
+ {
+ throw new ArgumentOutOfRangeException(nameof(bitLength));
+ }
+ _writer.WriteBits(value, bitLength);
+ }
+
+ public void WriteSbyte(String logicalName, int bitLength, sbyte value)
+ {
+ if ((bitLength < 0) || (bitLength > 8))
+ {
+ throw new ArgumentOutOfRangeException(nameof(bitLength));
+ }
+ _writer.WriteBits(unchecked((ulong)value), bitLength);
+ }
+
+ public void WriteShort(String logicalName, int bitLength, short value)
+ {
+ if ((bitLength < 0) || (bitLength > 16))
+ {
+ throw new ArgumentOutOfRangeException(nameof(bitLength));
+ }
+ _writer.WriteBits(unchecked((ulong)value), bitLength);
+ }
+
+ public void WriteInt(String logicalName, int bitLength, int value)
+ {
+ if ((bitLength < 0) || (bitLength > 32))
+ {
+ throw new ArgumentOutOfRangeException(nameof(bitLength));
+ }
+ _writer.WriteBits(unchecked((ulong)value), bitLength);
+ }
+
+ public void WriteLong(String logicalName, int bitLength, long value)
+ {
+ if ((bitLength < 0) || (bitLength > 64))
+ {
+ throw new ArgumentOutOfRangeException(nameof(bitLength));
+ }
+ _writer.WriteBits(unchecked((ulong)value), bitLength);
+ }
+
+ public void WriteFloat(String logicalName, int bitLength, float value)
+ {
+ if (bitLength == 32)
+ {
+ // The previous version called WriteByte(8, bytes[i]) against a
+ // (value, bitLength) signature, so it wrote the constant 8 with a
+ // bit width taken from the data.
+ _writer.WriteBits(unchecked((uint)BitConverter.SingleToInt32Bits(value)), 32);
+ return;
+ }
+ if (bitLength == 16)
+ {
+ // KNX DPT 9.x fixed point. The previous version had an empty branch
+ // here and silently emitted nothing.
+ WriteKnxFloat16(value);
+ return;
+ }
+ throw new NotImplementedException("This encoding is currently not supported");
+ }
+
+ /// <summary>
+ /// Encodes a float in the KNX 16 bit fixed-point form: sign, 4 bit exponent,
+ /// 11 bit two's complement mantissa, where value = 0.01 * mantissa * 2^exp.
+ /// </summary>
+ private void WriteKnxFloat16(float value)
+ {
+ var scaled = value / 0.01;
+ var exp = 0;
+ while (scaled < -2048 || scaled > 2047)
+ {
+ scaled /= 2;
+ exp++;
+ if (exp > 15)
+ {
+ throw new ArgumentOutOfRangeException(
+ nameof(value), value, "Value cannot be represented as a KNX 16 bit float.");
+ }
+ }
+
+ var mantissa = (int)Math.Round(scaled);
+ var sign = mantissa < 0;
+ if (sign)
+ {
+ mantissa += 2048;
+ }
+
+ _writer.WriteBool(sign);
+ _writer.WriteBits((ulong)exp, 4);
+ _writer.WriteBits((ulong)(mantissa & 0x7FF), 11);
+ }
+
+ public void WriteDouble(String logicalName, int bitLength, double value)
+ {
+ if (bitLength == 32)
+ {
+ WriteFloat(logicalName, 32, (float)value);
+ return;
+ }
+ if (bitLength == 64)
+ {
+ _writer.WriteBits(unchecked((ulong)BitConverter.DoubleToInt64Bits(value)), 64);
+ return;
+ }
+ throw new NotImplementedException("This encoding is currently not supported");
+ }
+
+ public void WriteString(String logicalName, int bitLength, string encoding, string value)
+ {
+ if (bitLength < 0 || bitLength % 8 != 0)
+ {
+ throw new ArgumentOutOfRangeException(
+ nameof(bitLength), bitLength, "String lengths must be a whole number of bytes.");
+ }
+
+ var raw = ResolveEncoding(encoding).GetBytes(value ?? string.Empty);
+ var fieldBytes = bitLength / 8;
+
+ // Truncate or NUL-pad to the declared field width.
+ for (var i = 0; i < fieldBytes; i++)
+ {
+ _writer.WriteBits(i < raw.Length ? raw[i] : (byte)0, 8);
+ }
+ }
+
+ public void WriteByteArray(String logicalName, byte[] value)
+ {
+ _writer.WriteBytes(value);
+ }
+
+ private static Encoding ResolveEncoding(string encoding)
+ {
+ // mspec spells these without the separator, e.g. "UTF8" rather than "UTF-8".
+ switch ((encoding ?? "UTF8").Replace("-", "").ToUpperInvariant())
+ {
+ case "":
+ case "UTF8":
+ return Encoding.UTF8;
+ case "ASCII":
+ return Encoding.ASCII;
+ case "UTF16":
+ case "UTF16LE":
+ return Encoding.Unicode;
+ case "UTF16BE":
+ return Encoding.BigEndianUnicode;
+ default:
+ throw new NotImplementedException($"Unsupported string encoding '{encoding}'");
+ }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/plc4net/spi/spi/model/values/PlcWORD.cs b/plc4net/spi/model/values/PlcBOOL.cs
similarity index 56%
rename from plc4net/spi/spi/model/values/PlcWORD.cs
rename to plc4net/spi/model/values/PlcBOOL.cs
index 82ae20fec0..674dbe855c 100644
--- a/plc4net/spi/spi/model/values/PlcWORD.cs
+++ b/plc4net/spi/model/values/PlcBOOL.cs
@@ -21,62 +21,68 @@ using org.apache.plc4net.api.value;
namespace org.apache.plc4net.spi.model.values
{
- public class PlcWORD : PlcBitString
+ public class PlcBOOL : PlcSimpleValueAdapter
{
- private ushort value;
-
- public PlcWORD(ushort value)
+ private bool value;
+
+ public PlcBOOL(bool value)
{
this.value = value;
}
-
- public new int GetBoolLength()
+
+ public override bool IsBool()
{
- return 16;
+ return true;
}
-
- public new bool GetBoolAt(int index)
+
+ public override bool GetBool()
{
- if (index > 15)
- {
- return false;
- }
+ return value;
+ }
- return ((value >> index) & 1) == 1;
+ public override bool[] GetBoolArray()
+ {
+ return new[] { value };
}
- public new bool[] GetBoolArray()
+ // A BOOL is also 1/0 as a number and "true"/"false" as text (matching plc4j).
+
+ public override bool IsByte() => true;
+ public override byte GetByte() => (byte)(value ? 1 : 0);
+
+ public override bool IsUshort() => true;
+ public override ushort GetUshort() => (ushort)(value ? 1 : 0);
+
+ public override bool IsInt() => true;
+ public override int GetInt() => value ? 1 : 0;
+
+ public override bool IsString()
+ {
+ return true;
+ }
+
+ public override string GetString()
{
- return new[]
- {
- GetBoolAt(0), GetBoolAt(1),
- GetBoolAt(2), GetBoolAt(3),
- GetBoolAt(4), GetBoolAt(5),
- GetBoolAt(6), GetBoolAt(7),
- GetBoolAt(8), GetBoolAt(9),
- GetBoolAt(10), GetBoolAt(11),
- GetBoolAt(12), GetBoolAt(13),
- GetBoolAt(14), GetBoolAt(15)
- };
+ return value ? "true" : "false";
}
- protected bool Equals(PlcWORD other)
+ protected bool Equals(PlcBOOL other)
{
return value == other.value;
}
- public override bool Equals(object obj)
+ public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
- return Equals((PlcWORD) obj);
+ return Equals((PlcBOOL)obj);
}
public override int GetHashCode()
{
return value.GetHashCode();
}
-
+
}
}
\ No newline at end of file
diff --git a/plc4net/spi/spi/model/values/PlcNULL.cs b/plc4net/spi/model/values/PlcBYTE.cs
similarity index 76%
copy from plc4net/spi/spi/model/values/PlcNULL.cs
copy to plc4net/spi/model/values/PlcBYTE.cs
index 4ec8ec3547..1c572f4367 100644
--- a/plc4net/spi/spi/model/values/PlcNULL.cs
+++ b/plc4net/spi/model/values/PlcBYTE.cs
@@ -17,33 +17,30 @@
// under the License.
//
-using org.apache.plc4net.api.value;
-
namespace org.apache.plc4net.spi.model.values
{
- public class PlcNULL : PlcValueAdapter
+ public class PlcBYTE : PlcBitString
{
- public PlcNULL()
+ public PlcBYTE(byte value) : base(value)
{
}
- protected bool Equals(PlcNULL other)
+ protected bool Equals(PlcBYTE other)
{
- throw new System.NotImplementedException();
+ return GetByte() == other.GetByte();
}
- public override bool Equals(object obj)
+ public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
- return Equals((PlcNULL) obj);
+ return Equals((PlcBYTE)obj);
}
public override int GetHashCode()
{
- throw new System.NotImplementedException();
+ return GetByte().GetHashCode();
}
-
}
}
\ No newline at end of file
diff --git a/plc4net/spi/model/values/PlcBitString.cs b/plc4net/spi/model/values/PlcBitString.cs
new file mode 100644
index 0000000000..ad89abbc1c
--- /dev/null
+++ b/plc4net/spi/model/values/PlcBitString.cs
@@ -0,0 +1,188 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+using System;
+using System.Globalization;
+
+namespace org.apache.plc4net.spi.model.values
+{
+ /// <summary>
+ /// Base for the IEC bit-string values (<c>BYTE</c>, <c>WORD</c>, <c>DWORD</c>,
+ /// <c>LWORD</c>). Keeps the pattern as a right-aligned <see cref="ulong"/> and
+ /// serves it both as individual bits and as the (unsigned) integer of the
+ /// matching width, so a value survives a parse/serialize round trip and a
+ /// consumer holding it as <see cref="IPlcValue"/> can still read a number.
+ /// </summary>
+ public class PlcBitString : PlcSimpleValueAdapter
+ {
+ private readonly ulong _bits;
+ private readonly int _byteWidth;
+
+ public PlcBitString()
+ {
+ }
+
+ public PlcBitString(byte value)
+ {
+ _bits = value;
+ _byteWidth = 1;
+ }
+
+ public PlcBitString(ushort value)
+ {
+ _bits = value;
+ _byteWidth = 2;
+ }
+
+ public PlcBitString(uint value)
+ {
+ _bits = value;
+ _byteWidth = 4;
+ }
+
+ public PlcBitString(ulong value)
+ {
+ _bits = value;
+ _byteWidth = 8;
+ }
+
+ public override bool IsByte()
+ {
+ return _byteWidth == 1;
+ }
+
+ public override byte GetByte()
+ {
+ return (byte)_bits;
+ }
+
+ public override bool IsUshort()
+ {
+ return _byteWidth <= 2;
+ }
+
+ public override ushort GetUshort()
+ {
+ return (ushort)_bits;
+ }
+
+ public override bool IsUint()
+ {
+ return _byteWidth <= 4;
+ }
+
+ public override uint GetUint()
+ {
+ return (uint)_bits;
+ }
+
+ public override bool IsUlong()
+ {
+ return true;
+ }
+
+ public override ulong GetUlong()
+ {
+ return _bits;
+ }
+
+ // ── signed-integer + string views (a BYTE/WORD is also a plain number) ──
+
+ public override bool IsShort()
+ {
+ return _bits <= (ulong)short.MaxValue;
+ }
+
+ public override short GetShort()
+ {
+ if (!IsShort())
+ {
+ throw new ArgumentOutOfRangeException(nameof(GetShort),
+ $"bit-string value {_bits} does not fit in a short");
+ }
+ return (short)_bits;
+ }
+
+ public override bool IsInt()
+ {
+ return _bits <= int.MaxValue;
+ }
+
+ public override int GetInt()
+ {
+ if (!IsInt())
+ {
+ throw new ArgumentOutOfRangeException(nameof(GetInt),
+ $"bit-string value {_bits} does not fit in an int");
+ }
+ return (int)_bits;
+ }
+
+ public override bool IsLong()
+ {
+ return _bits <= long.MaxValue;
+ }
+
+ public override long GetLong()
+ {
+ if (!IsLong())
+ {
+ throw new ArgumentOutOfRangeException(nameof(GetLong),
+ $"bit-string value {_bits} does not fit in a long");
+ }
+ return (long)_bits;
+ }
+
+ public override bool IsString()
+ {
+ return true;
+ }
+
+ public override string GetString()
+ {
+ return _bits.ToString(CultureInfo.InvariantCulture);
+ }
+
+ public override int GetBoolLength()
+ {
+ return _byteWidth * 8;
+ }
+
+ public override bool GetBoolAt(int index)
+ {
+ if (index < 0 || index >= _byteWidth * 8)
+ {
+ return false;
+ }
+
+ return ((_bits >> index) & 1UL) == 1UL;
+ }
+
+ public override bool[] GetBoolArray()
+ {
+ var array = new bool[_byteWidth * 8];
+ for (var i = 0; i < array.Length; i++)
+ {
+ array[i] = GetBoolAt(i);
+ }
+
+ return array;
+ }
+ }
+}
\ No newline at end of file
diff --git a/plc4net/spi/spi/model/values/PlcCHAR.cs b/plc4net/spi/model/values/PlcCHAR.cs
similarity index 84%
rename from plc4net/spi/spi/model/values/PlcCHAR.cs
rename to plc4net/spi/model/values/PlcCHAR.cs
index 6e1194392b..b80bd57181 100644
--- a/plc4net/spi/spi/model/values/PlcCHAR.cs
+++ b/plc4net/spi/model/values/PlcCHAR.cs
@@ -24,29 +24,39 @@ namespace org.apache.plc4net.spi.model.values
public class PlcCHAR : PlcSimpleValueAdapter
{
private char value;
-
+
public PlcCHAR(char value)
{
this.value = value;
}
+ public override bool IsString()
+ {
+ return true;
+ }
+
+ public override string GetString()
+ {
+ return value.ToString();
+ }
+
protected bool Equals(PlcCHAR other)
{
return value == other.value;
}
- public override bool Equals(object obj)
+ public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
- return Equals((PlcCHAR) obj);
+ return Equals((PlcCHAR)obj);
}
public override int GetHashCode()
{
return value.GetHashCode();
}
-
+
}
}
\ No newline at end of file
diff --git a/plc4net/spi/spi/model/values/PlcDATE.cs b/plc4net/spi/model/values/PlcDATE.cs
similarity index 57%
rename from plc4net/spi/spi/model/values/PlcDATE.cs
rename to plc4net/spi/model/values/PlcDATE.cs
index 008e6839d7..91cd520ce0 100644
--- a/plc4net/spi/spi/model/values/PlcDATE.cs
+++ b/plc4net/spi/model/values/PlcDATE.cs
@@ -18,36 +18,65 @@
//
using System;
-using org.apache.plc4net.api.value;
namespace org.apache.plc4net.spi.model.values
{
- public class PlcDATE : PlcValueAdapter
+ /// <summary>IEC 61131 <c>DATE</c> - a calendar date, no time of day.</summary>
+ public class PlcDATE : PlcSimpleValueAdapter
{
- private DateTime value;
-
- public PlcDATE(DateTime value)
+ private static readonly DateOnly Epoch = new DateOnly(1970, 1, 1);
+
+ private readonly DateOnly value;
+
+ public PlcDATE(DateOnly value)
{
this.value = value;
}
+ public static PlcDATE OfDaysSinceEpoch(int daysSinceEpoch) =>
+ new PlcDATE(Epoch.AddDays(daysSinceEpoch));
+
+ public int GetDaysSinceEpoch()
+ {
+ return value.DayNumber - Epoch.DayNumber;
+ }
+
+ public override bool IsDate()
+ {
+ return true;
+ }
+
+ public override DateOnly GetDate()
+ {
+ return value;
+ }
+
+ public override bool IsDateTime()
+ {
+ return true;
+ }
+
+ public override DateTime GetDateTime()
+ {
+ return value.ToDateTime(TimeOnly.MinValue);
+ }
+
protected bool Equals(PlcDATE other)
{
return value.Equals(other.value);
}
- public override bool Equals(object obj)
+ public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
- return Equals((PlcDATE) obj);
+ return Equals((PlcDATE)obj);
}
public override int GetHashCode()
{
return value.GetHashCode();
}
-
}
}
\ No newline at end of file
diff --git a/plc4net/spi/model/values/PlcDATE_AND_LTIME.cs b/plc4net/spi/model/values/PlcDATE_AND_LTIME.cs
new file mode 100644
index 0000000000..98bfc27f4e
--- /dev/null
+++ b/plc4net/spi/model/values/PlcDATE_AND_LTIME.cs
@@ -0,0 +1,127 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+using System;
+using org.apache.plc4net.spi.generation;
+
+namespace org.apache.plc4net.spi.model.values
+{
+ /// <summary>
+ /// IEC 61131 <c>DATE_AND_LTIME</c> (S7 <c>DTL</c>) - a full instant with
+ /// nanosecond-of-second precision. The whole-second part is a
+ /// <see cref="DateTime"/>; the sub-second part is kept exactly as
+ /// <see cref="GetNanosecondsOfSecond"/> so both the segmented (DTL) and the
+ /// nanoseconds-since-epoch wire forms round-trip.
+ /// </summary>
+ public class PlcDATE_AND_LTIME : PlcSimpleValueAdapter
+ {
+ private const long NanosPerSecond = 1_000_000_000L;
+
+ private readonly DateTime wholeSeconds;
+ private readonly uint nanosecondsOfSecond;
+
+ public PlcDATE_AND_LTIME(DateTime dateTime, uint nanosecondsOfSecond)
+ {
+ // Split the instant into whole seconds + a nanoseconds-of-second so
+ // both wire forms round-trip; any sub-second part carried by the
+ // DateTime is folded in rather than dropped. Kind is normalised to
+ // Unspecified to match plc4j's zoneless LocalDateTime semantics -
+ // otherwise the two factories below disagree (UnixEpoch is Utc,
+ // `new DateTime(y,m,d,…)` is Unspecified).
+ if (nanosecondsOfSecond > 999_999_999)
+ throw new ArgumentOutOfRangeException(nameof(nanosecondsOfSecond),
+ $"nanosecondsOfSecond must be in [0, 999_999_999], got {nanosecondsOfSecond}.");
+ var subSecondTicks = dateTime.Ticks % TimeSpan.TicksPerSecond;
+ var nanoFromTicks = (uint)(subSecondTicks * 100);
+ if (nanosecondsOfSecond + nanoFromTicks > 999_999_999)
+ throw new ArgumentOutOfRangeException(nameof(nanosecondsOfSecond),
+ $"nanosecondsOfSecond ({nanosecondsOfSecond}) + subsecond ticks ({nanoFromTicks}) exceeds 999_999_999.");
+ wholeSeconds = DateTime.SpecifyKind(
+ new DateTime(dateTime.Ticks - subSecondTicks), DateTimeKind.Unspecified);
+ this.nanosecondsOfSecond = nanosecondsOfSecond + nanoFromTicks;
+ }
+
+ public static PlcDATE_AND_LTIME OfNanosecondsSinceEpoch(ulong nanosecondsSinceEpoch)
+ {
+ var seconds = (long)(nanosecondsSinceEpoch / NanosPerSecond);
+ var nanos = (uint)(nanosecondsSinceEpoch % NanosPerSecond);
+ return new PlcDATE_AND_LTIME(DateTime.UnixEpoch.AddSeconds(seconds), nanos);
+ }
+
+ public static PlcDATE_AND_LTIME OfSegments(
+ int year, int month, int day, int hour, int minutes, int seconds, long nanoseconds)
+ {
+ try
+ {
+ if (nanoseconds < 0 || nanoseconds > 999_999_999)
+ throw new ArgumentOutOfRangeException(nameof(nanoseconds),
+ $"nanoseconds must be in [0, 999_999_999], got {nanoseconds}.");
+ return new PlcDATE_AND_LTIME(
+ new DateTime(year, month == 0 ? 1 : month, day == 0 ? 1 : day, hour, minutes, seconds),
+ (uint)nanoseconds);
+ }
+ catch (ArgumentOutOfRangeException e)
+ {
+ throw new ParseException(
+ $"DATE_AND_LTIME {year:D4}-{month:D2}-{day:D2} "
+ + $"{hour:D2}:{minutes:D2}:{seconds:D2} is not a valid instant.", e);
+ }
+ }
+
+ public uint GetNanosecondsOfSecond()
+ {
+ return nanosecondsOfSecond;
+ }
+
+ public ulong GetNanosecondsSinceEpoch()
+ {
+ var seconds = (wholeSeconds.Ticks - DateTime.UnixEpoch.Ticks) / TimeSpan.TicksPerSecond;
+ return (ulong)seconds * (ulong)NanosPerSecond + nanosecondsOfSecond;
+ }
+
+ public override bool IsDateTime()
+ {
+ return true;
+ }
+
+ public override DateTime GetDateTime()
+ {
+ return wholeSeconds.AddTicks(nanosecondsOfSecond / 100);
+ }
+
+ protected bool Equals(PlcDATE_AND_LTIME other)
+ {
+ return wholeSeconds.Equals(other.wholeSeconds)
+ && nanosecondsOfSecond == other.nanosecondsOfSecond;
+ }
+
+ public override bool Equals(object? obj)
+ {
+ if (ReferenceEquals(null, obj)) return false;
+ if (ReferenceEquals(this, obj)) return true;
+ if (obj.GetType() != this.GetType()) return false;
+ return Equals((PlcDATE_AND_LTIME)obj);
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(wholeSeconds, nanosecondsOfSecond);
+ }
+ }
+}
\ No newline at end of file
diff --git a/plc4net/spi/spi/model/values/PlcDATE_AND_TIME.cs b/plc4net/spi/model/values/PlcDATE_AND_TIME.cs
similarity index 53%
copy from plc4net/spi/spi/model/values/PlcDATE_AND_TIME.cs
copy to plc4net/spi/model/values/PlcDATE_AND_TIME.cs
index c1a478ed2a..2594ca7522 100644
--- a/plc4net/spi/spi/model/values/PlcDATE_AND_TIME.cs
+++ b/plc4net/spi/model/values/PlcDATE_AND_TIME.cs
@@ -18,35 +18,64 @@
//
using System;
-using org.apache.plc4net.api.value;
+using org.apache.plc4net.spi.generation;
namespace org.apache.plc4net.spi.model.values
{
- public class PlcDATE_AND_TIME : PlcValueAdapter
+ /// <summary>IEC 61131 <c>DATE_AND_TIME</c> (S7 <c>DT</c>) - a full instant,
+ /// millisecond resolution.</summary>
+ public class PlcDATE_AND_TIME : PlcSimpleValueAdapter
{
- private DateTime value;
+ private readonly DateTime value;
+
public PlcDATE_AND_TIME(DateTime value)
{
this.value = value;
}
+ public static PlcDATE_AND_TIME OfSegments(
+ int year, int month, int day, int hour, int minutes, int seconds, int nanoseconds)
+ {
+ try
+ {
+ return new PlcDATE_AND_TIME(new DateTime(
+ year, month == 0 ? 1 : month, day == 0 ? 1 : day, hour, minutes, seconds)
+ .AddTicks(nanoseconds / 100));
+ }
+ catch (ArgumentOutOfRangeException e)
+ {
+ throw new ParseException(
+ $"DATE_AND_TIME {year:D4}-{month:D2}-{day:D2} "
+ + $"{hour:D2}:{minutes:D2}:{seconds:D2} is not a valid instant.", e);
+ }
+ }
+
+ public override bool IsDateTime()
+ {
+ return true;
+ }
+
+ public override DateTime GetDateTime()
+ {
+ return value;
+ }
+
protected bool Equals(PlcDATE_AND_TIME other)
{
return value.Equals(other.value);
}
- public override bool Equals(object obj)
+ public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
- return Equals((PlcDATE_AND_TIME) obj);
+ return Equals((PlcDATE_AND_TIME)obj);
}
public override int GetHashCode()
{
return value.GetHashCode();
}
-
}
}
\ No newline at end of file
diff --git a/plc4net/spi/spi/model/values/PlcDINT.cs b/plc4net/spi/model/values/PlcDINT.cs
similarity index 100%
rename from plc4net/spi/spi/model/values/PlcDINT.cs
rename to plc4net/spi/model/values/PlcDINT.cs
diff --git a/plc4net/spi/spi/model/values/PlcRawByteArray.cs b/plc4net/spi/model/values/PlcDWORD.cs
similarity index 74%
rename from plc4net/spi/spi/model/values/PlcRawByteArray.cs
rename to plc4net/spi/model/values/PlcDWORD.cs
index 3585bfccd7..322b167fa2 100644
--- a/plc4net/spi/spi/model/values/PlcRawByteArray.cs
+++ b/plc4net/spi/model/values/PlcDWORD.cs
@@ -19,32 +19,28 @@
namespace org.apache.plc4net.spi.model.values
{
- public class PlcRawByteArray : PlcValueAdapter
+ public class PlcDWORD : PlcBitString
{
- private byte[] value;
-
- public PlcRawByteArray(byte[] value)
+ public PlcDWORD(uint value) : base(value)
{
- this.value = value;
}
-
- protected bool Equals(PlcRawByteArray other)
+
+ protected bool Equals(PlcDWORD other)
{
- return value == other.value;
+ return GetUint() == other.GetUint();
}
- public override bool Equals(object obj)
+ public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
- return value.Equals(obj as byte[]);
+ return Equals((PlcDWORD)obj);
}
public override int GetHashCode()
{
- return value.GetHashCode();
+ return GetUint().GetHashCode();
}
-
}
}
\ No newline at end of file
diff --git a/plc4net/spi/spi/model/values/PlcINT.cs b/plc4net/spi/model/values/PlcINT.cs
similarity index 100%
rename from plc4net/spi/spi/model/values/PlcINT.cs
rename to plc4net/spi/model/values/PlcINT.cs
diff --git a/plc4net/spi/spi/model/values/PlcLINT.cs b/plc4net/spi/model/values/PlcLINT.cs
similarity index 100%
rename from plc4net/spi/spi/model/values/PlcLINT.cs
rename to plc4net/spi/model/values/PlcLINT.cs
diff --git a/plc4net/spi/spi/model/values/PlcLREAL.cs b/plc4net/spi/model/values/PlcLREAL.cs
similarity index 100%
rename from plc4net/spi/spi/model/values/PlcLREAL.cs
rename to plc4net/spi/model/values/PlcLREAL.cs
diff --git a/plc4net/spi/spi/model/values/PlcLTIME.cs b/plc4net/spi/model/values/PlcLTIME.cs
similarity index 51%
rename from plc4net/spi/spi/model/values/PlcLTIME.cs
rename to plc4net/spi/model/values/PlcLTIME.cs
index 7a425d3abd..504dceb660 100644
--- a/plc4net/spi/spi/model/values/PlcLTIME.cs
+++ b/plc4net/spi/model/values/PlcLTIME.cs
@@ -18,36 +18,60 @@
//
using System;
-using org.apache.plc4net.api.value;
namespace org.apache.plc4net.spi.model.values
{
+ /// <summary>
+ /// IEC 61131 <c>LTIME</c> - a duration in nanoseconds. The raw
+ /// <see cref="GetNanoseconds"/> is exact; <see cref="GetDuration"/> rounds
+ /// to the 100 ns <see cref="TimeSpan"/> tick.
+ /// </summary>
public class PlcLTIME : PlcSimpleValueAdapter
{
- private DateTime value;
-
- public PlcLTIME(DateTime value)
+ private readonly ulong nanoseconds;
+
+ public PlcLTIME(ulong nanoseconds)
+ {
+ this.nanoseconds = nanoseconds;
+ }
+
+ public static PlcLTIME OfNanoseconds(ulong nanoseconds) => new PlcLTIME(nanoseconds);
+
+ public ulong GetNanoseconds()
+ {
+ return nanoseconds;
+ }
+
+ public override bool IsDuration()
+ {
+ return true;
+ }
+
+ public override TimeSpan GetDuration()
{
- this.value = value;
+ const long MaxTicks = long.MaxValue;
+ if (nanoseconds > unchecked((ulong)MaxTicks * 100))
+ throw new OverflowException(
+ $"LTIME duration {nanoseconds} ns exceeds TimeSpan.MaxValue ({MaxTicks} ticks).");
+ return TimeSpan.FromTicks((long)(nanoseconds / 100));
}
protected bool Equals(PlcLTIME other)
{
- return value.Equals(other.value);
+ return nanoseconds == other.nanoseconds;
}
- public override bool Equals(object obj)
+ public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
- return Equals((PlcLTIME) obj);
+ return Equals((PlcLTIME)obj);
}
public override int GetHashCode()
{
- return value.GetHashCode();
+ return nanoseconds.GetHashCode();
}
-
}
}
\ No newline at end of file
diff --git a/plc4net/spi/model/values/PlcLTIME_OF_DAY.cs b/plc4net/spi/model/values/PlcLTIME_OF_DAY.cs
new file mode 100644
index 0000000000..98292af2f5
--- /dev/null
+++ b/plc4net/spi/model/values/PlcLTIME_OF_DAY.cs
@@ -0,0 +1,89 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+using System;
+using org.apache.plc4net.spi.generation;
+
+namespace org.apache.plc4net.spi.model.values
+{
+ /// <summary>
+ /// IEC 61131 <c>LTIME_OF_DAY</c> - a wall-clock time in nanoseconds since
+ /// midnight. The raw <see cref="GetNanosecondsSinceMidnight"/> is exact;
+ /// <see cref="GetTime"/> rounds to the 100 ns <see cref="TimeOnly"/> tick.
+ /// </summary>
+ public class PlcLTIME_OF_DAY : PlcSimpleValueAdapter
+ {
+ private readonly ulong nanosecondsSinceMidnight;
+
+ public PlcLTIME_OF_DAY(ulong nanosecondsSinceMidnight)
+ {
+ if (nanosecondsSinceMidnight >= NanosPerDay)
+ throw new ArgumentOutOfRangeException(nameof(nanosecondsSinceMidnight),
+ $"nanosecondsSinceMidnight must be < {NanosPerDay} ns/day, got {nanosecondsSinceMidnight}.");
+ this.nanosecondsSinceMidnight = nanosecondsSinceMidnight;
+ }
+
+ private const ulong NanosPerDay = 86_400_000_000_000UL;
+
+ public static PlcLTIME_OF_DAY OfNanosecondsSinceMidnight(ulong nanosecondsSinceMidnight)
+ {
+ if (nanosecondsSinceMidnight >= NanosPerDay)
+ {
+ throw new ParseException(
+ $"LTIME_OF_DAY {nanosecondsSinceMidnight} ns is not within a 24-hour day.");
+ }
+ return new PlcLTIME_OF_DAY(nanosecondsSinceMidnight);
+ }
+
+ public ulong GetNanosecondsSinceMidnight()
+ {
+ return nanosecondsSinceMidnight;
+ }
+
+ public override bool IsTime()
+ {
+ return true;
+ }
+
+ public override TimeOnly GetTime()
+ {
+ var maxTicksAsNanos = unchecked((ulong)long.MaxValue * 100);
+ var ns = Math.Min(nanosecondsSinceMidnight, Math.Min(NanosPerDay - 1, maxTicksAsNanos));
+ return TimeOnly.FromTimeSpan(TimeSpan.FromTicks((long)(ns / 100)));
+ }
+
+ protected bool Equals(PlcLTIME_OF_DAY other)
+ {
+ return nanosecondsSinceMidnight == other.nanosecondsSinceMidnight;
+ }
+
+ public override bool Equals(object? obj)
+ {
+ if (ReferenceEquals(null, obj)) return false;
+ if (ReferenceEquals(this, obj)) return true;
+ if (obj.GetType() != this.GetType()) return false;
+ return Equals((PlcLTIME_OF_DAY)obj);
+ }
+
+ public override int GetHashCode()
+ {
+ return nanosecondsSinceMidnight.GetHashCode();
+ }
+ }
+}
\ No newline at end of file
diff --git a/plc4net/spi/spi/model/values/PlcBOOL.cs b/plc4net/spi/model/values/PlcLWORD.cs
similarity index 73%
rename from plc4net/spi/spi/model/values/PlcBOOL.cs
rename to plc4net/spi/model/values/PlcLWORD.cs
index cea3965767..a611c4a6f1 100644
--- a/plc4net/spi/spi/model/values/PlcBOOL.cs
+++ b/plc4net/spi/model/values/PlcLWORD.cs
@@ -17,36 +17,30 @@
// under the License.
//
-using org.apache.plc4net.api.value;
-
namespace org.apache.plc4net.spi.model.values
{
- public class PlcBOOL : PlcSimpleValueAdapter
+ public class PlcLWORD : PlcBitString
{
- private bool value;
-
- public PlcBOOL(bool value)
+ public PlcLWORD(ulong value) : base(value)
{
- this.value = value;
}
- protected bool Equals(PlcBOOL other)
+ protected bool Equals(PlcLWORD other)
{
- return value == other.value;
+ return GetUlong() == other.GetUlong();
}
- public override bool Equals(object obj)
+ public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
- return Equals((PlcBOOL) obj);
+ return Equals((PlcLWORD)obj);
}
public override int GetHashCode()
{
- return value.GetHashCode();
+ return GetUlong().GetHashCode();
}
-
}
}
\ No newline at end of file
diff --git a/plc4net/spi/spi/model/values/PlcPlcList.cs b/plc4net/spi/model/values/PlcList.cs
similarity index 58%
rename from plc4net/spi/spi/model/values/PlcPlcList.cs
rename to plc4net/spi/model/values/PlcList.cs
index 156ae5e360..847536bc94 100644
--- a/plc4net/spi/spi/model/values/PlcPlcList.cs
+++ b/plc4net/spi/model/values/PlcList.cs
@@ -18,35 +18,64 @@
//
using System.Collections.Generic;
+using System.Linq;
using org.apache.plc4net.api.value;
namespace org.apache.plc4net.spi.model.values
{
+ /// <summary>An ordered list of <see cref="IPlcValue"/> - a KNX group of
+ /// datapoints, a Modbus multi-register read.</summary>
public class PlcList : PlcValueAdapter
{
- private List<IPlcValue> values;
-
+ private readonly List<IPlcValue> values;
+
public PlcList(List<IPlcValue> values)
{
- this.values = values;
+ this.values = values ?? new List<IPlcValue>();
+ }
+
+ public override bool IsList()
+ {
+ return true;
+ }
+
+ public override int GetLength()
+ {
+ return values.Count;
+ }
+
+ public override IPlcValue GetIndex(int index)
+ {
+ return values[index];
+ }
+
+ public override List<IPlcValue> GetList()
+ {
+ return values;
}
protected bool Equals(PlcList other)
{
- return Equals(values, other.values);
+ return values.SequenceEqual(other.values);
}
- public override bool Equals(object obj)
+ public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
- return Equals((PlcList) obj);
+ return Equals((PlcList)obj);
}
public override int GetHashCode()
{
- return (values != null ? values.GetHashCode() : 0);
+ var hash = 17;
+ foreach (var value in values)
+ {
+ hash = hash * 31 + (value != null ? value.GetHashCode() : 0);
+ }
+
+ return hash;
}
}
}
\ No newline at end of file
diff --git a/plc4net/spi/spi/model/values/PlcDATE_AND_TIME.cs b/plc4net/spi/model/values/PlcNULL.cs
similarity index 59%
rename from plc4net/spi/spi/model/values/PlcDATE_AND_TIME.cs
rename to plc4net/spi/model/values/PlcNULL.cs
index c1a478ed2a..d238047dc0 100644
--- a/plc4net/spi/spi/model/values/PlcDATE_AND_TIME.cs
+++ b/plc4net/spi/model/values/PlcNULL.cs
@@ -17,36 +17,48 @@
// under the License.
//
-using System;
using org.apache.plc4net.api.value;
namespace org.apache.plc4net.spi.model.values
{
- public class PlcDATE_AND_TIME : PlcValueAdapter
+ /// <summary>
+ /// The absence of a value — the fall-through result of a <c>[dataIo]</c>
+ /// parse and of an unresolved lookup. All instances are equal.
+ /// </summary>
+ public class PlcNULL : PlcValueAdapter
{
- private DateTime value;
- public PlcDATE_AND_TIME(DateTime value)
+ public PlcNULL()
{
- this.value = value;
}
- protected bool Equals(PlcDATE_AND_TIME other)
+ public override bool IsNull()
{
- return value.Equals(other.value);
+ return true;
}
- public override bool Equals(object obj)
+ public override bool IsNullable()
{
- if (ReferenceEquals(null, obj)) return false;
- if (ReferenceEquals(this, obj)) return true;
- if (obj.GetType() != this.GetType()) return false;
- return Equals((PlcDATE_AND_TIME) obj);
+ return true;
+ }
+
+ public override string GetString()
+ {
+ return "null";
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return obj is PlcNULL;
}
public override int GetHashCode()
{
- return value.GetHashCode();
+ return 0;
+ }
+
+ public override string ToString()
+ {
+ return "PlcNULL";
}
-
}
}
\ No newline at end of file
diff --git a/plc4net/spi/spi/model/values/PlcREAL.cs b/plc4net/spi/model/values/PlcREAL.cs
similarity index 100%
rename from plc4net/spi/spi/model/values/PlcREAL.cs
rename to plc4net/spi/model/values/PlcREAL.cs
diff --git a/plc4net/spi/spi/model/values/PlcBYTE.cs b/plc4net/spi/model/values/PlcRawByteArray.cs
similarity index 58%
rename from plc4net/spi/spi/model/values/PlcBYTE.cs
rename to plc4net/spi/model/values/PlcRawByteArray.cs
index b96869e9d1..678bcce26a 100644
--- a/plc4net/spi/spi/model/values/PlcBYTE.cs
+++ b/plc4net/spi/model/values/PlcRawByteArray.cs
@@ -17,62 +17,54 @@
// under the License.
//
-using org.apache.plc4net.api.value;
+using System;
+using System.Linq;
namespace org.apache.plc4net.spi.model.values
{
- public class PlcBYTE : PlcBitString
+ /// <summary>An opaque run of bytes - a KNX property's raw group address, a
+ /// datapoint field with no finer structure.</summary>
+ public class PlcRawByteArray : PlcValueAdapter
{
- private byte value;
+ private readonly byte[] value;
- public PlcBYTE(byte value)
+ public PlcRawByteArray(byte[] value)
{
- this.value = value;
+ this.value = value ?? Array.Empty<byte>();
}
- public new int GetBoolLength()
+ public override byte[] GetRaw()
{
- return 8;
+ return value;
}
- public new bool GetBoolAt(int index)
+ public override int GetLength()
{
- if (index > 7)
- {
- return false;
- }
-
- return ((value >> index) & 1) == 1;
- }
-
- public new bool[] GetBoolArray()
- {
- return new[]
- {
- GetBoolAt(0), GetBoolAt(1),
- GetBoolAt(2), GetBoolAt(3),
- GetBoolAt(4), GetBoolAt(5),
- GetBoolAt(6), GetBoolAt(7)
- };
+ return value.Length;
}
- protected bool Equals(PlcBYTE other)
+ protected bool Equals(PlcRawByteArray other)
{
- return value == other.value;
+ return value.SequenceEqual(other.value);
}
- public override bool Equals(object obj)
+ public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
- return Equals((PlcBYTE) obj);
+ return Equals((PlcRawByteArray)obj);
}
public override int GetHashCode()
{
- return value.GetHashCode();
+ var hash = 17;
+ foreach (var b in value)
+ {
+ hash = hash * 31 + b;
+ }
+
+ return hash;
}
-
}
}
\ No newline at end of file
diff --git a/plc4net/spi/spi/model/values/PlcSINT.cs b/plc4net/spi/model/values/PlcSINT.cs
similarity index 100%
rename from plc4net/spi/spi/model/values/PlcSINT.cs
rename to plc4net/spi/model/values/PlcSINT.cs
diff --git a/plc4net/spi/spi/model/values/PlcSTRING.cs b/plc4net/spi/model/values/PlcSTRING.cs
similarity index 84%
rename from plc4net/spi/spi/model/values/PlcSTRING.cs
rename to plc4net/spi/model/values/PlcSTRING.cs
index 1362c86c25..00f6a4e5c2 100644
--- a/plc4net/spi/spi/model/values/PlcSTRING.cs
+++ b/plc4net/spi/model/values/PlcSTRING.cs
@@ -24,29 +24,39 @@ namespace org.apache.plc4net.spi.model.values
public class PlcSTRING : PlcSimpleValueAdapter
{
private string value;
-
+
public PlcSTRING(string value)
{
this.value = value;
}
+ public override bool IsString()
+ {
+ return true;
+ }
+
+ public override string GetString()
+ {
+ return value;
+ }
+
protected bool Equals(PlcSTRING other)
{
return value == other.value;
}
- public override bool Equals(object obj)
+ public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
- return Equals((PlcSTRING) obj);
+ return Equals((PlcSTRING)obj);
}
public override int GetHashCode()
{
return (value != null ? value.GetHashCode() : 0);
}
-
+
}
}
\ No newline at end of file
diff --git a/plc4net/spi/model/values/PlcSimpleNumericValueAdapter.cs b/plc4net/spi/model/values/PlcSimpleNumericValueAdapter.cs
new file mode 100644
index 0000000000..beb0a96687
--- /dev/null
+++ b/plc4net/spi/model/values/PlcSimpleNumericValueAdapter.cs
@@ -0,0 +1,253 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+using System;
+using System.Globalization;
+
+namespace org.apache.plc4net.spi.model.values
+{
+ public abstract class SimpleNumericValueAdapter<T> : PlcSimpleValueAdapter where T : IComparable
+ {
+ private readonly IComparable value;
+
+ protected SimpleNumericValueAdapter(IComparable value)
+ {
+ this.value = value;
+ }
+
+ /// <summary>
+ /// The boxed value, widened to decimal for range checks.
+ /// </summary>
+ /// <remarks>
+ /// Range checks cannot go through <see cref="IComparable.CompareTo"/>: comparing a
+ /// boxed Int32 against a UInt64 bound throws ArgumentException, because CompareTo
+ /// requires both operands to be the same runtime type. Widening to a common type
+ /// first is what makes the comparison well defined.
+ ///
+ /// decimal covers every integral PLC type exactly, including ulong.MaxValue. It
+ /// cannot hold the outer range of float/double, so an out-of-range REAL/LREAL
+ /// reports false rather than throwing.
+ /// </remarks>
+ private bool TryAsDecimal(out decimal result)
+ {
+ try
+ {
+ result = Convert.ToDecimal(value, CultureInfo.InvariantCulture);
+ return true;
+ }
+ catch (OverflowException)
+ {
+ result = default;
+ return false;
+ }
+ catch (InvalidCastException)
+ {
+ result = default;
+ return false;
+ }
+ }
+
+ private bool InRange(decimal min, decimal max)
+ {
+ return TryAsDecimal(out var d) && d >= min && d <= max;
+ }
+
+ public override bool IsBool()
+ {
+ return true;
+ }
+
+ public override bool GetBool()
+ {
+ return TryAsDecimal(out var d) ? d != 0m : false;
+ }
+
+ public override bool IsByte()
+ {
+ return InRange(byte.MinValue, byte.MaxValue);
+ }
+
+ public override byte GetByte()
+ {
+ if (!IsByte())
+ {
+ throw new ArgumentOutOfRangeException(nameof(value));
+ }
+ return Convert.ToByte(value, CultureInfo.InvariantCulture);
+ }
+
+ public override bool IsUshort()
+ {
+ return InRange(ushort.MinValue, ushort.MaxValue);
+ }
+
+ public override ushort GetUshort()
+ {
+ if (!IsUshort())
+ {
+ throw new ArgumentOutOfRangeException(nameof(value));
+ }
+ return Convert.ToUInt16(value, CultureInfo.InvariantCulture);
+ }
+
+ public override bool IsUint()
+ {
+ return InRange(uint.MinValue, uint.MaxValue);
+ }
+
+ public override uint GetUint()
+ {
+ if (!IsUint())
+ {
+ throw new ArgumentOutOfRangeException(nameof(value));
+ }
+ return Convert.ToUInt32(value, CultureInfo.InvariantCulture);
+ }
+
+ public override bool IsUlong()
+ {
+ return InRange(ulong.MinValue, ulong.MaxValue);
+ }
+
+ public override ulong GetUlong()
+ {
+ if (!IsUlong())
+ {
+ throw new ArgumentOutOfRangeException(nameof(value));
+ }
+ return Convert.ToUInt64(value, CultureInfo.InvariantCulture);
+ }
+
+ public override bool IsSbyte()
+ {
+ return InRange(sbyte.MinValue, sbyte.MaxValue);
+ }
+
+ public override sbyte GetSbyte()
+ {
+ if (!IsSbyte())
+ {
+ throw new ArgumentOutOfRangeException(nameof(value));
+ }
+ return Convert.ToSByte(value, CultureInfo.InvariantCulture);
+ }
+
+ public override bool IsShort()
+ {
+ return InRange(short.MinValue, short.MaxValue);
+ }
+
+ public override short GetShort()
+ {
+ if (!IsShort())
+ {
+ throw new ArgumentOutOfRangeException(nameof(value));
+ }
+ return Convert.ToInt16(value, CultureInfo.InvariantCulture);
+ }
+
+ public override bool IsInt()
+ {
+ return InRange(int.MinValue, int.MaxValue);
+ }
+
+ public override int GetInt()
+ {
+ if (!IsInt())
+ {
+ throw new ArgumentOutOfRangeException(nameof(value));
+ }
+ return Convert.ToInt32(value, CultureInfo.InvariantCulture);
+ }
+
+ public override bool IsLong()
+ {
+ return InRange(long.MinValue, long.MaxValue);
+ }
+
+ public override long GetLong()
+ {
+ if (!IsLong())
+ {
+ throw new ArgumentOutOfRangeException(nameof(value));
+ }
+ return Convert.ToInt64(value, CultureInfo.InvariantCulture);
+ }
+
+ public override bool IsFloat()
+ {
+ var d = Convert.ToDouble(value, CultureInfo.InvariantCulture);
+ // NaN / ±Infinity are representable as a float; only a finite value
+ // outside the float range is not.
+ return double.IsNaN(d) || double.IsInfinity(d)
+ || (d >= -float.MaxValue && d <= float.MaxValue);
+ }
+
+ public override float GetFloat()
+ {
+ if (!IsFloat())
+ {
+ throw new ArgumentOutOfRangeException(nameof(value));
+ }
+ return Convert.ToSingle(value, CultureInfo.InvariantCulture);
+ }
+
+ public override bool IsDouble()
+ {
+ return true;
+ }
+
+ public override double GetDouble()
+ {
+ return Convert.ToDouble(value, CultureInfo.InvariantCulture);
+ }
+
+ public override bool IsString()
+ {
+ return true;
+ }
+
+ public override string GetString()
+ {
+ // Convert.ToString only yields null for a null reference. Every subclass
+ // stores a numeric value type, but the IComparable constraint on T does
+ // not enforce that, so fall back to empty to keep the non-null contract.
+ return Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty;
+ }
+
+ protected bool Equals(SimpleNumericValueAdapter<T> other)
+ {
+ return Equals(value, other.value);
+ }
+
+ public override bool Equals(object? obj)
+ {
+ if (ReferenceEquals(null, obj)) return false;
+ if (ReferenceEquals(this, obj)) return true;
+ if (obj.GetType() != this.GetType()) return false;
+ return Equals((SimpleNumericValueAdapter<T>)obj);
+ }
+
+ public override int GetHashCode()
+ {
+ return (value != null ? value.GetHashCode() : 0);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/plc4net/spi/spi/model/values/PlcSimpleValueAdapter.cs b/plc4net/spi/model/values/PlcSimpleValueAdapter.cs
similarity index 92%
rename from plc4net/spi/spi/model/values/PlcSimpleValueAdapter.cs
rename to plc4net/spi/model/values/PlcSimpleValueAdapter.cs
index 4cd0a10b97..1be08f64e7 100644
--- a/plc4net/spi/spi/model/values/PlcSimpleValueAdapter.cs
+++ b/plc4net/spi/model/values/PlcSimpleValueAdapter.cs
@@ -21,16 +21,16 @@ namespace org.apache.plc4net.spi.model.values
{
public abstract class PlcSimpleValueAdapter : PlcValueAdapter
{
-
- public bool IsSimple()
+
+ public override bool IsSimple()
{
return true;
}
- public int GetLength()
+ public override int GetLength()
{
return 1;
}
-
+
}
}
\ No newline at end of file
diff --git a/plc4net/spi/model/values/PlcStruct.cs b/plc4net/spi/model/values/PlcStruct.cs
new file mode 100644
index 0000000000..94368066db
--- /dev/null
+++ b/plc4net/spi/model/values/PlcStruct.cs
@@ -0,0 +1,91 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+using System.Collections.Generic;
+using System.Linq;
+using org.apache.plc4net.api.value;
+
+namespace org.apache.plc4net.spi.model.values
+{
+ /// <summary>A named group of <see cref="IPlcValue"/> - the shape a KNX
+ /// composite datapoint (<c>DPT_Switch_Control</c>, <c>DPT_DateTime</c>)
+ /// parses to.</summary>
+ public class PlcStruct : PlcValueAdapter
+ {
+ private readonly Dictionary<string, IPlcValue> values;
+
+ public PlcStruct(Dictionary<string, IPlcValue> values)
+ {
+ this.values = values ?? new Dictionary<string, IPlcValue>();
+ }
+
+ public override bool IsStruct()
+ {
+ return true;
+ }
+
+ public override string[] GetKeys()
+ {
+ return values.Keys.ToArray();
+ }
+
+ public override bool HasKey(string key)
+ {
+ return values.ContainsKey(key);
+ }
+
+ public override IPlcValue? GetValue(string key)
+ {
+ return values.TryGetValue(key, out var value) ? value : null;
+ }
+
+ public override Dictionary<string, IPlcValue> GetStruct()
+ {
+ return values;
+ }
+
+ protected bool Equals(PlcStruct other)
+ {
+ return values.Count == other.values.Count
+ && values.All(kv => other.values.TryGetValue(kv.Key, out var v)
+ && Equals(kv.Value, v));
+ }
+
+ public override bool Equals(object? obj)
+ {
+ if (ReferenceEquals(null, obj)) return false;
+ if (ReferenceEquals(this, obj)) return true;
+ if (obj.GetType() != this.GetType()) return false;
+ return Equals((PlcStruct)obj);
+ }
+
+ public override int GetHashCode()
+ {
+ var hash = 17;
+ foreach (var kv in values.OrderBy(kv => kv.Key, System.StringComparer.Ordinal))
+ {
+ hash = hash * 31 + kv.Key.GetHashCode();
+ hash = hash * 31 + (kv.Value != null ? kv.Value.GetHashCode() : 0);
+ }
+
+ return hash;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/plc4net/spi/spi/model/values/PlcTIME.cs b/plc4net/spi/model/values/PlcTIME.cs
similarity index 72%
rename from plc4net/spi/spi/model/values/PlcTIME.cs
rename to plc4net/spi/model/values/PlcTIME.cs
index 41a412b20a..984b3820f1 100644
--- a/plc4net/spi/spi/model/values/PlcTIME.cs
+++ b/plc4net/spi/model/values/PlcTIME.cs
@@ -18,36 +18,48 @@
//
using System;
-using org.apache.plc4net.api.value;
namespace org.apache.plc4net.spi.model.values
{
+ /// <summary>IEC 61131 <c>TIME</c> - a duration, millisecond resolution.</summary>
public class PlcTIME : PlcSimpleValueAdapter
{
- private TimeSpan value;
-
+ private readonly TimeSpan value;
+
public PlcTIME(TimeSpan value)
{
this.value = value;
}
+ public static PlcTIME OfMilliseconds(long milliseconds) =>
+ new PlcTIME(TimeSpan.FromMilliseconds(milliseconds));
+
+ public override bool IsDuration()
+ {
+ return true;
+ }
+
+ public override TimeSpan GetDuration()
+ {
+ return value;
+ }
+
protected bool Equals(PlcTIME other)
{
return value.Equals(other.value);
}
- public override bool Equals(object obj)
+ public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
- return Equals((PlcTIME) obj);
+ return Equals((PlcTIME)obj);
}
public override int GetHashCode()
{
return value.GetHashCode();
}
-
}
}
\ No newline at end of file
diff --git a/plc4net/spi/spi/model/values/PlcTIME _OF_DAY.cs b/plc4net/spi/model/values/PlcTIME_OF_DAY.cs
similarity index 53%
rename from plc4net/spi/spi/model/values/PlcTIME _OF_DAY.cs
rename to plc4net/spi/model/values/PlcTIME_OF_DAY.cs
index 5144bc683d..442e5c0999 100644
--- a/plc4net/spi/spi/model/values/PlcTIME _OF_DAY.cs
+++ b/plc4net/spi/model/values/PlcTIME_OF_DAY.cs
@@ -18,36 +18,61 @@
//
using System;
-using org.apache.plc4net.api.value;
+using org.apache.plc4net.spi.generation;
namespace org.apache.plc4net.spi.model.values
{
+ /// <summary>IEC 61131 <c>TIME_OF_DAY</c> - a wall-clock time, no date.</summary>
public class PlcTIME_OF_DAY : PlcSimpleValueAdapter
{
- private DateTime value;
-
- public PlcTIME_OF_DAY(DateTime value)
+ private const long MillisPerDay = 86_400_000L;
+
+ private readonly TimeOnly value;
+
+ public PlcTIME_OF_DAY(TimeOnly value)
{
this.value = value;
}
+ public static PlcTIME_OF_DAY OfMillisecondsSinceMidnight(long millisecondsSinceMidnight)
+ {
+ if (millisecondsSinceMidnight < 0 || millisecondsSinceMidnight >= MillisPerDay)
+ {
+ // A corrupt frame must surface as a ParseException the codec can
+ // catch, not a framework ArgumentOutOfRangeException that escapes
+ // the receive loop.
+ throw new ParseException(
+ $"TIME_OF_DAY {millisecondsSinceMidnight} ms is not within a 24-hour day.");
+ }
+ return new PlcTIME_OF_DAY(TimeOnly.FromTimeSpan(TimeSpan.FromMilliseconds(millisecondsSinceMidnight)));
+ }
+
+ public override bool IsTime()
+ {
+ return true;
+ }
+
+ public override TimeOnly GetTime()
+ {
+ return value;
+ }
+
protected bool Equals(PlcTIME_OF_DAY other)
{
return value.Equals(other.value);
}
- public override bool Equals(object obj)
+ public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
- return Equals((PlcTIME_OF_DAY) obj);
+ return Equals((PlcTIME_OF_DAY)obj);
}
public override int GetHashCode()
{
return value.GetHashCode();
}
-
}
}
\ No newline at end of file
diff --git a/plc4net/spi/spi/model/values/PlcUDINT.cs b/plc4net/spi/model/values/PlcUDINT.cs
similarity index 100%
rename from plc4net/spi/spi/model/values/PlcUDINT.cs
rename to plc4net/spi/model/values/PlcUDINT.cs
diff --git a/plc4net/spi/spi/model/values/PlcUINT.cs b/plc4net/spi/model/values/PlcUINT.cs
similarity index 100%
rename from plc4net/spi/spi/model/values/PlcUINT.cs
rename to plc4net/spi/model/values/PlcUINT.cs
diff --git a/plc4net/spi/spi/model/values/PlcULINT.cs b/plc4net/spi/model/values/PlcULINT.cs
similarity index 100%
rename from plc4net/spi/spi/model/values/PlcULINT.cs
rename to plc4net/spi/model/values/PlcULINT.cs
diff --git a/plc4net/spi/spi/model/values/PlcUSINT.cs b/plc4net/spi/model/values/PlcUSINT.cs
similarity index 100%
rename from plc4net/spi/spi/model/values/PlcUSINT.cs
rename to plc4net/spi/model/values/PlcUSINT.cs
diff --git a/plc4net/spi/model/values/PlcValueAdapter.cs b/plc4net/spi/model/values/PlcValueAdapter.cs
new file mode 100644
index 0000000000..11753852e5
--- /dev/null
+++ b/plc4net/spi/model/values/PlcValueAdapter.cs
@@ -0,0 +1,281 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+using System;
+using System.Collections.Generic;
+using org.apache.plc4net.api.value;
+using org.apache.plc4net.exceptions;
+
+namespace org.apache.plc4net.spi.model.values
+{
+ public abstract class PlcValueAdapter : IPlcValue
+ {
+ public virtual bool IsSimple()
+ {
+ return false;
+ }
+
+ public virtual bool IsNullable()
+ {
+ return false;
+ }
+
+ public virtual bool IsNull()
+ {
+ return false;
+ }
+
+ public virtual bool IsBool()
+ {
+ return false;
+ }
+
+ public virtual int GetBoolLength()
+ {
+ return 1;
+ }
+
+ public virtual bool GetBool()
+ {
+ return default;
+ }
+
+ public virtual bool GetBoolAt(int index)
+ {
+ if (index == 0)
+ {
+ return GetBool();
+ }
+ return default;
+ }
+
+ public virtual bool[] GetBoolArray()
+ {
+ throw new PlcIncompatibleDatatypeException(
+ $"Value of type {GetType().Name} has no boolean array (check IsBool()).");
+ }
+
+ public virtual bool IsByte()
+ {
+ return false;
+ }
+
+ public virtual byte GetByte()
+ {
+ return default;
+ }
+
+ public virtual bool IsUshort()
+ {
+ return false;
+ }
+
+ public virtual ushort GetUshort()
+ {
+ return default;
+ }
+
+ public virtual bool IsUint()
+ {
+ return false;
+ }
+
+ public virtual uint GetUint()
+ {
+ return default;
+ }
+
+ public virtual bool IsUlong()
+ {
+ return false;
+ }
+
+ public virtual ulong GetUlong()
+ {
+ return default;
+ }
+
+ public virtual bool IsSbyte()
+ {
+ return false;
+ }
+
+ public virtual sbyte GetSbyte()
+ {
+ return default;
+ }
+
+ public virtual bool IsShort()
+ {
+ return false;
+ }
+
+ public virtual short GetShort()
+ {
+ return default;
+ }
+
+ public virtual bool IsInt()
+ {
+ return false;
+ }
+
+ public virtual int GetInt()
+ {
+ return default;
+ }
+
+ public virtual bool IsLong()
+ {
+ return false;
+ }
+
+ public virtual long GetLong()
+ {
+ return default;
+ }
+
+ public virtual bool IsFloat()
+ {
+ return false;
+ }
+
+ public virtual float GetFloat()
+ {
+ return default;
+ }
+
+ public virtual bool IsDouble()
+ {
+ return false;
+ }
+
+ public virtual double GetDouble()
+ {
+ return default;
+ }
+
+ public virtual bool IsString()
+ {
+ return false;
+ }
+
+ public virtual string GetString()
+ {
+ throw new PlcIncompatibleDatatypeException(
+ $"Value of type {GetType().Name} has no string representation (check IsString()).");
+ }
+
+ public virtual bool IsDuration()
+ {
+ return false;
+ }
+
+ public virtual TimeSpan GetDuration()
+ {
+ return default;
+ }
+
+ public virtual bool IsDate()
+ {
+ return false;
+ }
+
+ public virtual DateOnly GetDate()
+ {
+ return default;
+ }
+
+ public virtual bool IsTime()
+ {
+ return false;
+ }
+
+ public virtual TimeOnly GetTime()
+ {
+ return default;
+ }
+
+ public virtual bool IsDateTime()
+ {
+ return false;
+ }
+
+ public virtual DateTime GetDateTime()
+ {
+ return default;
+ }
+
+ public virtual byte[] GetRaw()
+ {
+ throw new PlcIncompatibleDatatypeException(
+ $"Value of type {GetType().Name} has no raw representation.");
+ }
+
+ public virtual bool IsList()
+ {
+ return false;
+ }
+
+ public virtual int GetLength()
+ {
+ return 1;
+ }
+
+ public virtual IPlcValue GetIndex(int index)
+ {
+ throw new PlcIncompatibleDatatypeException(
+ $"Value of type {GetType().Name} is not indexable (check IsList()).");
+ }
+
+ public virtual List<IPlcValue> GetList()
+ {
+ throw new PlcIncompatibleDatatypeException(
+ $"Value of type {GetType().Name} is not a list (check IsList()).");
+ }
+
+ public virtual bool IsStruct()
+ {
+ return false;
+ }
+
+ public virtual string[] GetKeys()
+ {
+ throw new PlcIncompatibleDatatypeException(
+ $"Value of type {GetType().Name} is not a struct (check IsStruct()).");
+ }
+
+ public virtual bool HasKey(string key)
+ {
+ return false;
+ }
+
+ public virtual IPlcValue? GetValue(string key)
+ {
+ throw new PlcIncompatibleDatatypeException(
+ $"Value of type {GetType().Name} is not a struct (check IsStruct()).");
+ }
+
+ public virtual Dictionary<string, IPlcValue> GetStruct()
+ {
+ throw new PlcIncompatibleDatatypeException(
+ $"Value of type {GetType().Name} is not a struct (check IsStruct()).");
+ }
+ }
+}
\ No newline at end of file
diff --git a/plc4net/spi/spi/model/values/PlcWCHAR.cs b/plc4net/spi/model/values/PlcWCHAR.cs
similarity index 84%
rename from plc4net/spi/spi/model/values/PlcWCHAR.cs
rename to plc4net/spi/model/values/PlcWCHAR.cs
index e5589a8e66..73c44ddb01 100644
--- a/plc4net/spi/spi/model/values/PlcWCHAR.cs
+++ b/plc4net/spi/model/values/PlcWCHAR.cs
@@ -24,29 +24,39 @@ namespace org.apache.plc4net.spi.model.values
public class PlcWCHAR : PlcSimpleValueAdapter
{
private char value;
-
+
public PlcWCHAR(char value)
{
this.value = value;
}
+ public override bool IsString()
+ {
+ return true;
+ }
+
+ public override string GetString()
+ {
+ return value.ToString();
+ }
+
protected bool Equals(PlcWCHAR other)
{
return value == other.value;
}
- public override bool Equals(object obj)
+ public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
- return Equals((PlcWCHAR) obj);
+ return Equals((PlcWCHAR)obj);
}
public override int GetHashCode()
{
return value.GetHashCode();
}
-
+
}
}
\ No newline at end of file
diff --git a/plc4net/spi/spi/model/values/PlcNULL.cs b/plc4net/spi/model/values/PlcWORD.cs
similarity index 76%
rename from plc4net/spi/spi/model/values/PlcNULL.cs
rename to plc4net/spi/model/values/PlcWORD.cs
index 4ec8ec3547..6c6e1f8cd1 100644
--- a/plc4net/spi/spi/model/values/PlcNULL.cs
+++ b/plc4net/spi/model/values/PlcWORD.cs
@@ -17,33 +17,30 @@
// under the License.
//
-using org.apache.plc4net.api.value;
-
namespace org.apache.plc4net.spi.model.values
{
- public class PlcNULL : PlcValueAdapter
+ public class PlcWORD : PlcBitString
{
- public PlcNULL()
+ public PlcWORD(ushort value) : base(value)
{
}
- protected bool Equals(PlcNULL other)
+ protected bool Equals(PlcWORD other)
{
- throw new System.NotImplementedException();
+ return GetUshort() == other.GetUshort();
}
- public override bool Equals(object obj)
+ public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
- return Equals((PlcNULL) obj);
+ return Equals((PlcWORD)obj);
}
public override int GetHashCode()
{
- throw new System.NotImplementedException();
+ return GetUshort().GetHashCode();
}
-
}
}
\ No newline at end of file
diff --git a/plc4net/spi/spi/model/values/PlcWSTRING.cs b/plc4net/spi/model/values/PlcWSTRING.cs
similarity index 84%
rename from plc4net/spi/spi/model/values/PlcWSTRING.cs
rename to plc4net/spi/model/values/PlcWSTRING.cs
index 27766cd196..3c153208e1 100644
--- a/plc4net/spi/spi/model/values/PlcWSTRING.cs
+++ b/plc4net/spi/model/values/PlcWSTRING.cs
@@ -24,29 +24,39 @@ namespace org.apache.plc4net.spi.model.values
public class PlcWSTRING : PlcSimpleValueAdapter
{
private string value;
-
+
public PlcWSTRING(string value)
{
this.value = value;
}
+ public override bool IsString()
+ {
+ return true;
+ }
+
+ public override string GetString()
+ {
+ return value;
+ }
+
protected bool Equals(PlcWSTRING other)
{
return value == other.value;
}
- public override bool Equals(object obj)
+ public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
- return Equals((PlcWSTRING) obj);
+ return Equals((PlcWSTRING)obj);
}
public override int GetHashCode()
{
return (value != null ? value.GetHashCode() : 0);
}
-
+
}
}
\ No newline at end of file
diff --git a/plc4net/api/plc4net-api.csproj b/plc4net/spi/spi.csproj
similarity index 64%
copy from plc4net/api/plc4net-api.csproj
copy to plc4net/spi/spi.csproj
index ee7f65ff08..5ec0f8cc6c 100644
--- a/plc4net/api/plc4net-api.csproj
+++ b/plc4net/spi/spi.csproj
@@ -20,16 +20,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net452</TargetFramework>
- <RootNamespace>org.apache.plc4net</RootNamespace>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
- <Authors>Apache PLC4X</Authors>
- <Copyright>The Apache Software Foundation</Copyright>
- <PackageProjectUrl>https://plc4x.apache.org</PackageProjectUrl>
- <Title>PLC4Net: API</Title>
- <PackageLicenseUrl>https://www.apache.org/licenses/LICENSE-2.0</PackageLicenseUrl>
- <PackageVersion>0.8.0-SNAPSHOT</PackageVersion>
- <LangVersion>11.0</LangVersion>
- </PropertyGroup>
+ <AssemblyName>plc4net-spi</AssemblyName>
+ <Title>PLC4Net: SPI</Title>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\api\api.csproj" />
+ </ItemGroup>
</Project>
diff --git a/plc4net/spi/spi/generation/ParseException.cs b/plc4net/spi/spi/generation/ParseException.cs
deleted file mode 100644
index 786c9cdf25..0000000000
--- a/plc4net/spi/spi/generation/ParseException.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-namespace org.apache.plc4net.spi.generation
-{
- public class ParseException
- {
-
- }
-}
\ No newline at end of file
diff --git a/plc4net/spi/spi/generation/ReadBuffer.cs b/plc4net/spi/spi/generation/ReadBuffer.cs
deleted file mode 100644
index 28140d2902..0000000000
--- a/plc4net/spi/spi/generation/ReadBuffer.cs
+++ /dev/null
@@ -1,206 +0,0 @@
-//
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements. See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership. The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License. You may obtain a copy of the License at
-//
-// https://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied. See the License for the
-// specific language governing permissions and limitations
-// under the License.
-//
-
-using System;
-using System.Text;
-using Ayx.BitIO;
-
-namespace org.apache.plc4net.spi.generation
-{
-
- public class ReadBuffer
- {
- private readonly byte[] _data;
- private readonly BitReader _reader;
-
- public ReadBuffer(byte[] data)
- {
- this._data = data;
- _reader = new BitReader(data);
- }
-
- public void Reset()
- {
- _reader.Position = 0;
- }
-
- public int GetPos()
- {
- return _reader.Position;
- }
-
- public byte[] GetBytes()
- {
- return _data;
- }
-
- public int GetTotalBytes()
- {
- return _data.Length;
- }
-
- public bool HasMore(int bitLength)
- {
- return bitLength < _reader.Remain;
- }
-
- public byte PeekByte(int offset)
- {
- return _data[(_reader.Position / 8) + offset];
- }
-
- public bool ReadBit(String logicalName)
- {
- return _reader.ReadBool();
- }
-
- public byte ReadByte(String logicalName, int bitLength)
- {
- if ((bitLength < 0) || (bitLength > 8))
- {
- throw new ArgumentOutOfRangeException();
- }
- return (byte) _reader.ReadInt(bitLength);
- }
-
- public ushort ReadUshort(String logicalName, int bitLength)
- {
- if ((bitLength < 0) || (bitLength > 16))
- {
- throw new ArgumentOutOfRangeException();
- }
- return (ushort) _reader.ReadInt(bitLength);
- }
-
- public uint ReadUint(String logicalName, int bitLength)
- {
- if ((bitLength < 0) || (bitLength > 32))
- {
- throw new ArgumentOutOfRangeException();
- }
- return (uint) _reader.ReadInt(bitLength);
- }
-
- public ulong ReadUlong(String logicalName, int bitLength)
- {
- if ((bitLength < 0) || (bitLength > 64))
- {
- throw new ArgumentOutOfRangeException();
- }
-
- ulong firstInt = 0;
- if (bitLength > 32)
- {
- firstInt = (ulong) _reader.ReadInt(bitLength - 32) << 32;
- }
- return firstInt | (ulong) _reader.ReadInt(bitLength);
- }
-
- public sbyte ReadSbyte(String logicalName, int bitLength)
- {
- if ((bitLength < 0) || (bitLength > 8))
- {
- throw new ArgumentOutOfRangeException();
- }
- return (sbyte) _reader.ReadInt(bitLength);
- }
-
- public short ReadShort(String logicalName, int bitLength)
- {
- if ((bitLength < 0) || (bitLength > 16))
- {
- throw new ArgumentOutOfRangeException();
- }
- return (short) _reader.ReadInt(bitLength);
- }
-
- public int ReadInt(String logicalName, int bitLength)
- {
- if ((bitLength < 0) || (bitLength > 32))
- {
- throw new ArgumentOutOfRangeException();
- }
- return _reader.ReadInt(bitLength);
- }
-
- public long ReadLong(String logicalName, int bitLength)
- {
- if ((bitLength < 0) || (bitLength > 64))
- {
- throw new ArgumentOutOfRangeException();
- }
-
- long firstInt = 0;
- if (bitLength > 32)
- {
- firstInt = (long) _reader.ReadInt(bitLength - 32) << 32;
- }
- return firstInt | (long) _reader.ReadInt(bitLength);
- }
-
- public float ReadFloat(String logicalName, int bitLength)
- {
- if (bitLength == 32)
- {
- return BitConverter.ToSingle(BitConverter.GetBytes(ReadInt(logicalName, 32)), 0);
- }
- // This is the format as described in the KNX spec ... it's not a real half precision floating point.
- if (bitLength == 16)
- {
- bool sign = true;
- sign = _reader.ReadBool();
-
- var exp = _reader.ReadInt(4);
- var mantissa = _reader.ReadInt(11);
- // In the mantissa notation actually the first bit is omitted, we need to add it back
- var f = 0.01 * mantissa * Math.Pow(2, exp);
- if (sign)
- {
- return (float) f * -1;
- }
- return (float) f;
- }
- throw new NotImplementedException("This encoding is currently not supported");
- }
-
- public double ReadDouble(String logicalName, int bitLength)
- {
- if (bitLength == 32)
- {
- return BitConverter.ToDouble(BitConverter.GetBytes(ReadInt(logicalName, 32)), 0);
- }
- if (bitLength == 64)
- {
- return BitConverter.ToDouble(BitConverter.GetBytes(ReadLong(logicalName, 64)), 0);
- }
- throw new NotImplementedException("This encoding is currently not supported");
- }
-
- public string ReadString(String logicalName, int bitLength, Encoding encoding)
- {
- throw new NotImplementedException("This encoding is currently not supported");
- }
-
- public byte[] ReadByteArray(String logicalName, int bitLength)
- {
- throw new NotImplementedException("This function is currently not supported");
- }
-
- }
-}
\ No newline at end of file
diff --git a/plc4net/spi/spi/generation/WriteBuffer.cs b/plc4net/spi/spi/generation/WriteBuffer.cs
deleted file mode 100644
index 4bd9de1e43..0000000000
--- a/plc4net/spi/spi/generation/WriteBuffer.cs
+++ /dev/null
@@ -1,178 +0,0 @@
-//
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements. See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership. The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License. You may obtain a copy of the License at
-//
-// https://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied. See the License for the
-// specific language governing permissions and limitations
-// under the License.
-//
-
-using System;
-using Ayx.BitIO;
-
-namespace org.apache.plc4net.spi.generation
-{
- public class WriteBuffer
- {
- private readonly BitWriter _writer;
-
- public WriteBuffer()
- {
- _writer = new BitWriter();
- }
-
- public int GetPos()
- {
- return _writer.Length;
- }
-
- public byte[] GetBytes()
- {
- return _writer.GetBytes();
- }
-
- public int GetTotalBytes()
- {
- return _writer.Length / 8 + (_writer.Length % 8 != 0 ? 1 : 0);
- }
-
- public void WriteBit(String logicalName, bool value)
- {
- _writer.WriteBool(value);
- }
-
- public void WriteByte(String logicalName, int bitLength, byte value)
- {
- if (bitLength > 8)
- {
- throw new ArgumentOutOfRangeException();
- }
- _writer.WriteByte(value, bitLength);
- }
-
- public void WriteUshort(String logicalName, int bitLength, ushort value)
- {
- if (bitLength > 16)
- {
- throw new ArgumentOutOfRangeException();
- }
- _writer.WriteInt(value, bitLength);
- }
-
- public void WriteUint(String logicalName, int bitLength, uint value)
- {
- if (bitLength > 32)
- {
- throw new ArgumentOutOfRangeException();
- }
- _writer.WriteInt((int) value, bitLength);
- }
-
- public void WriteUlong(String logicalName, int bitLength, ulong value)
- {
- if (bitLength > 64)
- {
- throw new ArgumentOutOfRangeException();
- }
- var highInt = (int) ((value & 0xFFFFFFFF00000000) >> 32);
- var lowInt = (int) (value & 0xFFFFFFFF);
- if (bitLength > 32)
- {
- _writer.WriteInt(highInt, bitLength - 32);
- _writer.WriteInt(lowInt, 32);
- }
- else
- {
- _writer.WriteInt(lowInt, bitLength);
- }
- }
-
- public void WriteSbyte(String logicalName, int bitLength, sbyte value)
- {
- if (bitLength > 8)
- {
- throw new ArgumentOutOfRangeException();
- }
- _writer.WriteInt(value, bitLength);
- }
-
- public void WriteShort(String logicalName, int bitLength, short value)
- {
- if (bitLength > 16)
- {
- throw new ArgumentOutOfRangeException();
- }
- _writer.WriteInt(value, bitLength);
- }
-
- public void WriteInt(String logicalName, int bitLength, int value)
- {
- if (bitLength > 32)
- {
- throw new ArgumentOutOfRangeException();
- }
- _writer.WriteInt(value, bitLength);
- }
-
- public void WriteLong(String logicalName, int bitLength, long value)
- {
- if (bitLength > 64)
- {
- throw new ArgumentOutOfRangeException();
- }
- WriteUlong(logicalName, bitLength, (ulong) value);
- }
-
- public void WriteFloat(String logicalName, int bitLength, float value)
- {
- if (bitLength == 32)
- {
- var bytes = BitConverter.GetBytes(value);
- for (var i = 0; i < bytes.Length; i++)
- {
- _writer.WriteByte(8, bytes[i]);
- }
- }
- else if (bitLength == 16)
- {
-
- }
- else
- {
- throw new NotImplementedException("This encoding is currently not supported");
- }
- }
-
- public void WriteDouble(String logicalName, int bitLength, double value)
- {
- if (bitLength == 64)
- {
- var bytes = BitConverter.GetBytes(value);
- for (var i = 0; i < bytes.Length; i++)
- {
- _writer.WriteByte(8, bytes[i]);
- }
- }
- else
- {
- throw new NotImplementedException("This encoding is currently not supported");
- }
- }
-
- public void WriteString(String logicalName, int bitLength, string encoding, string value)
- {
- throw new NotImplementedException("This encoding is currently not supported");
- }
-
- }
-}
\ No newline at end of file
diff --git a/plc4net/spi/spi/model/values/PlcDWORD.cs b/plc4net/spi/spi/model/values/PlcDWORD.cs
deleted file mode 100644
index 8999178afd..0000000000
--- a/plc4net/spi/spi/model/values/PlcDWORD.cs
+++ /dev/null
@@ -1,90 +0,0 @@
-//
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements. See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership. The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License. You may obtain a copy of the License at
-//
-// https://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied. See the License for the
-// specific language governing permissions and limitations
-// under the License.
-//
-
-using org.apache.plc4net.api.value;
-
-namespace org.apache.plc4net.spi.model.values
-{
- public class PlcDWORD : PlcBitString
- {
- private uint value;
-
- public PlcDWORD(uint value)
- {
- this.value = value;
- }
-
- public new int GetBoolLength()
- {
- return 32;
- }
-
- public new bool GetBoolAt(int index)
- {
- if (index > 31)
- {
- return false;
- }
-
- return ((value >> index) & 1) == 1;
- }
-
- public new bool[] GetBoolArray()
- {
- return new[]
- {
- GetBoolAt(0), GetBoolAt(1),
- GetBoolAt(2), GetBoolAt(3),
- GetBoolAt(4), GetBoolAt(5),
- GetBoolAt(6), GetBoolAt(7),
- GetBoolAt(8), GetBoolAt(9),
- GetBoolAt(10), GetBoolAt(11),
- GetBoolAt(12), GetBoolAt(13),
- GetBoolAt(14), GetBoolAt(15),
- GetBoolAt(16), GetBoolAt(17),
- GetBoolAt(18), GetBoolAt(19),
- GetBoolAt(20), GetBoolAt(21),
- GetBoolAt(22), GetBoolAt(23),
- GetBoolAt(24), GetBoolAt(25),
- GetBoolAt(26), GetBoolAt(27),
- GetBoolAt(28), GetBoolAt(29),
- GetBoolAt(30), GetBoolAt(31)
- };
- }
-
- protected bool Equals(PlcDWORD other)
- {
- return value == other.value;
- }
-
- public override bool Equals(object obj)
- {
- if (ReferenceEquals(null, obj)) return false;
- if (ReferenceEquals(this, obj)) return true;
- if (obj.GetType() != this.GetType()) return false;
- return Equals((PlcDWORD) obj);
- }
-
- public override int GetHashCode()
- {
- return (int) value;
- }
-
- }
-}
\ No newline at end of file
diff --git a/plc4net/spi/spi/model/values/PlcLWORD.cs b/plc4net/spi/spi/model/values/PlcLWORD.cs
deleted file mode 100644
index f4e21eca05..0000000000
--- a/plc4net/spi/spi/model/values/PlcLWORD.cs
+++ /dev/null
@@ -1,106 +0,0 @@
-//
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements. See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership. The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License. You may obtain a copy of the License at
-//
-// https://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied. See the License for the
-// specific language governing permissions and limitations
-// under the License.
-//
-
-using org.apache.plc4net.api.value;
-
-namespace org.apache.plc4net.spi.model.values
-{
- public class PlcLWORD : PlcBitString
- {
- private ulong value;
-
- public PlcLWORD(ulong value)
- {
- this.value = value;
- }
-
- public new int GetBoolLength()
- {
- return 64;
- }
-
- public new bool GetBoolAt(int index)
- {
- if (index > 63)
- {
- return false;
- }
-
- return ((value >> index) & 1) == 1;
- }
-
- public new bool[] GetBoolArray()
- {
- return new[]
- {
- GetBoolAt(0), GetBoolAt(1),
- GetBoolAt(2), GetBoolAt(3),
- GetBoolAt(4), GetBoolAt(5),
- GetBoolAt(6), GetBoolAt(7),
- GetBoolAt(8), GetBoolAt(9),
- GetBoolAt(10), GetBoolAt(11),
- GetBoolAt(12), GetBoolAt(13),
- GetBoolAt(14), GetBoolAt(15),
- GetBoolAt(16), GetBoolAt(17),
- GetBoolAt(18), GetBoolAt(19),
- GetBoolAt(20), GetBoolAt(21),
- GetBoolAt(22), GetBoolAt(23),
- GetBoolAt(24), GetBoolAt(25),
- GetBoolAt(26), GetBoolAt(27),
- GetBoolAt(28), GetBoolAt(29),
- GetBoolAt(30), GetBoolAt(31),
- GetBoolAt(32), GetBoolAt(33),
- GetBoolAt(34), GetBoolAt(35),
- GetBoolAt(36), GetBoolAt(37),
- GetBoolAt(38), GetBoolAt(39),
- GetBoolAt(40), GetBoolAt(41),
- GetBoolAt(42), GetBoolAt(43),
- GetBoolAt(44), GetBoolAt(45),
- GetBoolAt(46), GetBoolAt(47),
- GetBoolAt(48), GetBoolAt(49),
- GetBoolAt(50), GetBoolAt(51),
- GetBoolAt(52), GetBoolAt(53),
- GetBoolAt(54), GetBoolAt(55),
- GetBoolAt(56), GetBoolAt(57),
- GetBoolAt(58), GetBoolAt(59),
- GetBoolAt(60), GetBoolAt(61),
- GetBoolAt(62), GetBoolAt(63)
- };
- }
-
- protected bool Equals(PlcLWORD other)
- {
- return value == other.value;
- }
-
- public override bool Equals(object obj)
- {
- if (ReferenceEquals(null, obj)) return false;
- if (ReferenceEquals(this, obj)) return true;
- if (obj.GetType() != this.GetType()) return false;
- return Equals((PlcLWORD) obj);
- }
-
- public override int GetHashCode()
- {
- return value.GetHashCode();
- }
-
- }
-}
\ No newline at end of file
diff --git a/plc4net/spi/spi/model/values/PlcSimpleNumericValueAdapter.cs b/plc4net/spi/spi/model/values/PlcSimpleNumericValueAdapter.cs
deleted file mode 100644
index 8e8c1597fa..0000000000
--- a/plc4net/spi/spi/model/values/PlcSimpleNumericValueAdapter.cs
+++ /dev/null
@@ -1,212 +0,0 @@
-//
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements. See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership. The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License. You may obtain a copy of the License at
-//
-// https://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied. See the License for the
-// specific language governing permissions and limitations
-// under the License.
-//
-
-using System;
-
-namespace org.apache.plc4net.spi.model.values
-{
- public abstract class SimpleNumericValueAdapter<T> : PlcSimpleValueAdapter where T : IComparable
- {
- private IComparable value;
-
- public SimpleNumericValueAdapter(IComparable value)
- {
- this.value = value;
- }
-
- public new bool IsBool()
- {
- return true;
- }
-
- public new bool GetBool()
- {
- return value.CompareTo(0) != 0;
- }
-
- public new bool IsByte()
- {
- return (value.CompareTo(byte.MinValue) >= 0) && (value.CompareTo(byte.MaxValue) <= 0);
- }
-
- public new byte GetByte()
- {
- if (!IsByte())
- {
- throw new ArgumentOutOfRangeException();
- }
- return (byte) value;
- }
-
- public new bool IsUshort()
- {
- return (value.CompareTo(ushort.MinValue) >= 0) && (value.CompareTo(ushort.MaxValue) <= 0);
- }
-
- public new ushort GetUshort()
- {
- if (!IsUshort())
- {
- throw new ArgumentOutOfRangeException();
- }
- return (ushort) value;
- }
-
- public new bool IsUint()
- {
- return (value.CompareTo(uint.MinValue) >= 0) && (value.CompareTo(uint.MaxValue) <= 0);
- }
-
- public new uint GetUint()
- {
- if (!IsUint())
- {
- throw new ArgumentOutOfRangeException();
- }
- return (uint) value;
- }
-
- public new bool IsUlong()
- {
- return (value.CompareTo(ulong.MinValue) >= 0) && (value.CompareTo(ulong.MaxValue) <= 0);
- }
-
- public new ulong GetUlong()
- {
- if (!IsUlong())
- {
- throw new ArgumentOutOfRangeException();
- }
- return (ulong) value;
- }
-
- public new bool IsSbyte()
- {
- return (value.CompareTo(sbyte.MinValue) >= 0) && (value.CompareTo(sbyte.MaxValue) <= 0);
- }
-
- public new sbyte GetSbyte()
- {
- if (!IsSbyte())
- {
- throw new ArgumentOutOfRangeException();
- }
- return (sbyte) value;
- }
-
- public new bool IsShort()
- {
- return (value.CompareTo(short.MinValue) >= 0) && (value.CompareTo(short.MaxValue) <= 0);
- }
-
- public new short GetShort()
- {
- if (!IsShort())
- {
- throw new ArgumentOutOfRangeException();
- }
- return (short) value;
- }
-
- public new bool IsInt()
- {
- return (value.CompareTo(int.MinValue) >= 0) && (value.CompareTo(int.MaxValue) <= 0);
- }
-
- public new int GetInt()
- {
- if (!IsInt())
- {
- throw new ArgumentOutOfRangeException();
- }
- return (int) value;
- }
-
- public new bool IsLong()
- {
- return (value.CompareTo(long.MinValue) >= 0) && (value.CompareTo(long.MaxValue) <= 0);
- }
-
- public new long GetLong()
- {
- if (!IsLong())
- {
- throw new ArgumentOutOfRangeException();
- }
- return (long) value;
- }
-
- public new bool IsFloat()
- {
- return (value.CompareTo(-float.MaxValue) >= 0) && (value.CompareTo(float.MaxValue) <= 0);
- }
-
- public new float GetFloat()
- {
- if (!IsFloat())
- {
- throw new ArgumentOutOfRangeException();
- }
- return (float) value;
- }
-
- public new bool IsDouble()
- {
- return (value.CompareTo(-double.MaxValue) >= 0) && (value.CompareTo(double.MaxValue) <= 0);
- }
-
- public new double GetDouble()
- {
- if (!IsDouble())
- {
- throw new ArgumentOutOfRangeException();
- }
- return (double) value;
- }
-
- public new bool IsString()
- {
- return true;
- }
-
- public new String GetString()
- {
- return value.ToString();
- }
-
- protected bool Equals(SimpleNumericValueAdapter<T> other)
- {
- return Equals(value, other.value);
- }
-
- public override bool Equals(object obj)
- {
- if (ReferenceEquals(null, obj)) return false;
- if (ReferenceEquals(this, obj)) return true;
- if (obj.GetType() != this.GetType()) return false;
- return Equals((SimpleNumericValueAdapter<T>) obj);
- }
-
- public override int GetHashCode()
- {
- return (value != null ? value.GetHashCode() : 0);
- }
-
- }
-}
\ No newline at end of file
diff --git a/plc4net/spi/spi/model/values/PlcStruct.cs b/plc4net/spi/spi/model/values/PlcStruct.cs
deleted file mode 100644
index 781afd5785..0000000000
--- a/plc4net/spi/spi/model/values/PlcStruct.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-//
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements. See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership. The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License. You may obtain a copy of the License at
-//
-// https://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied. See the License for the
-// specific language governing permissions and limitations
-// under the License.
-//
-
-using System.Collections.Generic;
-using org.apache.plc4net.api.value;
-
-namespace org.apache.plc4net.spi.model.values
-{
-
- public class PlcStruct : PlcValueAdapter
- {
- private Dictionary<string, IPlcValue> values;
-
- public PlcStruct(Dictionary<string, IPlcValue> values)
- {
- this.values = values;
- }
-
- protected bool Equals(PlcStruct other)
- {
- return Equals(values, other.values);
- }
-
- public override bool Equals(object obj)
- {
- if (ReferenceEquals(null, obj)) return false;
- if (ReferenceEquals(this, obj)) return true;
- if (obj.GetType() != this.GetType()) return false;
- return Equals((PlcStruct) obj);
- }
-
- public override int GetHashCode()
- {
- return (values != null ? values.GetHashCode() : 0);
- }
- }
-
-}
\ No newline at end of file
diff --git a/plc4net/spi/spi/model/values/PlcValueAdapter.cs b/plc4net/spi/spi/model/values/PlcValueAdapter.cs
deleted file mode 100644
index 8ead9812a4..0000000000
--- a/plc4net/spi/spi/model/values/PlcValueAdapter.cs
+++ /dev/null
@@ -1,242 +0,0 @@
-//
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements. See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership. The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License. You may obtain a copy of the License at
-//
-// https://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied. See the License for the
-// specific language governing permissions and limitations
-// under the License.
-//
-
-using System;
-using System.Collections.Generic;
-using org.apache.plc4net.api.value;
-
-namespace org.apache.plc4net.spi.model.values
-{
- public abstract class PlcValueAdapter : IPlcValue
- {
- public bool IsSimple()
- {
- return false;
- }
-
- public bool IsNullable()
- {
- return false;
- }
-
- public bool IsNull()
- {
- return false;
- }
-
- public bool IsBool()
- {
- return false;
- }
-
- public int GetBoolLength()
- {
- return 1;
- }
-
- public bool GetBool()
- {
- return default;
- }
-
- public bool GetBoolAt(int index)
- {
- if (index == 0)
- {
- return GetBool();
- }
- return default;
- }
-
- public bool[] GetBoolArray()
- {
- return default;
- }
-
- public bool IsByte()
- {
- return false;
- }
-
- public byte GetByte()
- {
- return default;
- }
-
- public bool IsUshort()
- {
- return false;
- }
-
- public ushort GetUshort()
- {
- return default;
- }
-
- public bool IsUint()
- {
- return false;
- }
-
- public uint GetUint()
- {
- return default;
- }
-
- public bool IsUlong()
- {
- return false;
- }
-
- public ulong GetUlong()
- {
- return default;
- }
-
- public bool IsSbyte()
- {
- return false;
- }
-
- public sbyte GetSbyte()
- {
- return default;
- }
-
- public bool IsShort()
- {
- return false;
- }
-
- public short GetShort()
- {
- return default;
- }
-
- public bool IsInt()
- {
- return false;
- }
-
- public int GetInt()
- {
- return default;
- }
-
- public bool IsLong()
- {
- return false;
- }
-
- public long GetLong()
- {
- return default;
- }
-
- public bool IsFloat()
- {
- return false;
- }
-
- public float GetFloat()
- {
- return default;
- }
-
- public bool IsDouble()
- {
- return false;
- }
-
- public double GetDouble()
- {
- return default;
- }
-
- public bool IsString()
- {
- return false;
- }
-
- public string GetString()
- {
- return default;
- }
-
- public bool IsDateTime()
- {
- return false;
- }
-
- public DateTime GetDateTime()
- {
- return default;
- }
-
- public byte[] GetRaw()
- {
- return default;
- }
-
- public bool IsList()
- {
- return false;
- }
-
- public int GetLength()
- {
- return 1;
- }
-
- public IPlcValue GetIndex(int index)
- {
- return default;
- }
-
- public List<IPlcValue> GetList()
- {
- return default;
- }
-
- public bool IsStruct()
- {
- return false;
- }
-
- public string[] GetKeys()
- {
- return default;
- }
-
- public bool HasKey(string key)
- {
- return false;
- }
-
- public IPlcValue GetValue(string key)
- {
- return default;
- }
-
- public Dictionary<string, IPlcValue> GetStruct()
- {
- return default;
- }
- }
-}
\ No newline at end of file
diff --git a/plc4net/spi/transports/BaseTransportInstance.cs b/plc4net/spi/transports/BaseTransportInstance.cs
new file mode 100644
index 0000000000..70489c3045
--- /dev/null
+++ b/plc4net/spi/transports/BaseTransportInstance.cs
@@ -0,0 +1,96 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+using System;
+
+namespace org.apache.plc4net.spi.transports
+{
+ /// <summary>
+ /// Shared state for transport implementations: the configuration and the
+ /// driver-config carved off the connection URL.
+ /// </summary>
+ public abstract class BaseTransportInstance : ITransportInstance
+ {
+ private string _driverConfig = string.Empty;
+
+ protected BaseTransportInstance(ITransportConfiguration configuration)
+ {
+ Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
+ }
+
+ public ITransportConfiguration Configuration { get; }
+
+ public string DriverConfig
+ {
+ get => _driverConfig;
+ protected set => _driverConfig = value ?? string.Empty;
+ }
+
+ /// <summary>
+ /// Records the portion of the address the transport did not consume. Transports
+ /// call this immediately after construction.
+ /// </summary>
+ public void SetDriverConfig(string driverConfig)
+ {
+ DriverConfig = driverConfig;
+ }
+
+ public abstract bool IsOpen { get; }
+
+ public abstract int GetNumBytesAvailable();
+
+ public abstract byte[] PeekReadableBytes(int numBytes);
+
+ public abstract byte[] Read(int numBytes);
+
+ public abstract void Write(byte[] bytes);
+
+ public abstract void Close();
+
+ protected void EnsureOpen()
+ {
+ if (!IsOpen)
+ {
+ throw new TransportException("Transport is closed");
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!disposing)
+ {
+ return;
+ }
+ try
+ {
+ Close();
+ }
+ catch (TransportException)
+ {
+ // Dispose must not throw; a failure to close is already terminal.
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/plc4net/spi/transports/ITransport.cs b/plc4net/spi/transports/ITransport.cs
new file mode 100644
index 0000000000..472f5ea31d
--- /dev/null
+++ b/plc4net/spi/transports/ITransport.cs
@@ -0,0 +1,108 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+using System;
+using System.Collections.Generic;
+
+namespace org.apache.plc4net.spi.transports
+{
+ /// <summary>
+ /// Factory for a kind of transport (tcp, udp, serial, cotp, ...).
+ /// </summary>
+ public interface ITransport
+ {
+ /// <summary>The code used in a connection string to select this transport, e.g. "tcp".</summary>
+ string TransportCode { get; }
+
+ /// <summary>Human-readable name.</summary>
+ string TransportName { get; }
+
+ /// <summary>
+ /// Builds a configuration for this transport from the connection string's
+ /// parameters, applying this transport's defaults.
+ /// </summary>
+ ITransportConfiguration CreateConfiguration(IReadOnlyDictionary<string, string> parameters);
+
+ /// <summary>
+ /// Opens a connection.
+ /// </summary>
+ /// <param name="transportConfig">the address portion of the URL, e.g. "192.168.0.1:102"</param>
+ /// <param name="configuration">configuration produced by <see cref="CreateConfiguration"/></param>
+ ITransportInstance CreateTransportInstance(string transportConfig, ITransportConfiguration configuration);
+ }
+
+ /// <summary>Registry of the transports available to drivers.</summary>
+ public interface ITransportManager
+ {
+ /// <summary>Returns the transport for a code, or null when none is registered.</summary>
+ ITransport? GetTransport(string transportCode);
+
+ IReadOnlyCollection<string> GetTransportCodes();
+ }
+
+ /// <summary>
+ /// In-memory <see cref="ITransportManager"/>. Transports register themselves here
+ /// rather than being discovered, which keeps the .NET port free of the assembly
+ /// scanning the Java side does via ServiceLoader.
+ /// </summary>
+ public class DefaultTransportManager : ITransportManager
+ {
+ private readonly Dictionary<string, ITransport> _transports =
+ new Dictionary<string, ITransport>(StringComparer.OrdinalIgnoreCase);
+
+ public DefaultTransportManager()
+ {
+ }
+
+ public DefaultTransportManager(IEnumerable<ITransport> transports)
+ {
+ if (transports == null)
+ {
+ return;
+ }
+ foreach (var transport in transports)
+ {
+ Register(transport);
+ }
+ }
+
+ public void Register(ITransport transport)
+ {
+ if (transport == null)
+ {
+ throw new ArgumentNullException(nameof(transport));
+ }
+ _transports[transport.TransportCode] = transport;
+ }
+
+ public ITransport? GetTransport(string transportCode)
+ {
+ if (transportCode == null)
+ {
+ return null;
+ }
+ return _transports.TryGetValue(transportCode, out var transport) ? transport : null;
+ }
+
+ public IReadOnlyCollection<string> GetTransportCodes()
+ {
+ return new List<string>(_transports.Keys);
+ }
+ }
+}
\ No newline at end of file
diff --git a/plc4net/spi/transports/ITransportInstance.cs b/plc4net/spi/transports/ITransportInstance.cs
new file mode 100644
index 0000000000..a62e777a4f
--- /dev/null
+++ b/plc4net/spi/transports/ITransportInstance.cs
@@ -0,0 +1,95 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+using System;
+
+namespace org.apache.plc4net.spi.transports
+{
+ /// <summary>
+ /// Marker for a transport's configuration object. Concrete transports define their
+ /// own (host, port, timeouts, buffer sizes, ...).
+ /// </summary>
+ public interface ITransportConfiguration
+ {
+ }
+
+ /// <summary>
+ /// A live connection to a device, exposed as a byte stream.
+ /// </summary>
+ /// <remarks>
+ /// Mirrors the Java SPI3 <c>TransportInstance</c> contract. Deliberately byte-oriented
+ /// and peekable: most industrial protocols put the message length inside a fixed-size
+ /// header, so a codec peeks the header, works out the full length, and only then
+ /// consumes the whole frame.
+ /// </remarks>
+ public interface ITransportInstance : IDisposable
+ {
+ /// <summary>The transport's configuration.</summary>
+ ITransportConfiguration Configuration { get; }
+
+ /// <summary>
+ /// The part of the connection URL the transport did not consume, which is
+ /// meaningful only to the driver. For <c>opcua:tcp://localhost:4840/milo</c> the
+ /// transport takes <c>localhost:4840</c> and this returns <c>/milo</c>. Empty
+ /// when the address carries no trailing part.
+ /// </summary>
+ string DriverConfig { get; }
+
+ /// <summary>Whether the underlying transport is still usable.</summary>
+ bool IsOpen { get; }
+
+ /// <summary>Number of bytes buffered and ready to read.</summary>
+ int GetNumBytesAvailable();
+
+ /// <summary>Returns the next bytes without advancing the read position.</summary>
+ byte[] PeekReadableBytes(int numBytes);
+
+ /// <summary>Returns the next bytes and marks them consumed.</summary>
+ byte[] Read(int numBytes);
+
+ /// <summary>Writes the given bytes to the transport.</summary>
+ void Write(byte[] bytes);
+
+ /// <summary>Closes the transport. Safe to call more than once.</summary>
+ void Close();
+ }
+
+ /// <summary>
+ /// A transport that can push a notification when bytes arrive, so the driver does not
+ /// have to poll <see cref="ITransportInstance.GetNumBytesAvailable"/>.
+ /// </summary>
+ public interface IAsyncTransportInstance : ITransportInstance
+ {
+ /// <summary>
+ /// Called after new bytes land in the buffer. Invoked on the transport's read
+ /// thread, so implementations should hand off rather than block.
+ /// </summary>
+ void RegisterDataListener(Action listener);
+
+ void RemoveDataListener();
+
+ /// <summary>
+ /// Called when the connection drops. The argument is the causing exception, or
+ /// null for an orderly close by the remote end.
+ /// </summary>
+ void RegisterDisconnectListener(Action<Exception> listener);
+
+ void RemoveDisconnectListener();
+ }
+}
\ No newline at end of file
diff --git a/plc4net/spi/transports/RingBuffer.cs b/plc4net/spi/transports/RingBuffer.cs
new file mode 100644
index 0000000000..d550b999f3
--- /dev/null
+++ b/plc4net/spi/transports/RingBuffer.cs
@@ -0,0 +1,153 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+using System;
+
+namespace org.apache.plc4net.spi.transports
+{
+ /// <summary>
+ /// Fixed-capacity byte ring buffer sitting between a transport's read loop and the
+ /// protocol codec that drains it.
+ /// </summary>
+ /// <remarks>
+ /// The codec needs to inspect a message header before it knows how long the message
+ /// is, so this supports <see cref="Peek"/> (look without consuming) alongside
+ /// <see cref="Read"/>. Callers are expected to hold the transport's read lock; this
+ /// type is not internally synchronised.
+ /// </remarks>
+ public class RingBuffer
+ {
+ private readonly byte[] _buffer;
+ private int _readPosition;
+ private int _writePosition;
+ private int _count;
+
+ public RingBuffer(int capacity)
+ {
+ if (capacity <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(capacity), capacity, "Capacity must be positive.");
+ }
+ _buffer = new byte[capacity];
+ }
+
+ public int Capacity => _buffer.Length;
+
+ public int AvailableForReading => _count;
+
+ public int RemainingForWriting => _buffer.Length - _count;
+
+ public void Write(byte[] data)
+ {
+ if (data == null)
+ {
+ throw new ArgumentNullException(nameof(data));
+ }
+ Write(data, 0, data.Length);
+ }
+
+ public void Write(byte[] data, int offset, int length)
+ {
+ if (data == null)
+ {
+ throw new ArgumentNullException(nameof(data));
+ }
+ if (offset < 0 || length < 0 || offset + length > data.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(length));
+ }
+ if (length > RemainingForWriting)
+ {
+ throw new TransportException(
+ $"Ring buffer overflow: tried to write {length} bytes, {RemainingForWriting} free.");
+ }
+
+ // At most two segments: up to the end of the backing array, then the
+ // wrapped remainder. Copying in bulk keeps the transport's read lock held
+ // for as short a time as possible.
+ var untilEnd = Math.Min(length, _buffer.Length - _writePosition);
+ Array.Copy(data, offset, _buffer, _writePosition, untilEnd);
+ if (untilEnd < length)
+ {
+ Array.Copy(data, offset + untilEnd, _buffer, 0, length - untilEnd);
+ }
+
+ _writePosition = Advance(_writePosition, length);
+ _count += length;
+ }
+
+ /// <summary>Returns the next <paramref name="numBytes"/> bytes without consuming them.</summary>
+ public byte[] Peek(int numBytes)
+ {
+ return Copy(numBytes, consume: false);
+ }
+
+ /// <summary>Returns the next <paramref name="numBytes"/> bytes and consumes them.</summary>
+ public byte[] Read(int numBytes)
+ {
+ return Copy(numBytes, consume: true);
+ }
+
+ private byte[] Copy(int numBytes, bool consume)
+ {
+ if (numBytes < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(numBytes));
+ }
+ if (numBytes > _count)
+ {
+ throw new TransportException(
+ $"Requested {numBytes} bytes but only {_count} available.");
+ }
+
+ var result = new byte[numBytes];
+ var untilEnd = Math.Min(numBytes, _buffer.Length - _readPosition);
+ Array.Copy(_buffer, _readPosition, result, 0, untilEnd);
+ if (untilEnd < numBytes)
+ {
+ Array.Copy(_buffer, 0, result, untilEnd, numBytes - untilEnd);
+ }
+
+ if (consume)
+ {
+ _readPosition = Advance(_readPosition, numBytes);
+ _count -= numBytes;
+ }
+ return result;
+ }
+
+ /// <summary>
+ /// Moves a position forward, wrapping at the end of the backing array.
+ /// <paramref name="delta"/> never exceeds the capacity, so the sum cannot
+ /// overflow in practice (.NET arrays are capped well below
+ /// <c>int.MaxValue/2</c>). The modulo is correct and clear.
+ /// </summary>
+ private int Advance(int position, int delta)
+ {
+ return (position + delta) % _buffer.Length;
+ }
+
+ public void Clear()
+ {
+ _readPosition = 0;
+ _writePosition = 0;
+ _count = 0;
+ }
+ }
+}
\ No newline at end of file
diff --git a/plc4net/spi/spi/model/values/PlcBitString.cs b/plc4net/spi/transports/TransportException.cs
similarity index 66%
copy from plc4net/spi/spi/model/values/PlcBitString.cs
copy to plc4net/spi/transports/TransportException.cs
index 42beac3bd3..482a68ae9e 100644
--- a/plc4net/spi/spi/model/values/PlcBitString.cs
+++ b/plc4net/spi/transports/TransportException.cs
@@ -17,29 +17,26 @@
// under the License.
//
-namespace org.apache.plc4net.spi.model.values
+using System;
+
+namespace org.apache.plc4net.spi.transports
{
- public class PlcBitString : PlcValueAdapter
+ /// <summary>
+ /// Raised when the underlying transport fails: connect, read, write or close.
+ /// </summary>
+ public class TransportException : Exception
{
- public PlcBitString()
- {
- }
-
- public PlcBitString(byte value)
+ public TransportException()
{
}
- public PlcBitString(ushort value)
+ public TransportException(string message) : base(message)
{
}
- public PlcBitString(uint value)
+ public TransportException(string message, Exception innerException)
+ : base(message, innerException)
{
}
-
- public PlcBitString(ulong value)
- {
- }
-
}
}
\ No newline at end of file
diff --git a/plc4net/test/spi-test/drivers/ConnectionStringTests.cs b/plc4net/test/spi-test/drivers/ConnectionStringTests.cs
new file mode 100644
index 0000000000..c9926345b8
--- /dev/null
+++ b/plc4net/test/spi-test/drivers/ConnectionStringTests.cs
@@ -0,0 +1,196 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+using System;
+using System.Globalization;
+using org.apache.plc4net.exceptions;
+using org.apache.plc4net.spi.drivers;
+using Xunit;
+
+namespace org.apache.plc4net.spi.test.drivers
+{
+ public class ConnectionStringTests
+ {
+ [Fact]
+ public void Parses_the_simple_form()
+ {
+ var cs = ConnectionString.Parse("s7://192.168.0.1");
+
+ Assert.Equal("s7", cs.ProtocolCode);
+ Assert.Null(cs.TransportCode);
+ Assert.Equal("192.168.0.1", cs.TransportConfig);
+ Assert.Equal(string.Empty, cs.ParamString);
+ }
+
+ [Fact]
+ public void Parses_the_form_with_an_explicit_transport()
+ {
+ var cs = ConnectionString.Parse("s7:cotp://10.0.0.5:102");
+
+ Assert.Equal("s7", cs.ProtocolCode);
+ Assert.Equal("cotp", cs.TransportCode);
+ Assert.Equal("10.0.0.5:102", cs.TransportConfig);
+ }
+
+ [Fact]
+ public void Parses_query_parameters()
+ {
+ var cs = ConnectionString.Parse("modbus-tcp://10.0.0.9:502?unit-identifier=1&retry=true");
+
+ Assert.Equal("1", cs.GetParameter("unit-identifier"));
+ Assert.Equal("true", cs.GetParameter("retry"));
+ }
+
+ [Fact]
+ public void URL_decodes_parameter_values()
+ {
+ var cs = ConnectionString.Parse("s7://host?remote-rack=0&remote-slot=1");
+
+ Assert.Equal("0", cs.GetParameter("remote-rack"));
+ Assert.Equal("1", cs.GetParameter("remote-slot"));
+ }
+
+ [Fact]
+ public void Percent_escapes_are_resolved()
+ {
+ var cs = ConnectionString.Parse("s7://host?path=%2Fsome%2Fpath&name=a%20b");
+
+ Assert.Equal("/some/path", cs.GetParameter("path"));
+ Assert.Equal("a b", cs.GetParameter("name"));
+ }
+
+ [Fact]
+ public void A_plus_sign_decodes_to_a_space()
+ {
+ // Java hands parameter values to URLDecoder.decode(value, UTF_8), the
+ // form-urlencoded decoder, so '+' means a space there. Uri.UnescapeDataString
+ // would leave it as a literal '+' and the same connection string would
+ // address a different device from each language.
+ var cs = ConnectionString.Parse("s7://host?name=a+b");
+
+ Assert.Equal("a b", cs.GetParameter("name"));
+ }
+
+ [Fact]
+ public void Parameters_are_case_insensitive()
+ {
+ var cs = ConnectionString.Parse("x://h?Key=val");
+
+ Assert.Equal("val", cs.GetParameter("KEY"));
+ Assert.Equal("val", cs.GetParameter("key"));
+ }
+
+ [Fact]
+ public void Bare_flags_are_read_as_true()
+ {
+ var cs = ConnectionString.Parse("s7://h?verbose");
+
+ Assert.Equal("true", cs.GetParameter("verbose"));
+ }
+
+ [Fact]
+ public void GetIntParameter_parses()
+ {
+ var cs = ConnectionString.Parse("s7://h?timeout=5000");
+
+ Assert.Equal(5000, cs.GetIntParameter("timeout", 1000));
+ }
+
+ [Fact]
+ public void GetIntParameter_does_not_depend_on_the_current_culture()
+ {
+ // A locale whose number formatting differs must not change how a connection
+ // string is read. de-DE groups with '.', so a culture-sensitive parse would
+ // read "5.000" as 5000 and could misread plain digits elsewhere.
+ var previous = CultureInfo.CurrentCulture;
+ try
+ {
+ CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("de-DE");
+ var cs = ConnectionString.Parse("s7://h?timeout=5000");
+
+ Assert.Equal(5000, cs.GetIntParameter("timeout", 1000));
+ Assert.Equal(1000, ConnectionString.Parse("s7://h?timeout=5.000")
+ .GetIntParameter("timeout", 1000));
+ }
+ finally
+ {
+ CultureInfo.CurrentCulture = previous;
+ }
+ }
+
+ [Fact]
+ public void GetIntParameter_falls_back_when_absent()
+ {
+ var cs = ConnectionString.Parse("s7://h");
+
+ Assert.Equal(1000, cs.GetIntParameter("timeout", 1000));
+ }
+
+ [Fact]
+ public void GetBoolParameter_works()
+ {
+ var cs = ConnectionString.Parse("s7://h?enabled=true&flag=false");
+
+ Assert.True(cs.GetBoolParameter("enabled", false));
+ Assert.False(cs.GetBoolParameter("flag", true));
+ Assert.True(cs.GetBoolParameter("missing", true));
+ }
+
+ [Fact]
+ public void RedactSecrets_masks_password_tokens_and_secrets()
+ {
+ var input = "s7://192.168.0.1?remote-rack=0&password=hunter2&token=abc123&secret=shh";
+
+ var redacted = ConnectionString.RedactSecrets(input);
+
+ Assert.Contains("password=***", redacted);
+ Assert.Contains("token=***", redacted);
+ Assert.Contains("secret=***", redacted);
+ // Non-secret parameters pass through.
+ Assert.Contains("remote-rack=0", redacted);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData(" ")]
+ [InlineData(null)]
+ public void Empty_connection_strings_throw(string? input)
+ {
+ Assert.Throws<PlcConnectionException>(() => ConnectionString.Parse(input));
+ }
+
+ [Fact]
+ public void Missing_protocol_code_throws()
+ {
+ Assert.Throws<PlcConnectionException>(
+ () => ConnectionString.Parse("://host"));
+ }
+
+ [Fact]
+ public void ToString_redacts_secrets()
+ {
+ var cs = ConnectionString.Parse("s7://192.168.0.1?password=secret");
+
+ var s = cs.ToString();
+
+ Assert.DoesNotContain("secret", s);
+ Assert.Contains("password=***", s);
+ }
+ }
+}
\ No newline at end of file
diff --git a/plc4net/test/spi-test/drivers/DriverBaseTests.cs b/plc4net/test/spi-test/drivers/DriverBaseTests.cs
new file mode 100644
index 0000000000..90a062e173
--- /dev/null
+++ b/plc4net/test/spi-test/drivers/DriverBaseTests.cs
@@ -0,0 +1,385 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using org.apache.plc4net.api;
+using org.apache.plc4net.api.authentication;
+using org.apache.plc4net.api.metadata;
+using org.apache.plc4net.exceptions;
+using org.apache.plc4net.messages;
+using org.apache.plc4net.model;
+using org.apache.plc4net.spi.drivers;
+using org.apache.plc4net.spi.transports;
+using Xunit;
+
+namespace org.apache.plc4net.spi.test.drivers
+{
+ /// <summary>
+ /// A minimal transport for testing driver logic without a real network.
+ /// </summary>
+ internal class FakeTransport : ITransport
+ {
+ public FakeTransport(string transportCode = "fake")
+ {
+ TransportCode = transportCode;
+ }
+
+ public string TransportCode { get; }
+ public string TransportName => "Fake Transport";
+
+ public ITransportConfiguration CreateConfiguration(IReadOnlyDictionary<string, string> parameters)
+ {
+ return new FakeTransportConfiguration();
+ }
+
+ public ITransportInstance CreateTransportInstance(
+ string transportConfig, ITransportConfiguration configuration)
+ {
+ return new FakeTransportInstance(configuration);
+ }
+ }
+
+ /// <summary>
+ /// A transport whose instance creation blocks until the test releases it, so a test can
+ /// observe what the caller does while the transport is still being created.
+ /// </summary>
+ internal class GatedTransport : ITransport
+ {
+ private readonly ManualResetEventSlim _entered;
+ private readonly ManualResetEventSlim _release;
+
+ public GatedTransport(ManualResetEventSlim entered, ManualResetEventSlim release)
+ {
+ _entered = entered;
+ _release = release;
+ }
+
+ public string TransportCode => "fake";
+ public string TransportName => "Gated Transport";
+
+ public ITransportConfiguration CreateConfiguration(IReadOnlyDictionary<string, string> parameters)
+ => new FakeTransportConfiguration();
+
+ public ITransportInstance CreateTransportInstance(
+ string transportConfig, ITransportConfiguration configuration)
+ {
+ _entered.Set();
+
+ // Bounded, so that a regression which creates the transport on the caller
+ // fails the test instead of hanging it.
+ if (!_release.Wait(TimeSpan.FromSeconds(10)))
+ {
+ throw new TimeoutException("The test never released the transport.");
+ }
+
+ return new FakeTransportInstance(configuration);
+ }
+ }
+
+ internal class FakeTransportConfiguration : ITransportConfiguration
+ {
+ }
+
+ internal class FakeTransportInstance : BaseTransportInstance
+ {
+ public FakeTransportInstance(ITransportConfiguration config) : base(config) { }
+ public override bool IsOpen => true;
+ public override int GetNumBytesAvailable() => 0;
+ public override byte[] PeekReadableBytes(int numBytes) => Array.Empty<byte>();
+ public override byte[] Read(int numBytes) => Array.Empty<byte>();
+ public override void Write(byte[] bytes) { }
+ public override void Close() { }
+ }
+
+ /// <summary>
+ /// A stub connection returned by the test driver.
+ /// </summary>
+ internal class FakeConnection : ConnectionBase
+ {
+ public FakeConnection(ConnectionString cs, ITransportInstance transport)
+ : base(cs, transport) { }
+
+ public override IPlcConnectionMetadata PlcConnectionMetadata => null!;
+ public override IPlcTag Parse(string tagQuery) => null!;
+ public override IPlcReadRequestBuilder? ReadRequestBuilder => null;
+ public override IPlcWriteRequestBuilder? WriteRequestBuilder => null;
+ public override IPlcSubscriptionRequestBuilder? SubscriptionRequestBuilder => null;
+ public override IPlcUnsubscriptionRequestBuilder? UnsubscriptionRequestBuilder => null;
+ }
+
+ /// <summary>
+ /// A stub driver wired to the "fake" transport.
+ /// </summary>
+ internal class StubDriver : DriverBase
+ {
+ private readonly string _protocolCode;
+
+ public StubDriver(ITransportManager transportManager, string protocolCode = "stub")
+ : base(transportManager)
+ {
+ _protocolCode = protocolCode;
+ }
+
+ public override string ProtocolCode => _protocolCode;
+ public override string ProtocolName => "Stub Driver";
+ public override string DefaultTransportCode => "fake";
+ protected override string[] SupportedTransportCodes => new[] { "fake" };
+
+ protected override ConnectionBase CreateConnection(
+ ConnectionString connectionString,
+ ITransportInstance transportInstance,
+ IPlcAuthentication? authentication)
+ {
+ return new FakeConnection(connectionString, transportInstance);
+ }
+ }
+
+ public class DriverBaseTests
+ {
+ [Fact]
+ public async System.Threading.Tasks.Task ConnectAsync_opens_transport_off_the_caller_thread()
+ {
+ using var entered = new ManualResetEventSlim();
+ using var release = new ManualResetEventSlim();
+ var transport = new GatedTransport(entered, release);
+ var driver = new StubDriver(new DefaultTransportManager(new[] { transport }));
+
+ // The transport cannot finish until it is released below, so ConnectAsync can only
+ // hand back a pending task if it creates the transport somewhere other than on the
+ // caller. Comparing thread ids would not show that: when the caller is a thread-pool
+ // thread, work it queues and then awaits can be picked up again by that same thread.
+ var connecting = driver.ConnectAsync("stub://192.168.0.1");
+ try
+ {
+ Assert.False(
+ connecting.IsCompleted,
+ "ConnectAsync finished while the transport was still being created.");
+ Assert.True(
+ entered.Wait(TimeSpan.FromSeconds(30)),
+ "The transport was never created.");
+ }
+ finally
+ {
+ release.Set();
+ }
+
+ using var connection = await connecting;
+ Assert.True(connection.IsConnected);
+ }
+
+ [Fact]
+ public void Connect_resolves_the_default_transport()
+ {
+ var transportManager = new DefaultTransportManager(new[] { new FakeTransport() });
+ var driver = new StubDriver(transportManager);
+
+ var connection = driver.Connect("stub://192.168.0.1");
+ Assert.True(connection.IsConnected);
+ }
+
+ [Fact]
+ public void Connect_rejects_an_unsupported_transport()
+ {
+ var transportManager = new DefaultTransportManager(new[] { new FakeTransport() });
+ var driver = new StubDriver(transportManager);
+
+ var ex = Assert.Throws<PlcConnectionException>(
+ () => driver.Connect("stub:unregistered://host"));
+ Assert.Contains("unregistered", ex.Message);
+ }
+
+ [Fact]
+ public void Connect_rejects_a_registered_but_unsupported_transport()
+ {
+ // "other" is registered, so it exists — but StubDriver does not list it in
+ // SupportedTransportCodes, so the driver's own check has to reject it.
+ var transportManager = new DefaultTransportManager(
+ new[] { new FakeTransport(), new FakeTransport("other") });
+ var driver = new StubDriver(transportManager);
+
+ var ex = Assert.Throws<PlcConnectionException>(
+ () => driver.Connect("stub:other://host"));
+ Assert.Contains("allow-unsupported-transport", ex.Message);
+ }
+
+ [Fact]
+ public void Connect_allows_unsupported_transport_when_opted_in()
+ {
+ var transportManager = new DefaultTransportManager(
+ new[] { new FakeTransport(), new FakeTransport("other") });
+ var driver = new StubDriver(transportManager);
+
+ // Same connection string as above, plus the opt-in flag.
+ var connection = driver.Connect("stub:other://host?allow-unsupported-transport=true");
+ Assert.True(connection.IsConnected);
+ }
+
+ [Fact]
+ public void Connect_rejects_a_wrong_protocol_code()
+ {
+ var transportManager = new DefaultTransportManager(new[] { new FakeTransport() });
+ var driver = new StubDriver(transportManager);
+
+ Assert.Throws<PlcConnectionException>(
+ () => driver.Connect("wrong-protocol://host"));
+ }
+
+ [Fact]
+ public void Connect_matches_the_protocol_code_case_insensitively()
+ {
+ // Connection strings are lowercase by grammar, but a driver is free to
+ // declare its code in any casing.
+ var transportManager = new DefaultTransportManager(new[] { new FakeTransport() });
+ var driver = new StubDriver(transportManager, "Stub");
+
+ var connection = driver.Connect("stub://192.168.0.1");
+ Assert.True(connection.IsConnected);
+ }
+
+ [Fact]
+ public void Connect_runs_the_protocol_level_setup_hook()
+ {
+ var transportManager = new DefaultTransportManager(new[] { new FakeTransport() });
+ var driver = new HookedDriver(transportManager);
+
+ var connection = (HookedConnection)driver.Connect("hooked://192.168.0.1");
+
+ // The hook must have run by the time Connect() returns, so callers never
+ // receive a connection whose protocol session is still unestablished.
+ Assert.Equal(1, connection.OnConnectCalls);
+ }
+
+ [Fact]
+ public void Connect_closes_the_transport_when_the_setup_hook_fails()
+ {
+ var transportManager = new DefaultTransportManager(new[] { new FakeTransport() });
+ var driver = new HookedDriver(transportManager, failOnConnect: true);
+
+ Assert.Throws<PlcConnectionException>(() => driver.Connect("hooked://192.168.0.1"));
+
+ // The caller never gets a reference to the connection, so if Connect() did not
+ // close the transport itself the socket would stay bound with no way to reach it.
+ Assert.True(driver.LastTransportInstance!.Closed);
+ }
+
+ [Fact]
+ public void Connect_wraps_a_non_connection_exception_and_still_closes()
+ {
+ var transportManager = new DefaultTransportManager(new[] { new FakeTransport() });
+ // A handshake failure surfaces as TransportException, not PlcConnectionException
+ // (this is exactly what a real S7 COTP timeout does). The driver API promises
+ // PlcConnectionException on every failure path, so the wrap must kick in and
+ // the transport must still be closed.
+ var driver = new HookedDriver(transportManager, failOnConnect: true, failWithTransportException: true);
+
+ var ex = Assert.Throws<PlcConnectionException>(() => driver.Connect("hooked://192.168.0.1"));
+ Assert.IsType<TransportException>(ex.InnerException);
+ Assert.True(driver.LastTransportInstance!.Closed);
+ }
+ }
+
+ /// <summary>
+ /// A connection that records whether the protocol-level setup hook ran, and can be
+ /// told to fail inside it.
+ /// </summary>
+ internal class HookedConnection : ConnectionBase
+ {
+ private readonly bool _failOnConnect;
+ private readonly bool _failWithTransportException;
+
+ public HookedConnection(ConnectionString cs, ITransportInstance transport,
+ bool failOnConnect, bool failWithTransportException)
+ : base(cs, transport)
+ {
+ _failOnConnect = failOnConnect;
+ _failWithTransportException = failWithTransportException;
+ }
+
+ public int OnConnectCalls { get; private set; }
+
+ protected override void OnConnect()
+ {
+ OnConnectCalls++;
+ if (_failOnConnect)
+ {
+ throw _failWithTransportException
+ ? new TransportException("Simulated handshake failure.")
+ : new PlcConnectionException("Simulated handshake failure.");
+ }
+ }
+
+ public override IPlcConnectionMetadata PlcConnectionMetadata => null!;
+ public override IPlcTag Parse(string tagQuery) => null!;
+ public override IPlcReadRequestBuilder? ReadRequestBuilder => null;
+ public override IPlcWriteRequestBuilder? WriteRequestBuilder => null;
+ public override IPlcSubscriptionRequestBuilder? SubscriptionRequestBuilder => null;
+ public override IPlcUnsubscriptionRequestBuilder? UnsubscriptionRequestBuilder => null;
+ }
+
+ /// <summary>
+ /// A transport instance that reports whether it was closed.
+ /// </summary>
+ internal class ClosableTransportInstance : BaseTransportInstance
+ {
+ public ClosableTransportInstance(ITransportConfiguration config) : base(config) { }
+
+ public bool Closed { get; private set; }
+
+ public override bool IsOpen => !Closed;
+ public override int GetNumBytesAvailable() => 0;
+ public override byte[] PeekReadableBytes(int numBytes) => Array.Empty<byte>();
+ public override byte[] Read(int numBytes) => Array.Empty<byte>();
+ public override void Write(byte[] bytes) { }
+ public override void Close() => Closed = true;
+ }
+
+ internal class HookedDriver : DriverBase
+ {
+ private readonly bool _failOnConnect;
+ private readonly bool _failWithTransportException;
+
+ public HookedDriver(ITransportManager transportManager,
+ bool failOnConnect = false, bool failWithTransportException = false)
+ : base(transportManager)
+ {
+ _failOnConnect = failOnConnect;
+ _failWithTransportException = failWithTransportException;
+ }
+
+ public ClosableTransportInstance? LastTransportInstance { get; private set; }
+
+ public override string ProtocolCode => "hooked";
+ public override string ProtocolName => "Hooked Driver";
+ public override string DefaultTransportCode => "fake";
+ protected override string[] SupportedTransportCodes => new[] { "fake" };
+
+ protected override ConnectionBase CreateConnection(
+ ConnectionString connectionString,
+ ITransportInstance transportInstance,
+ IPlcAuthentication? authentication)
+ {
+ // Swap in an instance that reports closure, so the failure path can be asserted.
+ LastTransportInstance = new ClosableTransportInstance(transportInstance.Configuration);
+ return new HookedConnection(connectionString, LastTransportInstance,
+ _failOnConnect, _failWithTransportException);
+ }
+ }
+}
\ No newline at end of file
diff --git a/plc4net/test/spi-test/drivers/MessageCodecBaseTests.cs b/plc4net/test/spi-test/drivers/MessageCodecBaseTests.cs
new file mode 100644
index 0000000000..fe1968647f
--- /dev/null
+++ b/plc4net/test/spi-test/drivers/MessageCodecBaseTests.cs
@@ -0,0 +1,176 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+using System;
+using System.Collections.Generic;
+using org.apache.plc4net.spi.drivers;
+using org.apache.plc4net.spi.generation;
+using org.apache.plc4net.spi.transports;
+using Xunit;
+
+namespace org.apache.plc4net.spi.test.drivers
+{
+ /// <summary>
+ /// The framing loop shared by every codec: partial frames, several frames in one
+ /// read, and - the point of these tests - resynchronising after the byte stream is
+ /// left misaligned (a dropped datagram, a truncated or malformed frame).
+ /// </summary>
+ public class MessageCodecBaseTests
+ {
+ // 4-byte header: 0xAA 0xBB then a big-endian total length (header + body).
+ private sealed class ToyCodec : MessageCodecBase<byte[]>
+ {
+ public ToyCodec(ITransportInstance transport, Action<byte[]> handler)
+ : base("toy", transport, handler)
+ {
+ }
+
+ protected override int GetMinimumHeaderSize() => 4;
+
+ protected override int CalculateTotalMessageSize(byte[] header, int availableBytes)
+ {
+ if (header[0] != 0xAA || header[1] != 0xBB)
+ {
+ throw new MessageCodecException("bad magic");
+ }
+ var total = (header[2] << 8) | header[3];
+ if (total < 4)
+ {
+ throw new MessageCodecException("length below header size");
+ }
+ return total;
+ }
+
+ protected override byte[] ParseMessage(ReadBuffer readBuffer) => readBuffer.GetBytes();
+ }
+
+ private static byte[] Frame(params byte[] body)
+ {
+ var total = 4 + body.Length;
+ var frame = new byte[total];
+ frame[0] = 0xAA;
+ frame[1] = 0xBB;
+ frame[2] = (byte)(total >> 8);
+ frame[3] = (byte)total;
+ Array.Copy(body, 0, frame, 4, body.Length);
+ return frame;
+ }
+
+ private static (ToyCodec codec, List<byte[]> received) Build(byte[] buffered)
+ {
+ var transport = new BufferBackedTransport(buffered);
+ var received = new List<byte[]>();
+ return (new ToyCodec(transport, received.Add), received);
+ }
+
+ [Fact]
+ public void Delivers_a_whole_frame()
+ {
+ var (codec, received) = Build(Frame(1, 2, 3));
+ codec.ProcessIncomingData();
+ Assert.Single(received);
+ Assert.Equal(new byte[] { 0xAA, 0xBB, 0x00, 0x07, 1, 2, 3 }, received[0]);
+ }
+
+ [Fact]
+ public void Leaves_a_partial_frame_buffered()
+ {
+ var whole = Frame(1, 2, 3);
+ var (codec, received) = Build(new[] { whole[0], whole[1], whole[2], whole[3], whole[4] });
+ codec.ProcessIncomingData();
+ Assert.Empty(received);
+ }
+
+ [Fact]
+ public void Resyncs_past_leading_garbage_to_the_next_valid_frame()
+ {
+ var junk = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 };
+ var good = Frame(9, 9);
+ var buffer = new byte[junk.Length + good.Length];
+ Array.Copy(junk, buffer, junk.Length);
+ Array.Copy(good, 0, buffer, junk.Length, good.Length);
+
+ var (codec, received) = Build(buffer);
+ codec.ProcessIncomingData();
+
+ Assert.Single(received);
+ Assert.Equal(good, received[0]);
+ }
+
+ [Fact]
+ public void Resyncs_past_a_frame_with_an_impossible_length()
+ {
+ // A valid magic but a length of 2 (below the 4-byte header): the stream is
+ // misaligned. The codec must skip it, not consume 2 bytes and stay off.
+ var bad = new byte[] { 0xAA, 0xBB, 0x00, 0x02 };
+ var good = Frame(7);
+ var buffer = new byte[bad.Length + good.Length];
+ Array.Copy(bad, buffer, bad.Length);
+ Array.Copy(good, 0, buffer, bad.Length, good.Length);
+
+ var (codec, received) = Build(buffer);
+ codec.ProcessIncomingData();
+
+ Assert.Single(received);
+ Assert.Equal(good, received[0]);
+ }
+
+ /// <summary>An <see cref="ITransportInstance"/> that just serves a fixed byte buffer.</summary>
+ private sealed class BufferBackedTransport : ITransportInstance
+ {
+ private readonly List<byte> _buffer;
+
+ public BufferBackedTransport(byte[] initial) => _buffer = new List<byte>(initial);
+
+ public ITransportConfiguration Configuration => null!;
+ public string DriverConfig => string.Empty;
+ public bool IsOpen => true;
+
+ public int GetNumBytesAvailable() => _buffer.Count;
+
+ public byte[] PeekReadableBytes(int numBytes)
+ {
+ if (_buffer.Count < numBytes)
+ {
+ throw new TransportException("not enough buffered");
+ }
+ return _buffer.GetRange(0, numBytes).ToArray();
+ }
+
+ public byte[] Read(int numBytes)
+ {
+ var slice = PeekReadableBytes(numBytes);
+ _buffer.RemoveRange(0, numBytes);
+ return slice;
+ }
+
+ public void Write(byte[] bytes)
+ {
+ }
+
+ public void Close()
+ {
+ }
+
+ public void Dispose()
+ {
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/plc4net/test/spi-test/drivers/MessagesTests.cs b/plc4net/test/spi-test/drivers/MessagesTests.cs
new file mode 100644
index 0000000000..b5afad3213
--- /dev/null
+++ b/plc4net/test/spi-test/drivers/MessagesTests.cs
@@ -0,0 +1,126 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+using System.Threading;
+using System.Threading.Tasks;
+using org.apache.plc4net.api.value;
+using org.apache.plc4net.messages;
+using org.apache.plc4net.spi.drivers.functions;
+using org.apache.plc4net.spi.drivers.messages;
+using org.apache.plc4net.types;
+using Xunit;
+
+namespace org.apache.plc4net.spi.test.drivers
+{
+ /// <summary>
+ /// Covers the read/write request builders and their default implementations.
+ /// </summary>
+ public class MessagesTests
+ {
+ private class NoOpReader : PlcReader
+ {
+ public Task<IPlcReadResponse> Read(DefaultPlcReadRequest request,
+ CancellationToken cancellationToken = default)
+ {
+ var values = new System.Collections.Generic.Dictionary<string,
+ spi.drivers.messages.items.PlcResponseItem<IPlcValue>>();
+ foreach (var name in request.TagNames)
+ {
+ values[name] = new spi.drivers.messages.items.DefaultPlcResponseItem<IPlcValue>(
+ PlcResponseCode.Ok, null);
+ }
+ return Task.FromResult<IPlcReadResponse>(new DefaultPlcReadResponse(request, values));
+ }
+ }
+
+ private class NoOpWriter : PlcWriter
+ {
+ public Task<IPlcWriteResponse> Write(DefaultPlcWriteRequest request,
+ CancellationToken cancellationToken = default)
+ {
+ var codes = new System.Collections.Generic.Dictionary<string, PlcResponseCode>();
+ foreach (var name in request.TagNames)
+ {
+ codes[name] = PlcResponseCode.Ok;
+ }
+ return Task.FromResult<IPlcWriteResponse>(new DefaultPlcWriteResponse(request, codes));
+ }
+ }
+
+ [Fact]
+ public void A_read_request_can_be_built_with_tag_addresses()
+ {
+ var builder = new DefaultPlcReadRequestBuilder(new NoOpReader(), null);
+
+ var request = builder
+ .AddTagAddress("a", "coil:1")
+ .AddTagAddress("b", "coil:2")
+ .Build();
+
+ Assert.NotNull(request);
+ var tagRequest = (IPlcTagRequest)request;
+ Assert.Equal(2, tagRequest.TagCount);
+ Assert.Contains("a", tagRequest.TagNames);
+ Assert.Contains("b", tagRequest.TagNames);
+ }
+
+ [Fact]
+ public async Task A_read_request_executes_and_returns_a_response()
+ {
+ var request = new DefaultPlcReadRequestBuilder(new NoOpReader(), null)
+ .AddTagAddress("x", "holding:10")
+ .Build();
+
+ var response = (DefaultPlcReadResponse)await request.ExecuteAsync();
+
+ Assert.NotNull(response);
+ Assert.NotNull(response.Request);
+ Assert.Equal(PlcResponseCode.Ok, response.GetResponseCode("x"));
+ }
+
+ [Fact]
+ public void A_write_request_can_be_built_with_tag_values()
+ {
+ var builder = new DefaultPlcWriteRequestBuilder(new NoOpWriter(), null);
+
+ var request = builder
+ .AddTag("tag1", "coil:0", true)
+ .AddTag("tag2", "holding:1", (short)42)
+ .Build();
+
+ Assert.NotNull(request);
+ Assert.Equal(2, request.NumberOfValues);
+ var tagRequest = (IPlcTagRequest)request;
+ Assert.Equal(2, tagRequest.TagCount);
+ }
+
+ [Fact]
+ public async Task A_write_request_executes_and_returns_a_response()
+ {
+ var request = new DefaultPlcWriteRequestBuilder(new NoOpWriter(), null)
+ .AddTag("tag1", "coil:0", true)
+ .Build();
+
+ var response = (DefaultPlcWriteResponse)await request.ExecuteAsync();
+
+ Assert.NotNull(response);
+ Assert.Equal(PlcResponseCode.Ok, response.GetResponseCode("tag1"));
+ }
+ }
+}
\ No newline at end of file
diff --git a/plc4net/test/spi-test/drivers/PlcDriverManagerTests.cs b/plc4net/test/spi-test/drivers/PlcDriverManagerTests.cs
new file mode 100644
index 0000000000..e9eda44dcf
--- /dev/null
+++ b/plc4net/test/spi-test/drivers/PlcDriverManagerTests.cs
@@ -0,0 +1,128 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+using System;
+using org.apache.plc4net.api;
+using org.apache.plc4net.api.authentication;
+using org.apache.plc4net.exceptions;
+using Xunit;
+
+namespace org.apache.plc4net.spi.test.drivers
+{
+ /// <summary>
+ /// A driver that only carries a protocol code; connecting is out of scope here.
+ /// </summary>
+ internal class RegistryOnlyDriver : IPlcDriver
+ {
+ public RegistryOnlyDriver(string protocolCode)
+ {
+ ProtocolCode = protocolCode;
+ }
+
+ public string ProtocolCode { get; }
+
+ public string ProtocolName => "Registry Only Driver";
+
+ public IPlcConnection Connect(string connectionString)
+ {
+ throw new NotSupportedException();
+ }
+
+ public IPlcConnection Connect(string connectionString, IPlcAuthentication? authentication)
+ {
+ throw new NotSupportedException();
+ }
+ }
+
+ /// <summary>
+ /// The manager is a process-wide singleton, so each test registers under its own
+ /// protocol code rather than trying to reset shared state.
+ /// </summary>
+ public class PlcDriverManagerTests
+ {
+ [Fact]
+ public void A_driver_is_found_under_the_code_it_registered_with()
+ {
+ var driver = new RegistryOnlyDriver("registry-plain");
+ PlcDriverManager.Instance.RegisterDriver(driver);
+
+ Assert.Same(driver, PlcDriverManager.Instance.GetDriverByCode("registry-plain"));
+ }
+
+ [Fact]
+ public void A_mixed_case_driver_code_is_found_by_a_lowercase_lookup()
+ {
+ // Connection strings are lowercase by grammar, so a driver declaring
+ // "Registry-Mixed" still has to be reachable via "registry-mixed".
+ var driver = new RegistryOnlyDriver("Registry-Mixed");
+ PlcDriverManager.Instance.RegisterDriver(driver);
+
+ Assert.Same(driver, PlcDriverManager.Instance.GetDriverByCode("registry-mixed"));
+ Assert.Same(driver, PlcDriverManager.Instance.GetDriverByCode("REGISTRY-MIXED"));
+ }
+
+ [Fact]
+ public void An_unknown_code_reports_which_protocol_was_asked_for()
+ {
+ var ex = Assert.Throws<PlcConnectionException>(
+ () => PlcDriverManager.Instance.GetDriverByCode("registry-absent"));
+
+ Assert.Contains("registry-absent", ex.Message);
+ }
+
+ [Fact]
+ public void A_null_code_is_rejected_like_an_unknown_one()
+ {
+ Assert.Throws<PlcConnectionException>(
+ () => PlcDriverManager.Instance.GetDriverByCode(null));
+ }
+
+ [Fact]
+ public void Registering_null_is_rejected()
+ {
+ Assert.Throws<ArgumentNullException>(
+ () => PlcDriverManager.Instance.RegisterDriver(null!));
+ }
+
+ [Fact]
+ public void GetDriver_extracts_the_protocol_code_from_a_connection_string()
+ {
+ var driver = new RegistryOnlyDriver("s7");
+ PlcDriverManager.Instance.RegisterDriver(driver);
+
+ // The api layer only needs the scheme to dispatch to the right driver.
+ // This mirrors Java's DefaultPlcDriverManager.getDriverForUrl(), which
+ // uses new URI(url).getScheme().
+ Assert.Same(driver, PlcDriverManager.Instance.GetDriver("s7://192.168.0.1"));
+ Assert.Same(driver, PlcDriverManager.Instance.GetDriver("s7:cotp://10.0.0.5:102"));
+ }
+
+ [Fact]
+ public void GetDriver_extracts_the_protocol_code_from_a_transport_shorthand()
+ {
+ // The code must not collide with a real driver's: the manager is a global
+ // singleton, so registering a stub under "modbus-tcp" would make whichever
+ // test runs second see the other's driver.
+ var driver = new RegistryOnlyDriver("registry-shorthand");
+ PlcDriverManager.Instance.RegisterDriver(driver);
+
+ Assert.Same(driver, PlcDriverManager.Instance.GetDriver("registry-shorthand://10.0.0.9:502?unit-identifier=1"));
+ }
+ }
+}
\ No newline at end of file
diff --git a/plc4net/test/spi-test/generation/BufferTests.cs b/plc4net/test/spi-test/generation/BufferTests.cs
new file mode 100644
index 0000000000..d7bee9897c
--- /dev/null
+++ b/plc4net/test/spi-test/generation/BufferTests.cs
@@ -0,0 +1,230 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+using System;
+using System.Text;
+using org.apache.plc4net.spi.generation;
+using Xunit;
+
+namespace org.apache.plc4net.spi.test.generation
+{
+ /// <summary>
+ /// Round-trip coverage for the bit-level codec every generated driver depends on.
+ /// </summary>
+ public class BufferTests
+ {
+ private static ReadBuffer Reader(Action<WriteBuffer> write)
+ {
+ var wb = new WriteBuffer();
+ write(wb);
+ return new ReadBuffer(wb.GetBytes());
+ }
+
+ [Fact]
+ public void Bits_are_written_and_read_most_significant_first()
+ {
+ var wb = new WriteBuffer();
+ wb.WriteBit("", true);
+ wb.WriteBit("", false);
+ wb.WriteBit("", true);
+ wb.WriteBit("", false);
+ wb.WriteBit("", false);
+ wb.WriteBit("", false);
+ wb.WriteBit("", false);
+ wb.WriteBit("", true);
+
+ Assert.Equal(new byte[] { 0xA1 }, wb.GetBytes());
... 1347 lines suppressed ...