Skip to content

【WIP】feat(plc4net): revive the .NET port — net8.0, SPI3 alignment, driver runtime - #2656

Draft
TomNewChao wants to merge 15 commits into
apache:developfrom
openIndu:feature/plc4net-revival
Draft

【WIP】feat(plc4net): revive the .NET port — net8.0, SPI3 alignment, driver runtime#2656
TomNewChao wants to merge 15 commits into
apache:developfrom
openIndu:feature/plc4net-revival

Conversation

@TomNewChao

@TomNewChao TomNewChao commented Jul 26, 2026

Copy link
Copy Markdown

Revives plc4net, which the README currently lists as "not ready for usage - abandoned", to the point where it builds on all three platforms, has an executing test suite, and has a driver runtime aligned with SPI3.

Relates to #2655. Follows the dev@ thread from 2026-07-24 where Christofer suggested a PR as the way to get started.

Reviewing this

The six commits are independent and each stands alone. Reviewing commit by commit is much easier than reviewing the combined diff.

Commit Change Size
fade923 Retarget net452net8.0; hoist shared properties into Directory.Build.props; add Microsoft.NET.Test.Sdk 61+/37−
d3b4632 Make IPlcValue dispatch reach the concrete value types; add missing accessors 466+/148−
f95f216 Replace Ayx.BitIO with an in-house BitReader/BitWriter; repair ReadBuffer/WriteBuffer; fix ParseException 693+/146−
76cfe5b Add .github/workflows/dotnet-platform.yml (ubuntu/macos/windows) 120+
7285a3f Align the API with SPI3: PlcFieldPlcTag, synchronous Connect, ConnectionString 322+/177−
7486c4d Implement the driver runtime and a TCP transport 2082+

Why each one is needed

fade923 — net8.0. net452 builds fine on Windows but cannot build on Linux or macOS runners, which is why plc4net was never in the CI matrix. This commit also adds Microsoft.NET.Test.Sdk to the test projects; without it the suites compiled but never executed. That single omission is why everything below went unnoticed.

d3b4632 — the value model was dead through its own interface. Subclasses declared public new bool GetBool() rather than override. In C# new is compile-time name hiding and does not participate in virtual dispatch, so a caller holding an IPlcValue — which is how the API hands values out — landed on PlcValueAdapter's defaults. ((IPlcValue) new PlcDINT(42)).GetInt() returned 0. PlcBOOL, PlcSTRING, PlcCHAR, PlcWCHAR and PlcWSTRING additionally stored a value but never exposed it at all.

f95f216 — the bit codec. Ayx.BitIO is net45-only and unmaintained since 2016; internally it round-trips reads through a StringBuilder of '0'/'1' characters and throws on a 32-bit field, so plc4net could not read a float. It is replaced with an MSB-first BitReader/BitWriter (~240 lines, no dependencies). Fixing the codec then exposed the layer above it: ReadUlong/ReadLong consumed 2*bitLength-32 bits, WriteFloat/WriteDouble passed arguments in the wrong order, the 16-bit float branch was an empty block, ReadDouble(32) always threw, and ReadString/ReadByteArray/WriteString were NotImplementedException. ParseException did not derive from Exception.

The KNX DPT 9.x 16-bit format is implemented explicitly rather than as IEEE-754 half. One test documents that the format is coarse at the top of its range (step 327.68 at exponent 15) so the next person does not read it as a rounding bug.

7285a3f — SPI3 alignment. PlcFieldPlcTag, ConnectAsync → synchronous Connect, and a ConnectionString parser whose grammar is character-for-character the Java DriverBase.URI_PATTERN.

7486c4d — the driver runtime. DriverBase (connection-string dispatch, transport selection, supported-transport enforcement), ConnectionBase, MessageCodecBase<T>, the transport abstraction, and a TCP transport.

Relationship to the Java SPI3 contracts

plc4net plc4j
spi/drivers/DriverBase.cs plc4j/spi/drivers/.../spi/drivers/DriverBase.java
spi/drivers/MessageCodecBase.cs plc4j/spi/drivers/.../spi/drivers/MessageCodecBase.java
spi/transports/ITransportInstance.cs plc4j/transports/api/.../spi/transports/api/TransportInstance.java
api/api/ConnectionString.cs DriverBase.URI_PATTERN plus parameter parsing

Four places I deliberately diverged

Flagging these because they are the most likely thing to be objected to, and I would rather discuss them than have them found in review.

  1. ConnectionString is a public type in the API, not the SPI. It has no SPI dependency, and PlcDriverManager — which lives in the API — needs it to route the protocol code.
  2. Getters became C# properties (IsOpen, ProtocolCode). .NET convention.
  3. ITransportInstance extends IDisposable. .NET resource convention; enables using.
  4. The TCP read loop is async/await, not a virtual thread. .NET has no virtual threads; the async socket path is the equivalent.

If you would rather have a literal transliteration of the Java API, say so and I will change these — but the .NET ecosystem expectations argued for the above.

What is not here

  • Generated code has no parse/serialize — see the question below. This is the reason there is still no real driver.
  • No request/response correlation or timeouts.
  • Only a TCP transport. No UDP, serial, TLS, or COTP (S7 needs COTP).
  • Request builders are interfaces only; no implementations yet.
  • No tag-address parsing framework.
  • KNXnet/IP is still generated models only, not a driver.

Verification

  • dotnet build plc4net/plc4net.sln — 0 errors, 0 warnings
  • dotnet test plc4net/plc4net.sln — 77 passing (76 SPI, 1 KNX)
  • The C# generator was re-run against knxnetip.mspec; output is byte-for-byte identical to the checked-in .cs, so this PR does not drift from the protocol descriptions

Value-model tests assert exclusively through IPlcValue rather than the concrete types. That is deliberate: the new/override defect was invisible to tests bound to concrete types and only visible through the interface.

CI is the one thing I cannot verify from here — dotnet-platform.yml has never run in this repository's environment. If it fails I will fix it on the branch.

The question I would most like answered

CsLanguageOutput.getComplexTypeTemplates() registers only model-template.cs.ftlh. An io-template.cs.ftlh exists beside it but is not registered, and its body is still verbatim Java (implements MessageInput<>, @Override, throws ParseException). So generating a protocol for C# yields data classes and nothing that can parse or serialize a frame.

Java has since moved to the code-based generators under org.apache.plc4x.codegeneration.language.java; go, c and python are still on freemarker.

Should C# stay on freemarker and get a real io-template.cs.ftlh, or follow Java to a code-based generator?

I will do either. If freemarker is acceptable as an interim, I would start with a minimal KNXnet/IP round-trip as proof — which doubles as the specification for a code-based generator later, so neither path wastes the other's work.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Revives the plc4net (.NET) port by modernizing it to .NET 8, restoring test execution, aligning key API/SPI surfaces with PLC4X SPI3 concepts, and adding a foundational driver runtime plus a TCP transport implementation.

Changes:

  • Retarget projects to net8.0 and centralize shared build/package properties via Directory.Build.props.
  • Fix the PLC value model’s virtual dispatch and add a bit-level codec (BitReader/BitWriter) + repaired ReadBuffer/WriteBuffer.
  • Add SPI3-aligned runtime building blocks (connection-string parsing, driver/connection bases, message codec) and a TCP transport with CI workflow coverage.

Reviewed changes

Copilot reviewed 49 out of 49 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
plc4net/transports/tcp/TcpTransportInstance.cs Adds async TCP transport instance with background read loop + ring buffer.
plc4net/transports/tcp/TcpTransportConfiguration.cs Defines TCP transport configuration defaults and options.
plc4net/transports/tcp/TcpTransport.cs Implements TCP transport factory + address parsing and option parsing.
plc4net/transports/tcp/plc4net-transport-tcp.csproj Introduces TCP transport project.
plc4net/spi/spi/transports/TransportException.cs Adds transport-specific exception type.
plc4net/spi/spi/transports/RingBuffer.cs Adds fixed-capacity ring buffer used by transports/codecs.
plc4net/spi/spi/transports/ITransportInstance.cs Introduces transport instance contracts (sync + async listener variant).
plc4net/spi/spi/transports/ITransport.cs Introduces transport factory + transport manager registry.
plc4net/spi/spi/transports/BaseTransportInstance.cs Adds base transport instance with config + driver-config handling and Dispose.
plc4net/spi/spi/model/values/PlcWSTRING.cs Fixes string value dispatch/exposure.
plc4net/spi/spi/model/values/PlcWORD.cs Fixes overridden bit-accessors for WORD.
plc4net/spi/spi/model/values/PlcWCHAR.cs Fixes string dispatch/exposure for WCHAR.
plc4net/spi/spi/model/values/PlcValueAdapter.cs Makes IPlcValue API virtual to enable correct overriding/dispatch.
plc4net/spi/spi/model/values/PlcSTRING.cs Fixes string dispatch/exposure.
plc4net/spi/spi/model/values/PlcSimpleValueAdapter.cs Fixes overriding for “simple value” classification.
plc4net/spi/spi/model/values/PlcSimpleNumericValueAdapter.cs Fixes numeric conversions/range checks and interface dispatch.
plc4net/spi/spi/model/values/PlcLWORD.cs Fixes overridden bit-accessors for LWORD.
plc4net/spi/spi/model/values/PlcDWORD.cs Fixes overridden bit-accessors for DWORD.
plc4net/spi/spi/model/values/PlcCHAR.cs Fixes string dispatch/exposure for CHAR.
plc4net/spi/spi/model/values/PlcBYTE.cs Fixes overridden bit-accessors for BYTE.
plc4net/spi/spi/model/values/PlcBOOL.cs Fixes BOOL accessor dispatch and adds conversions.
plc4net/spi/spi/generation/WriteBuffer.cs Reworks write buffer to use in-house bit writer + fixes float/string/array writing.
plc4net/spi/spi/generation/ReadBuffer.cs Reworks read buffer to use in-house bit reader + fixes numeric/string/array reading.
plc4net/spi/spi/generation/ParseException.cs Makes ParseException a real Exception type.
plc4net/spi/spi/generation/BitWriter.cs Adds MSB-first bit writer.
plc4net/spi/spi/generation/BitReader.cs Adds MSB-first bit reader.
plc4net/spi/spi/drivers/MessageCodecBase.cs Adds SPI3-like message codec base and IMessage contract.
plc4net/spi/spi/drivers/DriverBase.cs Adds SPI3-like driver base (transport resolution + connection creation).
plc4net/spi/spi/drivers/ConnectionBase.cs Adds SPI3-like connection base wrapping a transport instance.
plc4net/spi/plc4net-spi.csproj Removes net45-only dependency and aligns packaging with shared props.
plc4net/spi-test/test/transports/TcpTransportAddressTests.cs Adds tests for TCP transport address parsing.
plc4net/spi-test/test/transports/RingBufferTests.cs Adds ring buffer unit tests.
plc4net/spi-test/test/model/values/PlcValueTests.cs Adds interface-dispatch-focused value model tests.
plc4net/spi-test/test/generation/BufferTests.cs Adds codec round-trip tests for bit reader/writer and buffers.
plc4net/spi-test/test/drivers/DriverBaseTests.cs Adds driver-base/transport-selection tests.
plc4net/spi-test/test/drivers/ConnectionStringTests.cs Adds tests for SPI3-aligned connection string parsing + secret redaction.
plc4net/spi-test/plc4net-spi-test.csproj Adds dedicated SPI test project with proper test SDK refs.
plc4net/plc4net.sln Updates solution to include new test + transport projects and platforms.
plc4net/drivers/knxnetip/plc4net-driver-knxproj.csproj Updates KNX driver project dependencies (e.g., NLog).
plc4net/drivers/knxnetip-test/test/knxnetip/readwrite/model/KnxDatapointTests.cs Fixes KNX test vector and asserts float parsing.
plc4net/drivers/knxnetip-test/plc4net-driver-knxproj-test.csproj Ensures test suite actually runs (adds Microsoft.NET.Test.Sdk, marks non-packable).
plc4net/Directory.Build.props Centralizes target framework + shared packaging/build properties.
plc4net/api/PlcDriverManager.cs Refactors driver manager to SPI3-style registry and sync connection creation.
plc4net/api/plc4net-api.csproj Aligns API project with centralized build props.
plc4net/api/api/model/IPlcTag.cs Renames Field→Tag concept for SPI3 alignment.
plc4net/api/api/IPlcDriver.cs Updates driver contract to sync SPI3-like Connect() methods.
plc4net/api/api/IPlcConnection.cs Updates connection contract to sync Close() + tag parsing.
plc4net/api/api/ConnectionString.cs Adds SPI3-aligned connection-string parser + secret redaction.
.github/workflows/dotnet-platform.yml Adds cross-platform CI job for building and running .NET tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread plc4net/transports/tcp/TcpTransport.cs
Comment thread plc4net/api/PlcDriverManager.cs
Comment thread plc4net/spi/spi/transports/RingBuffer.cs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 55 out of 55 changed files in this pull request and generated 3 comments.

Comment thread plc4net/api/api/ConnectionString.cs
Comment thread plc4net/api/api/ConnectionString.cs
Comment thread plc4net/transports/tcp/TcpTransportInstance.cs Outdated
@sruehl
sruehl requested a review from Copilot July 27, 2026 08:47
@chrisdutz

Copy link
Copy Markdown
Contributor

Wow ... quite a bit happened here during my few days off :-)

So one thing I wanted to point out: Even if in Java several external dependencies existed for various things (such as BitBuffers etc), I still rewrote everything from scratch in SPI3. One the one side, this way I could build a perfect fit for PLC4X, but also I thought this would be a perfect template for porting to other languages as there is no need to fins a "suitable but mostly not 100% replacement".

Another thing I noticed: We currently rely on the plc4x build-tools maven plugin to generate code. In my commercial offering I decided to give something else a try: I built an mspec parser and code-generator that fits the target language (in that case Rust) ... possibly it might be worth investigating the options to use a pure dotnet code generation tool ... the antlr4 grammar for mspec should help a lot with that. In the end a pure dotnet toolchain would eliminate the dependency on Java for dotnet developers while still being able to call the build from the overall maven reactor.

I think the API diverged quite a bit since it was created several years ago, but i think I saw you already picked up some of these changes.

If you need any help ...don't hesitate pinging me. I can also invite you in the plc4x slack channel, if you want more instant feedback (during sensible EU times ;-) )

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 55 out of 55 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (2)

plc4net/transports/tcp/TcpTransportInstance.cs:1

  • On orderly remote shutdown (bytesRead == 0), the socket is not disposed here. Because _open is set to 0 before any call to Close(), subsequent Close() calls can become no-ops (due to the CAS guard), leaving the socket/resources to finalization. Consider triggering the normal close/dispose path here (e.g., perform the same CAS-based shutdown/dispose sequence used by Close()), or refactor Close() to always dispose the socket even if _open is already 0.
    plc4net/transports/tcp/TcpTransportInstance.cs:1
  • Prefer Array.Empty<byte>() over new byte[0] to avoid an unnecessary allocation and follow common .NET conventions for empty arrays.

Comment thread .github/workflows/dotnet-platform.yml
Comment thread .github/workflows/dotnet-platform.yml
Comment thread plc4net/plc4net.sln
@TomNewChao

Copy link
Copy Markdown
Author

Wow ... quite a bit happened here during my few days off :-)

So one thing I wanted to point out: Even if in Java several external dependencies existed for various things (such as BitBuffers etc), I still rewrote everything from scratch in SPI3. One the one side, this way I could build a perfect fit for PLC4X, but also I thought this would be a perfect template for porting to other languages as there is no need to fins a "suitable but mostly not 100% replacement".

Another thing I noticed: We currently rely on the plc4x build-tools maven plugin to generate code. In my commercial offering I decided to give something else a try: I built an mspec parser and code-generator that fits the target language (in that case Rust) ... possibly it might be worth investigating the options to use a pure dotnet code generation tool ... the antlr4 grammar for mspec should help a lot with that. In the end a pure dotnet toolchain would eliminate the dependency on Java for dotnet developers while still being able to call the build from the overall maven reactor.

I think the API diverged quite a bit since it was created several years ago, but i think I saw you already picked up some of these changes.

If you need any help ...don't hesitate pinging me. I can also invite you in the plc4x slack channel, if you want more instant feedback (during sensible EU times ;-) )

Thanks for the pointers — the code generation one lands on the open question in the description, and with an option I hadn't considered.

The bit buffers were the same call in miniature: I dropped Ayx.BitIO and wrote the reader/writer rather than hunting for a closer package. That you rewrote that layer in SPI3 for the same reason is useful — I'll take SPI3 as the reference to follow rather than a source to copy line by line, and say so where .NET pushes a different shape.

On a pure .NET toolchain: agreed, and for the reason you give. Removing the Java dependency for .NET developers is worth more than finishing the freemarker templates. I had a look at the antlr4 grammar and the parser side does look straightforward — the work sits above it, in the type model.

Slack sounds like the right place for the rest — yes please, and thanks for the offer. I'm on UTC+8, so your working day runs from my afternoon into my evening; that lands inside sensible hours on both ends.

@chrisdutz

Copy link
Copy Markdown
Contributor

As it's challenging to get github-user-to-email-addresses ... please send me the address I should send the invite to cdutz@apache.org

@TomNewChao
TomNewChao force-pushed the feature/plc4net-revival branch from b6bb5ba to fa0c898 Compare July 27, 2026 14:08
@sruehl
sruehl requested a review from Copilot July 27, 2026 15:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 56 out of 56 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (2)

plc4net/transports/tcp/TcpTransportInstance.cs:1

  • Socket.Send(...) can legally return 0 (e.g., when the connection has been closed), which would make this loop spin forever because offset never increases. Consider capturing the return value, and if it is 0, treat it as a connection failure (throw TransportException / close the connection) to avoid an infinite loop.
    plc4net/transports/tcp/TcpTransportInstance.cs:1
  • When the ring buffer is full, the read loop polls with a 1ms delay. Under sustained backpressure this can cause unnecessary wakeups/CPU usage. Consider replacing this polling with a waitable signal (e.g., a SemaphoreSlim/AsyncAutoResetEvent that the consumer signals after draining), or at least use a larger/exponential backoff delay to reduce churn.

Comment thread plc4net/api/PlcDriverManager.cs Outdated
Comment thread plc4net/api/PlcDriverManager.cs Outdated
Comment thread .github/workflows/dotnet-platform.yml
@TomNewChao
TomNewChao force-pushed the feature/plc4net-revival branch 3 times, most recently from e285d48 to cf0302b Compare July 28, 2026 01:56
@sruehl
sruehl requested a review from Copilot July 28, 2026 06:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 56 out of 56 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (5)

.github/workflows/dotnet-platform.yml:28

  • The path filter plc4net** is overly broad (it also matches paths that merely start with plc4net, e.g. plc4netfoo/...). Using plc4net/** is the typical and more precise way to scope to the directory tree.
    paths:
      - code-generation/**
      - protocols/**
      - plc4net**
  pull_request:

.github/workflows/dotnet-platform.yml:65

  • actions/setup-java is configured with distribution: 'adopt', but AdoptOpenJDK has been superseded by Eclipse Temurin and may stop being supported/updated. Switching to temurin keeps the workflow on a maintained JDK distribution.
          distribution: 'adopt'

plc4net/spi/spi/transports/RingBuffer.cs:140

  • The comment says a single subtraction replaces a modulo, but the implementation uses a modulo. Either update the comment or change the implementation so the documentation matches the behavior.
    plc4net/spi/spi/generation/ReadBuffer.cs:61
  • HasMore currently returns true for negative bitLength values, which is nonsensical and can mask caller bugs. Consider rejecting negative sizes explicitly.
    plc4net/transports/tcp/TcpTransport.cs:56
  • receive-buffer-size can be set to 0 (or negative) via the connection string, which then crashes TcpTransportInstance when constructing the RingBuffer (capacity must be positive). Consider treating non-positive values as invalid and falling back to the default here.

@sruehl

sruehl commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

@chrisdutz should that be part of 1.0.0?

@chrisdutz

chrisdutz commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Well we shouldn't postpone the release too long. If something usable is done soon, sure. Otherwise the next release could be done any time.

Is actually a quite streamlined process (as long as it's part of the monorepo.

@chrisdutz

Copy link
Copy Markdown
Contributor

Also ... as this is considered a significant contribution .... before we can merge this you would need to file an ICLA with apache: https://www.apache.org/licenses/icla.pdf ... if you are doing this work as part of your day-job you should also consider your company signing a CCLA https://www.apache.org/licenses/cla-corporate.pdf
Possibly worth doing that now so it's not going to delay things once your work is ready to merge.

@chrisdutz

Copy link
Copy Markdown
Contributor

Now that I had the time to catch up with everything else after my holiday yesterday, now I had the time to thorougly read your initial message.

So the PLC4Net doesn't have to be identical to the Java version. It was allways our goal to make the libratries feel natural in their normal ecosystem. With Java we're using CompletableFutures, with Go we're using channels ... whatever is the best way to do things in the target language.

What we do try to keep, is the general usage pattern: So if connections are synchronous in Java, it would sort of be bad if it wasn't in other languages. Also if the naming of things was kept ... like the usage of "Tag" and "Query" and that you can add a Tag or a TagString, ....

Admittedly I don't quite understand what you mean with your first divergence: "ConnectionString is a public type in the API, not the SPI. It has no SPI dependency, and PlcDriverManager — which lives in the API — needs it to route the protocol code." Could you please explain that?

I think when I did the first work on PLC4Net, I chose KNX as this used the most of our mspec-functionality ... I thought if I get this driver working, the rest will most probably also work. Usually when starting on a new language I started with Modbus as this is the simplest one of them all for which a test-bench is cheaply available. KNX also had this super odd encoding for 16 bit float which isn't really a full half-precision-iec float. In the new SPI3 java version, I think I used a dedicated float implementation:

        ['PDT_KNX_FLOAT' REAL
            [simple   float 16      value floatEncoding='"KNXFloat"']
        ]

The implementation is here:
plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/EncodingKnxFloat.java

However, I built things in SPI3 so theoretically a protocol module could bring along it's own Encoding implementation by putting the class in the driver module and registering it via a org.apache.plc4x.java.spi.buffers.bytebased.encoding.Encoding property file.

As you could see I never really got very far with the .Net version as the person that claimed to want to help decided to go away. Usually my process was that I hand wrote some types from Modbus in the target language, then I copied the java templates and started adjusting them till the output matched the hand-written one. the data-io template you found was simply one copied but never touched by me as the work stopped before I had time to do that.

As you saw on the discussion list, we're currently considering splitting out Go and also creating go-native code generators. Possibly this is something you would also like to do. Initially the idea was to have one system for generating code. One system anyone would understand and maintaining would therefore be easier. Also is it one thing to understand a programming language, but it's a totally different thing to understand the tooling available in a language, the best practices and how to nicely integrate this into a smoothly running build. For java and maven we knew how to do this, for the others not so much ;-) So the current template+maven approch was simply a quick win that we knew how to do.

However, have I found out with my own closed-source work, that there are huge benefits for running this in a tool-native fashion ... then I guess only the resolution of the protocol modules would need to be implemented and the code-gen is built per language. The mspec format is documented in an Antlr4 grammar for which there should be tooling in most languages. The core mspec format is described here:
code-generation/protocol-base-mspec/src/main/antlr4/org/apache/plc4x/plugins/codegenerator/language/mspec/MSpec.g4
The expression syntax used in the little expression blocks inside are documented here:
code-generation/protocol-base-mspec/src/main/antlr4/org/apache/plc4x/plugins/codegenerator/language/mspec/expression/Expression.g4

I hope I manged to answer your questions ... if not ... don't hesitate to ask ... ideally here ... if it has to be quickly and you see I'm online in Slack, just ask there.

@sruehl
sruehl marked this pull request as draft July 28, 2026 10:35
@TomNewChao

TomNewChao commented Jul 28, 2026

Copy link
Copy Markdown
Author

Now that I had the time to catch up with everything else after my holiday yesterday, now I had the time to thorougly read your initial message.

So the PLC4Net doesn't have to be identical to the Java version. It was allways our goal to make the libratries feel natural in their normal ecosystem. With Java we're using CompletableFutures, with Go we're using channels ... whatever is the best way to do things in the target language.

What we do try to keep, is the general usage pattern: So if connections are synchronous in Java, it would sort of be bad if it wasn't in other languages. Also if the naming of things was kept ... like the usage of "Tag" and "Query" and that you can add a Tag or a TagString, ....

Admittedly I don't quite understand what you mean with your first divergence: "ConnectionString is a public type in the API, not the SPI. It has no SPI dependency, and PlcDriverManager — which lives in the API — needs it to route the protocol code." Could you please explain that?

I think when I did the first work on PLC4Net, I chose KNX as this used the most of our mspec-functionality ... I thought if I get this driver working, the rest will most probably also work. Usually when starting on a new language I started with Modbus as this is the simplest one of them all for which a test-bench is cheaply available. KNX also had this super odd encoding for 16 bit float which isn't really a full half-precision-iec float. In the new SPI3 java version, I think I used a dedicated float implementation:

        ['PDT_KNX_FLOAT' REAL
            [simple   float 16      value floatEncoding='"KNXFloat"']
        ]

The implementation is here: plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/EncodingKnxFloat.java

However, I built things in SPI3 so theoretically a protocol module could bring along it's own Encoding implementation by putting the class in the driver module and registering it via a org.apache.plc4x.java.spi.buffers.bytebased.encoding.Encoding property file.

As you could see I never really got very far with the .Net version as the person that claimed to want to help decided to go away. Usually my process was that I hand wrote some types from Modbus in the target language, then I copied the java templates and started adjusting them till the output matched the hand-written one. the data-io template you found was simply one copied but never touched by me as the work stopped before I had time to do that.

As you saw on the discussion list, we're currently considering splitting out Go and also creating go-native code generators. Possibly this is something you would also like to do. Initially the idea was to have one system for generating code. One system anyone would understand and maintaining would therefore be easier. Also is it one thing to understand a programming language, but it's a totally different thing to understand the tooling available in a language, the best practices and how to nicely integrate this into a smoothly running build. For java and maven we knew how to do this, for the others not so much ;-) So the current template+maven approch was simply a quick win that we knew how to do.

However, have I found out with my own closed-source work, that there are huge benefits for running this in a tool-native fashion ... then I guess only the resolution of the protocol modules would need to be implemented and the code-gen is built per language. The mspec format is documented in an Antlr4 grammar for which there should be tooling in most languages. The core mspec format is described here:

I hope I manged to answer your questions ... if not ... don't hesitate to ask ... ideally here ... if it has to be quickly and you see I'm online in Slack, just ask there.

Thanks — knowing the goal is to make the libraries feel natural in their normal
ecosystem while keeping the usage pattern and the naming settles several things I
was unsure about. Let me answer your question first, then lay out where I would
like to take this.

On the ConnectionString divergence — it does not hold up

The three states side by side:
ScreenShot_2026-07-28_195710_488

(*) = the full connection-string parser
protocol code + transport code + host + port + query params + URL decoding

(1) and (2) are the same shape. I broke that in (3).

What happened: I read "Uri cannot parse the whole s7:cotp://host form" — which
is true, Host comes back empty and Port -1 — as "Uri cannot be used here"
the manager only ever needs the protocol code, and I had never checked what Java
actually does there. Both behave identically:

image

Java has exactly the same limitation on the two-scheme form and lives with it,
because DefaultPlcDriverManager only ever takes the scheme. So the thing I
treated as a blocker was never one.

Counting the consumers settles which module the type belongs in:

api/PlcDriverManager.cs:60 ConnectionString.Parse(..) 1 site <- the line I added
spi/drivers/DriverBase.cs:83 ConnectionString parameter 3 sites <- genuine; these
spi/drivers/DriverBase.cs:101 ConnectionString.Parse(..) need transport code,
spi/drivers/DriverBase.cs:130 ResolveTransportCode(..) host, port, params

Revert that one line and the type has no consumers left in the api module. So it
moves down next to DriverBase and the manager goes back to Uri.Scheme. I will
fix that.

On the general approach

My intent throughout has been to follow how the Go and Java modules are used so
the project keeps one consistent shape. Your point about the usage pattern and
the naming is a fair hit — IPlcReadRequestBuilder still exposes
AddItem(name, fieldQuery), which is both the old "field" wording and missing
the Tag/TagString pair. I will align it with addTag/addTagAddress. Query I
would rather leave until there is browse support behind it — plc4net currently
has no PlcBrowser and no browse request at all, so adding the type on its own
would be the name without the capability.

On KNX

I did not choose it, I inherited it — plc4net/drivers/knxnetip/src already
carries the generated model, so finishing that capability looked like the
first step. My own roadmap is different: Modbus, then S7, then OPC UA, because
what I actually need is to reach PLCs from several vendors and feed them into an
IoT platform. Your note that you normally start with Modbus matches where I was
heading anyway.

On tool-native code generation

I agree with building it per language. Each language has its own idioms, tooling
and best practices, and a tool-native generator fits that far better than one
shared toolchain. Thanks for the two grammar pointers — I have looked at them and
they seem very tractable from .NET, so the part left to work out is the
resolution of the protocol modules you mentioned.

Where I would like to go next

  1. Get the CLA filed.
  2. Start with Modbus and prove the path end to end.
  3. Extend outwards to the protocols the PLCs I work with actually speak.

One request

This PR is really me probing for direction rather than proposing something
finished, and properly absorbing this project is going to take me a while
you consider creating a feature/plc4net branch I could target instead of
develop? contributing.adoc already describes feature branches with that
prefix, and it would let this land in reviewable increments without any of it
touching the 1.0.0 release — which I think also answers @sruehl's question above.
Happy to work that way if it suits you.

@TomNewChao TomNewChao closed this Jul 28, 2026
@TomNewChao TomNewChao reopened this Jul 28, 2026
@TomNewChao

Copy link
Copy Markdown
Author

Sorry, I accidentally hit the close button — reopened immediately. That was not intentional.

@chrisdutz

Copy link
Copy Markdown
Contributor

Hi Tom,

So you're proposing to build a generic "PLC4X Connection String Parser" component that know how to deal with all types of PLC4X connection strings? I fully agree that's probably the cleanest approach. In my ToddySoft implementation I think I also did it that way. The PLC4J SPI3 was more a migration that a rewrite. So Yeah ... I agree that possibly also for PLC4J such a central component would be a good option.

Nothing I keeping you from creating that branch and I think in the PR can't you simply select that as target? Or would someone with commit rights need to create that branch first? If that is the case, I'll be happy to create it.

Regarding naming inconsistencies: Yes ... we did some cleaning up in the API some time ago and I recall not bothering to update PLC4Net as it was considered abandoned and we generally left it in there as it contained the glue to integrate it into the overall reactor build if someone decided to want to work on this in the future (So we were waiting for you ;-) )

Chris

@sruehl

sruehl commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

the target ist not really a issue, it can stay on develop and it just stay open...

@chrisdutz

Copy link
Copy Markdown
Contributor

Good point @sruehl I mean ... currently the part of the code-base nobody is using or working on plc4net ... so generally it would even be safe to work on plc4net on develop directly as long as it doesn't break the build.

@TomNewChao

Copy link
Copy Markdown
Author

Hi Tom,

So you're proposing to build a generic "PLC4X Connection String Parser" component that know how to deal with all types of PLC4X connection strings? I fully agree that's probably the cleanest approach. In my ToddySoft implementation I think I also did it that way. The PLC4J SPI3 was more a migration that a rewrite. So Yeah ... I agree that possibly also for PLC4J such a central component would be a good option.

Nothing I keeping you from creating that branch and I think in the PR can't you simply select that as target? Or would someone with commit rights need to create that branch first? If that is the case, I'll be happy to create it.

Regarding naming inconsistencies: Yes ... we did some cleaning up in the API some time ago and I recall not bothering to update PLC4Net as it was considered abandoned and we generally left it in there as it contained the glue to integrate it into the overall reactor build if someone decided to want to work on this in the future (So we were waiting for you ;-) )

Chris

On the ConnectionString — I was actually proposing something smaller: just moving it from the api module down to the spi module, next to DriverBase, matching how plc4j splits it. But the idea of a central component is worth discussing once the basics are in place.

On the branch @sruehl — understood, and keeping it on develop is simpler. I will keep the build green and keep the PR open as the tracking point.

Thanks

@TomNewChao TomNewChao changed the title feat(plc4net): revive the .NET port — net8.0, SPI3 alignment, driver runtime 【WIP】feat(plc4net): revive the .NET port — net8.0, SPI3 alignment, driver runtime Jul 29, 2026
TomNewChao added 15 commits July 29, 2026 21:18
PLC4Net targeted net452 (.NET Framework 4.5.2), which went out of support in
April 2022 and cannot be built on the Linux and macOS GitHub runners. That is
the direct reason PLC4Net has no CI workflow while plc4c, plc4go, plc4j and
plc4py all have one.

- Retarget all four projects to net8.0 (LTS).
- Hoist the properties every project repeated into a shared
  Directory.Build.props, so the next framework bump is a one-line change.
- Correct the package version, which was still pinned at 0.8.0-SNAPSHOT while
  the project is on 1.0.0-SNAPSHOT. The Maven build already overrides this via
  -p:Version, so only standalone `dotnet build` was affected.
- Replace the deprecated PackageLicenseUrl with the PackageLicenseExpression
  SPDX form (clears NU5125).
- Add Microsoft.NET.Test.Sdk to the KNXnet/IP test project. Without it the
  xunit suite compiled but was never discovered or executed, so the tests
  have not actually been running.
- Mark the test project non-packable; it was emitting a NuGet package.
- Bump NLog 4.7.13 -> 5.3.4 and xunit 2.4.1 -> 2.9.2 for net8.0 support.

`dotnet build plc4net.sln` completes with 0 errors on the .NET 9 SDK.

Note: Ayx.BitIO 1.0.0 (used by ReadBuffer/WriteBuffer) is a .NET Framework
only package and now restores under NU1701 fallback. It still resolves, but
it is unmaintained and should be replaced before PLC4Net is relied on
cross-platform.
Reading any value through IPlcValue returned 0/false/null regardless of what
the value actually held.

PlcValueAdapter is the only class implementing IPlcValue, and every one of its
members was non-virtual and returned `default`. The subclasses that carry the
real data declared their accessors with `new` rather than `override`, so they
hid the base member without re-implementing the interface. The interface map
therefore still pointed at PlcValueAdapter. Calling through the concrete type
looked right, which is presumably why this went unnoticed, but every consumer
reaches a value through IPlcValue.

  IPlcValue v = new PlcDINT(42);
  v.GetInt();   // 0
  v.IsSimple(); // false

- Make the PlcValueAdapter members virtual and turn the 30 `new` declarations
  in PlcSimpleValueAdapter, SimpleNumericValueAdapter, PlcBYTE, PlcWORD,
  PlcDWORD and PlcLWORD into overrides.
- Implement the accessors on PlcBOOL, PlcSTRING, PlcCHAR, PlcWCHAR and
  PlcWSTRING, which stored a value but never exposed it.

Two further defects in SimpleNumericValueAdapter, both reachable once dispatch
works:

- The narrowing accessors unboxed with a direct `(byte) value` cast on an
  IComparable holding a boxed Int32, which throws InvalidCastException. They
  now convert via Convert.ToXxx.
- The range checks called IComparable.CompareTo against a bound of a different
  numeric type, e.g. a boxed Int64 against ulong.MinValue, which throws
  ArgumentException. They now widen to decimal first, which covers every
  integral PLC type exactly.

Adds plc4net-spi-test with 12 tests, all asserting through IPlcValue rather
than the concrete type so the regression cannot come back unnoticed.
Once the KNXnet/IP test actually ran, it failed inside Ayx.BitIO with an
ArgumentOutOfRangeException out of StringBuilder.ToString: the package encodes
every read as a string of '0'/'1' characters and mishandles a full 32 bit
field. PLC4Net could not read a float at all.

Ayx.BitIO is a .NET Framework only assembly, unmaintained since 2016, and was
being resolved under NU1701 fallback after the net8.0 retarget. Replace it with
BitReader/BitWriter in the SPI: plain managed MSB-first bit access, no external
dependency, and the same order every mspec-described protocol uses.

Repairing the buffers surfaced several more defects, none of which could have
worked in production:

- ReadUlong/ReadLong read (bitLength - 32) bits and then a further bitLength
  bits, consuming 96 bits for a 64 bit field. They never round-tripped against
  the corresponding writes.
- WriteFloat/WriteDouble called WriteByte(8, bytes[i]) against a
  (value, bitLength) signature, so they wrote the constant 8 with a bit width
  taken from the payload.
- The 16 bit KNX branch of WriteFloat was an empty block that emitted nothing.
- ReadDouble at 32 bits passed a 4 byte array to BitConverter.ToDouble, which
  always throws.
- ReadFloat/ReadDouble round-tripped through BitConverter.GetBytes, reordering
  the bytes on a little-endian host. They now reinterpret the raw bits.
- Signed narrow fields were cast rather than sign-extended, so a 4 bit -8 read
  back as +8.
- HasMore used < instead of <=, refusing a read that exactly filled the buffer.
- ReadString, ReadByteArray and WriteString threw NotImplementedException.
  S7 and Modbus both need them; they are implemented here.
- ParseException was an empty class that did not derive from Exception, so no
  parse failure was reportable. It now is one, and short reads raise it with
  the expected and available bit counts.

The KNX decode test passed only four bytes for DPT 14.019, which is a reserved
byte followed by a float32. Corrected to the full five byte frame.

44 tests pass (43 SPI, 1 KNXnet/IP); `dotnet build` is clean with no warnings.
plc4c, plc4go, plc4j and plc4py each have a platform workflow; plc4net had
none, so nothing verified it and it was free to rot. That is the mechanism by
which it ended up several years behind: a value model that returned `default`
through its own interface, a bit codec that could not read a float, and a test
suite that was never executed all survived because no build ever ran them.

Mirrors python-platform.yml: same triggers, same Maven and Develocity setup,
same cache strategy. Builds across ubuntu, macos and windows, which only became
possible once the projects moved off net452.

Runs `dotnet test` as an explicit step after the Maven build so individual
xunit failures show up in the job log rather than as an opaque exec-plugin
non-zero exit.
The existing API carried pre-SPI3 naming (IPlcField) and async-only driver
methods. This aligns it with the Java SPI3 contracts:

- IPlcField → IPlcTag (plc4j moved from PlcField to PlcTag in SPI3)
- IPlcDriver switches from async ConnectAsync() to synchronous Connect();
  connection-string dispatch happens in DriverBase, not piecemeal in each
  driver.
- IPlcConnection drops the async pattern; drivers that need async internally
  use the transport's event/callback model.
- PlcDriverManager can now resolve a protocol code from a connection string
  via ConnectionString.Parse (moved here from an spi package because it carries
  no SPI dependency).
- ConnectionString + RedactSecrets extracted from Java DriverBase.URI_PATTERN
  and the parameter redaction regex; the same pattern lets a connection string
  address the same device from Java or .NET.
The .NET port had no driver runtime — no transport layer, no codec, and no
driver or connection base. A concrete driver like S7 or Modbus had nothing to
build on. This is the foundation, modelled on the Java SPI3 contracts.

Transport layer:
- ITransport — factory for a kind of transport (tcp, cotp, …)
- ITransportInstance — live byte-level connection (open, peek, read, write,
  close). Mirrors Java TransportInstance.
- IAsyncTransportInstance — transport that pushes a data-available callback so
  the driver does not poll
- BaseTransportInstance — shared configuration and open-guard
- DefaultTransportManager — in-memory registry; drivers wire transports into
  it explicitly, avoiding assembly-scanning
- RingBuffer — fixed-capacity lock-free producer/consumer for a transport's
  read loop
- TransportException

TcpTransport — a concrete transport with async read loop, connect-timeout,
IPv6 address parsing, /driver-config extraction

Driver layer:
- DriverBase — connection-string dispatch, transport lookup, supported-transport
  enforcement, default transport resolution. Mirrors Java DriverBase.
- ConnectionBase — wraps a TransportInstance and exposes the IPlcConnection
  contract
- MessageCodecBase — peek/read/parse framing loop that turns a byte stream into
  whole protocol messages. Mirrors Java MessageCodecBase.
- IMessage — marker for generated/hand-written messages that can self-serialize

77 tests (43 buffer/value + 22 connection-string + 8 ring-buffer + 4 transport
address + 4 driver-base + 1 knx). dotnet build clean.
Driver protocol codes are now matched case-insensitively. The manager
registered a driver under its ProtocolCode verbatim but looked it up
lowercased, and DriverBase compared the two with a case-sensitive ordinal
comparison, so a driver whose code was not already lowercase could be
registered and then never found. Both now behave the way
DefaultTransportManager already did for transport codes.

Connection-string parameters are decoded the way Java decodes them.
Uri.UnescapeDataString leaves '+' as a literal plus, while Java hands values
to URLDecoder.decode(value, UTF_8), where it means a space; and
GetIntParameter parsed under the current culture rather than the invariant
one, so a connection string could mean different things on different machines.

The TCP transport no longer leaks its socket. One flag meant both "the
connection is usable" and "our end has been released", so when the read loop
cleared it on the way out - which is what happens whenever the peer hangs up -
a later Close() saw an already-closed connection and returned without
disposing anything, leaving the socket and its CancellationTokenSource to
finalization once per disconnect. Releasing is now guarded separately and
every exit from the read loop goes through it. A related guard was also
ineffective: Close() skips waiting on the read loop when called from inside
it, but compared Task.CurrentId against the loop's task id, and an async
method's continuations do not run as the task Task.Run returned, so the wait
always ran its full timeout.

RingBuffer copies in bulk instead of one byte at a time, on a path the read
loop walks while holding its lock, and the exception types drop the obsolete
binary-serialization constructors that warned under net8.0.

Building plc4net under -P with-dotnet runs the module's own RAT check, which
flags plc4net.sln. A solution file cannot carry a licence header - its Format
Version marker has to stay within the first two lines - so it is excluded in
plc4net's own pom. Nothing had caught this before because no CI job built the
.Net module.

Tests grow from 77 to 106, and the transport now has the first ones that drive
a real socket. That gap is why these defects survived: TcpTransportInstance
had no coverage at all, and it is where all of them were.
Move ConnectionString from the api module to spi/drivers, next to
DriverBase — the three consumers that actually need the full parser
(transport code, host, port, and parameters) are all in DriverBase.
PlcDriverManager, the one api-layer consumer, only ever needed the
protocol code, and new Uri(url).Scheme provides that the same way
Java DefaultPlcDriverManager.getDriverForUrl() uses URI.getScheme().
Both behave identically on every PLC4X connection-string form,
including the two-scheme case (s7:cotp://host → "s7").

Naming aligned with Java SPI3 per upstream feedback:
  IPlcField → IPlcTag (the empty legacy interface is removed)
  IPlcFieldRequest → IPlcTagRequest  (FieldCount → TagCount, etc.)
  IPlcFieldResponse → IPlcTagResponse
  AddItem(name, fieldQuery) → AddTagAddress(name, tagAddress) (read)
  AddItem(name, fieldQuery, values) → AddTag(name, tagAddress, values) (write)

Three defensive fixes from review:
  ReadBuffer.HasMore now rejects a negative bit length instead of
    silently returning true.
  TcpTransport.CreateConfiguration rejects a non-positive receive-
    buffer-size and falls back to the default (81920), so a connection
    string like ?receive-buffer-size=0 no longer crashes the
    RingBuffer constructor.
  RingBuffer.Advance: the comment now describes the code rather
    than a subtraction path that was never taken.

Tests grow from 107 to 112.
…amework

Test transport (in-memory loopback):
  A new plc4net-transport-test project that mirrors the Java SPI3
  TestTransport. It simulates a network with two ring buffers — one
  for injecting bytes that the driver reads, and one that captures
  what the driver writes. Drivers can now run end-to-end tests
  without hardware, and those tests work in CI.

Message infrastructure (plc4net-spi/drivers/messages):
  Ports the core of the Java SPI3 message layer to C#:

  Items:
    PlcTagItem<T>, PlcTagValueItem<T>, PlcResponseItem<T>
    DefaultPlcTagItem, DefaultPlcTagValueItem
    DefaultPlcResponseItem, DefaultPlcTagErrorItem

  Read path:
    DefaultPlcReadRequest     (IPlcReadRequest + IPlcTagRequest)
    DefaultPlcReadRequestBuilder  (AddTagAddress)
    DefaultPlcReadResponse    (explicit IPlcResponse.Request)

  Write path:
    DefaultPlcWriteRequest    (IPlcWriteRequest + IPlcTagRequest)
    DefaultPlcWriteRequestBuilder (AddTag, all overloads)
    DefaultPlcWriteResponse   (explicit IPlcResponse.Request)
    IPlcWriteResponse         (new api interface)

Driver contracts:
  PlcReader  — a driver implements this to execute read requests
  PlcWriter  — a driver implements this to execute write requests
  PlcTagHandler — a driver parses its address syntax with this

Tests grow from 112 to 124 (8 test transport + 4 message infrastructure).
The first stage of the pure-.NET mspec toolchain that Chris Dutz
described on PR apache#2656. This commit adds:

Generated C# parsers (from the upstream .g4 grammars):
  MSpecLexer/Parser/Listener/BaseListener      (from MSpec.g4)
  ExpressionLexer/Parser/Listener/BaseListener  (from Expression.g4)

  The grammars are the shared specification; each language generates
  its own parser. The one Java-specific semantic predicate —
  {getCharPositionInLine() == 0}? — is changed to the C# equivalent
  {Column == 0}?. The generated .cs files are checked in so that
  .NET developers never need Java to build mspec-based drivers.

MspecReader:
  A thin wrapper that turns mspec text (or a file path) into an
  ANTLR parse tree, collecting syntax errors rather than printing.

ParserSerializerTestsuiteRunner:
  A .NET executor for the language-neutral XML test suites under
  protocols/*/src/test/resources/. plc4j and plc4go each run the
  same XML through their own executor; this is the .NET third.
  It loads the XML, decodes hex test vectors, and exposes them
  as a typed model.

Tests grow from 124 to 131 (5 mspec parser + 2 testsuite runner).
The second stage of the pure-.NET mspec toolchain: the type model (IR)
that sits between the ANTLR parse tree and the language output.

Type model:
  Terms — IntegerLiteral, StringLiteral, BooleanLiteral, VariableLiteral,
    BinaryExpression, UnaryExpression, ScopeTerm
  Definitions — TypeDefinition, ComplexTypeDefinition,
    DiscriminatedComplexTypeDefinition, EnumTypeDefinition, Argument
  Fields — SimpleField, ConstField, ImplicitField, ArrayField,
    DiscriminatorField, TypeSwitchField, EnumField, ReservedField,
    VirtualField, PaddingField (+ CaseStatement)

MspecModelBuilder:
  Direct tree walker that builds the type model from an ANTLR parse
  tree. Handles complex types, data-io definitions, constants blocks,
  and 10 field types.

CSharpModelGenerator:
  Generates C# model classes with type mapping (uint 8→byte,
  uint 16→ushort, etc.) and PascalCase properties.

Pipeline tests: mspec → parse tree → type model → C# code.

Tests grow from 131 to 136.
The first real PLC4Net driver. Implements the Modbus TCP protocol
end-to-end: address parsing, PDU construction, MBAP framing, and
response decoding.

ModbusTag:
  Parses address strings like coil:0, holding:10, discrete:5,
  input:3. Case-insensitive, with clear error messages.

ModbusPDU:
  Builds and parses all common Modbus function codes:
  Read Coils (0x01), Read Discrete Inputs (0x02),
  Read Holding Registers (0x03), Read Input Registers (0x04),
  Write Single Coil (0x05), Write Single Register (0x06),
  Write Multiple Registers (0x10).

ModbusTcpDriver:
  Extends DriverBase, registers itself under "modbus-tcp",
  default transport "tcp". Supports unit-identifier parameter.

ModbusConnection:
  Implements PlcReader and PlcWriter. Direct byte-level I/O
  with MBAP header framing (transaction ID, protocol ID,
  length, unit ID). Handles Modbus error responses (function
  code + 0x80 + exception code).

Tests grow from 136 to 146 (10 new Modbus tests).
The second real PLC4Net driver. Implements the Siemens S7 (S7comm)
protocol with TPKT (RFC 1006) framing and COTP transport.

COTP transport (plc4net-transport-cotp):
  TpktFrame — 4-byte TPKT header wrapping/unwrapping for ISO-on-TCP
  CotpTransport — wraps a TCP connection with TPKT framing
  CotpTransportInstance — intercepts read/write to add/strip TPKT

S7 driver (plc4net-driver-s7):
  S7Tag — parses Siemens address syntax:
    %DBn.DBTm, %DBn.DBXm.b, %Mn.b, %In.b, %Qn.b, %Cn, %Tn
  S7PDU (S7Constants) — builds S7comm Read Var and Write Var
    request PDUs with correct transport sizes and area codes
  S7Driver — extends DriverBase, protocol code "s7", default
    transport "cotp"
  S7Connection — implements PlcReader with S7 response parsing

Tests grow from 146 to 159 (13 new S7/TKPT tests).
Sprint 6 — Generator Phase C+D:
  CSharpModelGenerator now handles the full set of field types:
  SimpleField (read/write via ReadByte/ReadUshort etc.),
  ConstField (validated on read), ArrayField (array properties),
  EnumField (enum type references), DiscriminatorField,
  VirtualField, PaddingField (skipped).

  Adds StaticParse generation with read-method dispatch,
  Serialize with write-method dispatch, and proper constructor
  with default+parameterized forms. Type mapping supports all
  IEC 61131-3 base types (bool, byte, uint, int, float, string).

Sprint 7 — Engineering:
  Plc4NetServiceExtensions:
    RegisterDriver — fluent extension for driver registration
    ScanAndRegisterDrivers — assembly scanning for DriverBase
      subclasses with parameterless constructors

Tests grow from 159 to 161.
@TomNewChao
TomNewChao force-pushed the feature/plc4net-revival branch from cf0302b to 176630d Compare July 29, 2026 13:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants