[WIP] feat(plc4net): revive the .NET port — net8.0, SPI3 runtime, pure-.NET mspec generator - #2656
[WIP] feat(plc4net): revive the .NET port — net8.0, SPI3 runtime, pure-.NET mspec generator#2656TomNewChao wants to merge 16 commits into
Conversation
There was a problem hiding this comment.
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.0and centralize shared build/package properties viaDirectory.Build.props. - Fix the PLC value model’s virtual dispatch and add a bit-level codec (
BitReader/BitWriter) + repairedReadBuffer/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.
|
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 ;-) ) |
There was a problem hiding this comment.
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_openis set to 0 before any call toClose(), subsequentClose()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 byClose()), or refactorClose()to always dispose the socket even if_openis already 0.
plc4net/transports/tcp/TcpTransportInstance.cs:1 - Prefer
Array.Empty<byte>()overnew byte[0]to avoid an unnecessary allocation and follow common .NET conventions for empty arrays.
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. |
|
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 |
b6bb5ba to
fa0c898
Compare
There was a problem hiding this comment.
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 becauseoffsetnever increases. Consider capturing the return value, and if it is 0, treat it as a connection failure (throwTransportException/ 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/AsyncAutoResetEventthat the consumer signals after draining), or at least use a larger/exponential backoff delay to reduce churn.
e285d48 to
cf0302b
Compare
There was a problem hiding this comment.
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 withplc4net, e.g.plc4netfoo/...). Usingplc4net/**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-javais configured withdistribution: 'adopt', but AdoptOpenJDK has been superseded by Eclipse Temurin and may stop being supported/updated. Switching totemurinkeeps 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.
|
@chrisdutz should that be part of 1.0.0? |
|
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. |
|
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 |
|
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: The implementation is here: 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 On the ConnectionString divergence — it does not hold up The three states side by side: (*) = the full connection-string parser (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
Java has exactly the same limitation on the two-scheme form and lives with it, Counting the consumers settles which module the type belongs in: api/PlcDriverManager.cs:60 ConnectionString.Parse(..) 1 site <- the line I added Revert that one line and the type has no consumers left in the api module. So it On the general approach My intent throughout has been to follow how the Go and Java modules are used so On KNX I did not choose it, I inherited it — plc4net/drivers/knxnetip/src already On tool-native code generation I agree with building it per language. Each language has its own idioms, tooling Where I would like to go next
One request This PR is really me probing for direction rather than proposing something |
|
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. |
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 |
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).
8b7a0a0 to
5683bfe
Compare
…port logging Two additions in one changeset: 1. CotpTransportInstance gains a public DiagnosticOutput property (TextWriter?). When set, every raw frame — CR, CC, DT send and DT receive — is written as annotated hex. Zero overhead when left null. HandshakeTimeout is changed from internal to public so the verification tool can shorten the timeout. 2. A new tools/s7-verify console project connects to a real S7-1200 or S7-1500, performs the full COTP CR/CC handshake with byte-level logging, executes an S7 Read Var request against a user-specified address, and prints a markdown verification report. Usage: s7-verify 192.168.0.10 0 1 %DB1.DBW0 The report is designed to be redirected to docs/test_report.md and attached to PR apache#2656 as hardware evidence.
|
@TomNewChao are you still working on this? |
Yeah, sorry, I'm on vacation right now. I'll get back to work as soon as I get back. |
net452 cannot build on the Linux and macOS CI runners, which kept plc4net off the CI matrix. Retargets to net8.0 (LTS), hoists the shared properties into Directory.Build.props (TreatWarningsAsErrors on - the module is warning-clean), and adds Microsoft.NET.Test.Sdk to the test projects, without which the suites compiled but never executed.
Subclasses declared 'public new bool GetBool()' rather than 'override', so a caller holding an IPlcValue landed on PlcValueAdapter's defaults; ((IPlcValue) new PlcDINT(42)).GetInt() returned 0. PlcBitString and PlcList stored a value but never overrode the accessors. Adds the temporal accessors IPlcValue needs (GetDuration / GetDate / GetTime), re-bases PlcTIME / PlcDATE / PlcTIME_OF_DAY on TimeSpan / DateOnly / TimeOnly, adds nanosecond-exact PlcLTIME_OF_DAY / PlcDATE_AND_LTIME, and stops PlcREAL throwing for a NaN / Infinity value.
Ayx.BitIO is net45-only, unmaintained, round-trips through a StringBuilder of 0/1 characters and throws on a 32-bit field. Replaced with an MSB-first BitReader / BitWriter, no dependency. That exposed the layer above: ReadUlong / ReadLong consumed the wrong bit count, WriteFloat / WriteDouble had the arguments reversed, ReadDouble(32) always threw, and the string / byte-array paths were NotImplementedException. ParseException did not derive from Exception. The KNX DPT 9.x 16-bit float is implemented explicitly - one test documents its coarse top-of-range step.
PlcField -> PlcTag, async ConnectAsync -> synchronous Connect / OnConnect, and a ConnectionString parser whose grammar is character-for-character Java's DriverBase.URI_PATTERN. The nested api/api layout is flattened.
DriverBase (connection-string dispatch, transport selection, supported- transport enforcement), ConnectionBase (the Connect / OnConnect split for a protocol handshake), MessageCodecBase<T>, the request / response and tag infrastructure, the transport abstraction + RingBuffer, and the IServiceCollection DI extension.
TcpTransport over real sockets; CotpTransport (CR/CC handshake, DT framing, TPDU-size negotiation, and DT fragmentation for blocks over the negotiated size); a serial transport (RS-232 / RS-485); and an in-memory loopback for driver tests. COTP frame layouts follow ISO 8073 / RFC 1006; the CC fixture mirrors an S7-1500 response.
The Java C# generator only ever emitted data classes - its io-template.cs.ftlh is un-migrated Java and unregistered. This is a pure-.NET replacement: checked-in ANTLR-for-C# parsers (the upstream .g4 with one lexer predicate ported Java to C#; see tools/code-gen/README.md), a type-model IR, and a C# emitter producing StaticParse / Serialize / GetLengthInBits per type and an IPlcValue codec per [dataIo].
The plc4x-maven-plugin (languageName=C#) is gone; the update-generated-code profile shells out to the .NET generator instead. pom.xml is now a thin dotnet bridge for the release reactor - day-to-day build and CI need no JDK. code-generation/language/cs is left intact (orphaned; plc4net was its only consumer).
Modbus TCP and RTU (over the serial transport). The wire model is generated from modbus.mspec and round-trips the six shared TCP ParserSerializerTestsuite.xml vectors byte-identical. The [dataIo] DataItem parses / serialises one IPlcValue keyed on dataType / numberOfValues / bigEndian - the scalar and string cases plus numberOfValues > 1 as a PlcList.
…tation Read Var, rack / slot addressing, Java-parity TSAP encoding, seven address forms, Write, and DT fragmentation. The 166-file S7 wire model is generated from s7.mspec and round-trips all eleven shared ParserSerializerTestsuite.xml vectors byte-identical. Its [dataIo] DataItem is fully generated - scalar, string, and the whole TIA date / time family (S5TIME, the Siemens epoch, BCD, nanosecond-exact LTIME / DTL); the S7 STATIC_CALL string / date targets get real bodies in a non-generated S7StaticHelper partial ported from plc4j.
s7-verify and modbus-verify - manual smoke tests against a real PLC with diagnostic transport logging. Not part of CI.
Builds and tests with the .NET SDK alone - no JDK, no Maven - on ubuntu, macos and windows. A license-header job reproduces the apache-rat check; a generated-code-is-current job regenerates the Modbus and S7 models and fails on drift.
docs/design.md (architecture, capabilities, the GAP-1..9 list, the deliberate divergences from the Java SPI3) and docs/testing.md (the coverage matrix, how to run, the CI description).
1b7960f to
35f7bf0
Compare
…groundwork Three GAP-8 items in the generator: - Modbus's DataItem honours its `bigEndian` argument. plc4j leaves byte order to the driver's buffer context; plc4net has none, so a `bigEndian == false` frame's multi-byte scalar and list values are swapped with BinaryPrimitives.ReverseEndianness (a BitConverter round trip for float / double). Only kicks in for a dataIo that declares a `bit bigEndian` arg, so S7's DataItem is untouched. - `vstring 'expr'` - a string whose bit length is a run-time expression (ADS: `vstring 'stringLength * 8' value`). BuildSimpleType now keeps the length expression; the emitter renders it into the ReadString / WriteString call and the length sum. - KNX groundwork: MspecReader rewrites the empty-string literal `''` (which the shared expression grammar's `TICK innerExpression TICK` rule cannot match) to a sentinel the renderer maps back to "". This is what stopped knx-master-data.mspec from parsing at all. With it, KNX now generates 181 files - but ~240 of KnxDatapoint's 704 cases are struct-valued and hit a throwing stub (needs PlcStruct dataIo emission), the hyphenated vstring ids (DPST-1-1) render as a subtraction, and KnxDatapointType (1882 rows) does not compile. The checked-in KNX model is unchanged; this is a starting point, not a working KNX generator. Also: the S7 date/time dataIo bodies now only fire for a dataIo with a `dataProtocolId` arg, so they no longer misfire on KNX's DATE / TIME case names. 327 spi tests (+6: a vstring pipeline test, four little-endian Modbus DataItem vectors, one cross-endian value assertion). 0 warnings. S7 and Modbus (big-endian) regenerate byte-identical.
…nums, hyphenated ids
Finishes the KNX groundwork from the previous commit. The generated
`knxnetip` model (180 files, KnxDatapointType's 1882 rows included) now
compiles with no warnings; the checked-in hand-adapted model is still the
baseline, but swapping it in is now a mechanical step.
Generator:
- `[… Struct …]` dataIo cases (KnxDatapoint's ~74, KnxProperty's) emit a
`PlcStruct` keyed by field name, mirroring plc4j: read each field, then
`_map["name"] = new Plc<width>(value)`. A fixed-count `byte` array field
(KnxProperty's `groupAddress`) becomes a `PlcRawByteArray`. Serialize
and length follow. 243 KnxDatapoint stubs drop to 21 (7 of KNX's own
TIME / DATE cases, each marked `// TODO: Check if this is correct` in the
mspec and consumed by nothing, stay stubbed); KnxProperty drops to 0.
- `external='true'` enums (PlcValueType) are left to the SPI - no file is
emitted and no accessor that would return one, instead of a shadowing
empty stub.
- A `vstring` / `string` enum-parameter value that lexes as an expression
is folded back to its literal: `'DPST-1-1'` (a `-` chain of names and
ints) to the string "DPST-1-1", a bare `'409'` to "409".
- A dataIo case with no discriminator (KnxProperty's `[* … List]`
catch-all) compiles to `else { … }`, not `else if (true) { … }`, so the
fallback `return` after it is not unreachable (CS0162 under
TreatWarningsAsErrors).
- `float 16` reads and writes as 16 bits (KNX DPT 9.x, via the buffer's
KNXFloat path) rather than being widened to 32.
SPI value model - both types stored their payload but exposed none of it:
- PlcStruct overrides IsStruct / GetKeys / HasKey / GetValue / GetStruct
and compares by content, order-independent.
- PlcRawByteArray overrides GetRaw / GetLength and compares by content.
336 spi tests (+9: struct and byte-array dataIo emission, the wildcard
else branch, external-enum skip, hyphenated-id folding, a real-knx.mspec
generation check, PlcStruct and PlcRawByteArray accessors / equality).
0 warnings. S7 and Modbus regenerate byte-identical.
Adds a working KNXnet/IP tunnelling driver, modelled on the Java driver in this repo. Also swaps the checked-in KNX model for the generator's output - the driver needs the generated `KnxDatapoint` API and its populated struct datapoints, and CI now drift-checks the KNX model alongside Modbus and S7. UDP transport (`plc4net/transports/udp/`): - A bound datagram socket connected to one peer, a background receive loop feeding a ring buffer, the local endpoint exposed for the KNX HPAI. Datagram boundaries are not preserved on the buffer, but KNXnet/IP frames are length-prefixed so a codec still recovers them. Address parsing shares the TCP transport's rules. 8 tests over a loopback socket. KNXnet/IP driver (`plc4net/drivers/knxnetip/`): - `KnxNetIpTag` - the three group-address notations (`1/2/3`, `1/2`, `1`), `*` wildcards, an optional `:DPT<n>[.<sub>]` suffix, wire encoding, and resolution of the hint to a generated `KnxDatapointType`. - `KnxNetIpMessageCodec` - 6-byte-header framing over `MessageCodecBase`. - `KnxNetIpConnection` - SEARCH -> CONNECT -> CONNECTIONSTATE (60 s heartbeat) -> DISCONNECT; group-value Read (correlates the `GroupValueResponse` a bus device sends back), Write, and a `RegisterGroupValueListener` bus monitor (the SPI has no subscription path yet - GAP-6). Values decode / encode through the generated `KnxDatapoint` when a `:DPT…` hint is present, raw bytes otherwise, matching the Java no-project-file path. - `KnxNetIpDriver` - protocol code `knxnetip`, transport `udp`, port 3671. Tests: 22 new in `knxnetip-test` - group-address parsing and DPT resolution, and the whole connection (handshake, typed and raw Write, Read correlation, read timeout, bus monitor) against a scripted gateway on a loopback UDP socket that parses and serialises every frame through the generated model. Not verified against a real gateway (none on hand); stays `[WIP]` like S7. 367 tests (+30: 8 UDP, 22 KNX). 0 warnings. Modbus and S7 regenerate byte-identical; the KNX model is deterministic on re-run.


Revives
plc4net— the README lists it as "not ready for usage - abandoned". It now builds on Linux/macOS/Windows, has an executing test suite (367 cases), a SPI3-aligned driver runtime, working Modbus (TCP/RTU), S7 and KNXnet/IP drivers, and a pure-.NET.mspec→ C# generator that replaces the never-completed Java C# templates — the Modbus, S7 and KNXnet/IP wire models are all generated and CI drift-checked.Relates to #2655. Follows the
dev@thread from 2026-07-24.Still
[WIP]No end-to-end verification against a real PLC yet. S7-1200/1500 hardware is on hand (ICLA acknowledged 2026-08-02); the COTP handshake and the S7 Read-Var PDU are checked against reference captures, but the full path through a live device has not been run. The KNXnet/IP driver is tested against a scripted gateway on a loopback socket, not a physical one. Not requesting merge — requesting direction, especially on the generator (see below).
What's in it
Directory.Build.props(TreatWarningsAsErrors);.github/workflows/dotnet-platform.yml— .NET SDK only, no JDK/Maven, ubuntu + macos + windows, anapache-rat-equivalent license-header job, a generated-code drift job.pom.xmlkept as a thindotnetbridge for the release reactor.Microsoft.NET.Test.Sdkadded — without it the suites compiled but never ran, which is how the defects below went unnoticed.IPlcValuedispatch fixed — subtypes declaredpublic newinstead ofoverride, so((IPlcValue) new PlcDINT(42)).GetInt()returned0.PlcBitString/PlcList/PlcStruct/PlcRawByteArrayeach stored a value but overrode none of the interface accessors. Temporal types re-based onTimeSpan/DateOnly/TimeOnly;PlcLTIME_OF_DAY/PlcDATE_AND_LTIMEadded (nanosecond-exact).Ayx.BitIO(net45-only, unmaintained, round-trips through a'0'/'1'StringBuilder, throws on a 32-bit field) → in-house MSB-firstBitReader/BitWriter, no dependency.ReadBuffer/WriteBufferrepaired: wrong bit counts, reversed arguments,NotImplementedExceptionstring/array paths,ReadDouble(32)always threw.ParseExceptiondid not derive fromException.PlcField→PlcTag, async → synchronousConnect/OnConnect,ConnectionStringgrammar character-for-character from Java'sDriverBase.URI_PATTERN.DriverBase,ConnectionBase,MessageCodecBase<T>, transport abstraction, TCP + UDP + COTP + serial + test transports. The UDP transport is a bound datagram socket connected to one peer, a background receive loop into a ring buffer, local endpoint exposed for the KNX HPAI.plc4net/tools/code-gen/— checked-in ANTLR-for-C# parsers (the upstream.g4with one lexer predicate ported Java→C#;tools/code-gen/README.mddocuments the regen procedure), a type-model IR, and a C# emitter producingStaticParse/Serialize/GetLengthInBitsper type and anIPlcValuecodec per[dataIo]. Covers every field type Modbus / S7 / KNX use, discriminated types, parameterised and external enums,vstringwith an inline length, adataIobigEndianbyte-order flag, and the[… Struct …]dataIocase (→PlcStruct). The Javaupdate-generated-codeplugin is removed.code-generation/language/cs/is left intact — plc4net was its only consumer, so it is now an orphan module.:DPT…-driven decoding over the generatedKnxDatapoint), modelled on the Java driver. All three wire models are generated and CI drift-checked; Modbus TCP + S7 round-trip the sharedParserSerializerTestsuite.xml(6 + 11 vectors), the S7 / ModbusDataItemdataIo round-trips hand-built IEC-61131 vectors, and the KNXnet/IP handshake + tunnelling frames round-trip against a scripted gateway on a loopback socket. The KNX model - 180 files,KnxDatapointType's 1882 rows - replaced the Java-plugin output (7 unconsumedKnxDatapointTIME/DATEcases stubbed).Reviewing this
~640 files. The bulk is generated model code (
drivers/{modbus,s7,knxnetip}/src/…/readwrite/model, regenerated by CI from the.mspec) and the checked-in ANTLR parsers (tools/code-gen/src/generated, ~9.5K). The hand-written surface a reviewer needs to read is ~200 files. The 16 commits are ordered by dependency, each scoped to one concern —git logreads top to bottom as: build → value model → bit codec → API → runtime → transports → generator → drop the Java plugin → Modbus → S7 → verify tools → CI → docs →dataIobyte-order /vstring/ KNX groundwork → KNX generates and compiles → KNXnet/IP driver + UDP transport.Deliberate divergences from the Java SPI3
Flagging these because they are the most likely thing to be objected to.
ConnectionStringgrammar is identical, but decoding usesUri.UnescapeDataString+ culture-invariant parse (Java usesURLDecoder.decode(…, UTF_8)+Integer.parseInt). Will align if preferred.IsOpen,ProtocolCode).ITransportInstance : IDisposable— .NET resource convention, enablesusing.async/await— .NET has no virtual threads.DATE_AND_TIME/DTLdayOfWeekis written Sunday=1..Saturday=7 (matching s7.mspec's own DTL comment and the Siemens doc) where plc4j writes ISO Monday=1..Sunday=7 — a knowing departure; the field is parse-discarded on both sides.The direction question
CsLanguageOutputregisters onlymodel-template.cs.ftlh. Anio-template.cs.ftlhexists beside it but is unregistered and its body is still verbatim Java (implements MessageInput<>,@Override). So generating a protocol for C# yields data classes and nothing that can parse or serialise a frame. Java has since moved to code-based generators; go/c/python are still on freemarker.This PR builds a pure-.NET generator rather than finishing the freemarker template, mirroring where plc4j went. Is a second, C#-only mspec toolchain the right call, and what should happen to
code-generation/language/cs? Happy to take this todev@as its own thread — flagging it here so the review of the rest is not gated on it.Known gaps (
plc4net/docs/design.mdGAP-1..9)Serial + Modbus RTU have no tests yet. No TLS transport. No request/response correlation or timeout layer at the SPI level, and no
PlcSubscriberfunction interface — the KNX bus monitor is a driver-specific callback instead of aPlcSubscriptionRequest.WriteRequestBuilderimplementations are partial. On the generator: thepeek/assert/manualArrayfield families (unused by Modbus / S7 / KNX) are not emitted; abyteOrdertype attribute is not honoured (only thedataIobigEndianflag); 7 of KNX's ownTIME/DATEdataIocases — marked// TODOin the mspec, consumed by nothing — stay stubbed.Verification
dotnet build plc4net/plc4net.sln --no-incremental— 0 errors, 0 warningsdotnet test plc4net/plc4net.sln— 367 passing (344 spi-test, 23 knxnetip-test), 0 failinggenerated-code-is-currentregenerates the Modbus, S7 and KNXnet/IP models and asserts no drift from the.mspecIPlcValue— thenew/overridedefect was invisible to tests bound to concrete typesdotnet-platform.yml) has not run in this repository yet — non-committer PR, workflow isaction_required