Skip to content

feat(plc4j/slmp): add read-only SLMP (MELSEC 3E) driver#2640

Open
LivingLikeKrillin wants to merge 16 commits into
apache:developfrom
LivingLikeKrillin:feature/slmp-driver
Open

feat(plc4j/slmp): add read-only SLMP (MELSEC 3E) driver#2640
LivingLikeKrillin wants to merge 16 commits into
apache:developfrom
LivingLikeKrillin:feature/slmp-driver

Conversation

@LivingLikeKrillin

Copy link
Copy Markdown
Contributor

Second increment of the SLMP roadmap in #2585, building on the merged protocol module (#2597): the Java driver (plc4j/drivers/slmp) on the new SPI.

What's in it

  • slmp:tcp://{host}:{port} driver (default port 5007), read-only v0: Batch Read (0x0401), word units
  • Word devices D / R (decimal) and W (hex, optional 0x prefix); data types WORD (default), INT, UINT, DINT, UDINT, REAL; addresses like D350:DINT, R200:REAL[4], W0x1A:WORD[10] (each of these exact forms is unit-tested)
  • Conservative single-frame ceiling of 960 words per request — oversized requests are rejected, there is no splitting optimizer yet
  • Config: monitoring-timer (written into each 3E frame, default 0 = wait infinitely) and request-timeout (client-side, default 5000 ms)
  • Correlation design: the 3E frame carries no transaction id, so the connection runs a single in-flight request (maxConcurrentRequests = 1) with one pending-response slot. A timeout surfaces as REMOTE_ERROR for that tag only; other failures as INTERNAL_ERROR; one tag's failure never fails the whole read (partial-failure isolation). The mis-attribution caveat for late responses after a timeout is documented at the slot — 3E simply cannot prevent it, so a timed-out read should be treated as unreliable by the caller.
  • Housekeeping: website protocol page + nav entry + feature-matrix rows, RELEASE_NOTES entry, POM on the repo-wide Maven 4.1.0 model. I also registered the driver in plc4j-driver-all, following the majority of drivers — but noticed UMAS isn't registered there; happy to drop the registration if you'd rather stage it the same way

Validation

  • The ParserSerializer vectors from Add SLMP (MELSEC Communication 3E) protocol module (read-only wire layer) #2597 (worked examples of the public Mitsubishi reference manual SH-080008) are now actually executed by this module — 5/5 round-trip byte-identically through the generated code, closing the gap flagged in the protocol PR
  • End-to-end DriverTestsuite IT (API read → codec → response → PlcValue)
  • Failure-path unit tests over a scripted transport (modeled on ModbusRtuConnectionRequestChainTest): timeout isolation, transport loss mid-flight, send failure, unsolicited frame. Writing these exposed a real bug during development — the partial-failure mapping sat behind allOf(...).thenApply(...), which never runs on exceptional completion, so one failed tag failed the entire read; fixed with handle() and now pinned by the tests
  • Interop against an independently developed third-party 3E implementation over a live TCP socket: batch reads of D/W devices, including a multi-word value produced by the other implementation's encoder and decoded by this driver — confirming low-word-first byte order across implementations. The exercise surfaced two deviations from SH-080008 on the other side: a response-subheader mismatch observed on the wire (their responses had to be corrected locally before they would parse, since this driver discriminates responses on the spec's 0xD000 subheader), and an R-device-code difference found while reading their source (avoided in the test by sticking to D/W). The throwaway harness is not part of this PR; logs and details available on request
  • JaCoCo: 86% line coverage on the hand-written classes from a clean build (module gate: 70%)
  • Module verify (tests + JaCoCo gate) green with the branch rebased onto current develop; Apache RAT at module level: 32/32 files approved, 0 unknown

Bench validation on real MELSEC hardware would be very welcome — the driver is read-only, so pointing it at a test PLC is side-effect-free.

Next (not in this PR)


Developed with AI assistance; the wire behavior is verified against SH-080008's worked examples and cross-checked against an independent third-party implementation as noted above.

Signed-off-by: Jooyoung Jung <livinglikekrillin@gmail.com>
Signed-off-by: Jooyoung Jung <livinglikekrillin@gmail.com>
Signed-off-by: Jooyoung Jung <livinglikekrillin@gmail.com>
Signed-off-by: Jooyoung Jung <livinglikekrillin@gmail.com>
Signed-off-by: Jooyoung Jung <livinglikekrillin@gmail.com>
Signed-off-by: Jooyoung Jung <livinglikekrillin@gmail.com>
Signed-off-by: Jooyoung Jung <livinglikekrillin@gmail.com>
Signed-off-by: Jooyoung Jung <livinglikekrillin@gmail.com>
Add SlmpParserSerializerTest (extends ParserSerializerTestsuiteRunner) so the
3E frame reference vectors in ParserSerializerTestsuite.xml are actually
exercised instead of sitting unused, and wire in the protocols-slmp test-jar
so the testsuite resource is on the driver test classpath.

Correct the deviceCode enum fields in the testsuite from the shorthand
<deviceCode>D</deviceCode> to the canonical <SlmpDeviceCode .../> element form
so the vectors round-trip through parse/serialize.

Signed-off-by: Jooyoung Jung <livinglikekrillin@gmail.com>
- Add SLMP protocol documentation page to the website and nav
- Add release-notes entry for the new driver
- Register the driver in the plc4j-driver-all bundle
- Migrate the module POM to the Maven 4.1.0 model used repo-wide

Signed-off-by: Jooyoung Jung <livinglikekrillin@gmail.com>
CompletableFuture.allOf completes exceptionally when any tag future
fails, and thenApply never runs on exceptional completion - so the
per-tag error mapping (timeout -> REMOTE_ERROR, anything else ->
INTERNAL_ERROR) was unreachable and a single failed tag failed the
entire read request, contrary to the documented partial-failure
isolation. Assemble the response in handle() instead.

Add SlmpConnectionFailurePathTest (modeled on
ModbusRtuConnectionRequestChainTest) covering timeout isolation,
transport loss, send failure and unsolicited frames.

Signed-off-by: Jooyoung Jung <livinglikekrillin@gmail.com>
The docs advertise W0x1A:WORD[10]; the 0x prefix, datatype and quantity
were each tested separately but never combined in one address.

Signed-off-by: Jooyoung Jung <livinglikekrillin@gmail.com>
Language table: Java marked work-in-progress (read-only v0).
Feature table: read single/multiple green, everything else red.

Signed-off-by: Jooyoung Jung <livinglikekrillin@gmail.com>
Align the website wording with the code comment: 960 is this driver's
conservative per-request ceiling, not asserted as the device maximum.

Signed-off-by: Jooyoung Jung <livinglikekrillin@gmail.com>
The timeout cleanup ran pendingResponse = null unconditionally on the
delayer thread. CompletableFuture dependents complete LIFO, so the
throttle releases the next request before that cleanup runs; in that
window the successor installs its own slot, which the cleanup then
wiped - dropping the successor's genuine response as unsolicited and
spuriously timing it out as well. Hold the slot in an AtomicReference
and compare-and-clear against the owning future; claims in
handleIncomingMessage/failPending use getAndSet.

Signed-off-by: Jooyoung Jung <livinglikekrillin@gmail.com>
- order the AtomicReference import per ASCII convention
- state the LIFO dependent ordering as OpenJDK behavior, and note the
  compare-and-clear is correct regardless of it
- log when a late response is dropped against an already-completed
  request, matching the unsolicited-frame log

Signed-off-by: Jooyoung Jung <livinglikekrillin@gmail.com>
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.

1 participant