Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
37a5e70
feat(plc4j/slmp): model Batch Write (0x1401) request in the mspec
LivingLikeKrillin Jul 20, 2026
fb48c02
test(plc4j/slmp): pin the Batch Write 0x1401 frame bytes
LivingLikeKrillin Jul 20, 2026
910a371
docs(plc4j/slmp): sync mspec header section index and SlmpRequestData…
LivingLikeKrillin Jul 20, 2026
3c3695c
feat(plc4j/slmp): add SlmpDataType.encode (mirror of decode)
LivingLikeKrillin Jul 20, 2026
a85b145
feat(plc4j/slmp): map Batch Write endCode to a per-tag response code
LivingLikeKrillin Jul 20, 2026
d82a5e7
feat(plc4j/slmp): implement Batch Write onWrite path
LivingLikeKrillin Jul 20, 2026
7fd1885
refactor(plc4j/slmp): generalize single-frame ceiling wording for bat…
LivingLikeKrillin Jul 20, 2026
9b31bb1
test(plc4j/slmp): end-to-end Batch Write driver-testsuite case
LivingLikeKrillin Jul 20, 2026
822d503
fix(plc4j/slmp): declare write capability in SlmpDriver
LivingLikeKrillin Jul 20, 2026
0f6abc9
docs(plc4j/slmp): drop stale read-only wording now that Batch Write i…
LivingLikeKrillin Jul 20, 2026
55557ee
docs(plc4j/slmp): drop stale read-only wording from the driver POM de…
LivingLikeKrillin Jul 20, 2026
65c87ac
docs(plc4j/slmp): correct SH-080008 section references against the ma…
LivingLikeKrillin Jul 20, 2026
006f344
refactor(plc4j/slmp): address review feedback
LivingLikeKrillin Jul 21, 2026
2bc9889
fix(plc4j/slmp): echo builder error items instead of NPE-masking them…
LivingLikeKrillin Jul 21, 2026
10f2f94
fix(plc4j/slmp): reject unexpected payload on write success; document…
LivingLikeKrillin Jul 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plc4j/drivers/slmp/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
<artifactId>plc4j-driver-slmp</artifactId>

<name>PLC4J: Driver: SLMP</name>
<description>Implementation of a PLC4X read-only driver for the SLMP / MELSEC Communication 3E protocol.</description>
<description>Implementation of a PLC4X driver for the SLMP / MELSEC Communication 3E protocol.</description>

<properties>
<project.build.outputTimestamp>2025-08-02T13:55:11Z</project.build.outputTimestamp>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ public static SlmpRequestData staticParse(ReadBuffer readBuffer, int command) th
builder = SlmpRandomReadRequest.staticParseSlmpRequestDataBuilder(readBuffer, command);
} else if (EvaluationHelper.equals(command, (int) (0x0406))) {
builder = SlmpMultiBlockReadRequest.staticParseSlmpRequestDataBuilder(readBuffer, command);
} else if (EvaluationHelper.equals(command, (int) (0x1401))) {
builder = SlmpWriteRequest.staticParseSlmpRequestDataBuilder(readBuffer, command);
}
if (builder == null) {
throw new BufferException("Unsupported case for discriminated type parameters parameters [command]");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.plc4x.java.slmp.readwrite;

import org.apache.plc4x.java.spi.buffers.api.Message;
import org.apache.plc4x.java.spi.buffers.api.ReadBuffer;
import org.apache.plc4x.java.spi.buffers.api.WithOption;
import org.apache.plc4x.java.spi.buffers.api.WriteBuffer;
import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferException;
import org.apache.plc4x.java.spi.fields.data.reader.DataReaderFactory;
import org.apache.plc4x.java.spi.fields.data.writer.DataWriterFactory;
import org.apache.plc4x.java.spi.fields.fields.reader.FieldReaderFactory;
import org.apache.plc4x.java.spi.fields.fields.writer.FieldWriterFactory;
import org.apache.plc4x.java.spi.fields.utils.ThreadLocalHelper;

/**
* Code generated by code-generation. DO NOT EDIT.
*/
public class SlmpWriteRequest extends SlmpRequestData implements Message {
protected final int headDeviceNumber;

protected final SlmpDeviceCode deviceCode;

protected final int numberOfPoints;

protected final byte[] writeData;

public SlmpWriteRequest(Integer headDeviceNumber, SlmpDeviceCode deviceCode,
Integer numberOfPoints, byte[] writeData) {
this.headDeviceNumber = headDeviceNumber;
this.deviceCode = deviceCode;
this.numberOfPoints = numberOfPoints;
this.writeData = writeData;
}

/**
* Discriminator field command
*/
@Override
public int getCommand() {
return (int) 0x1401;
}

/**
* Property field headDeviceNumber
*/
public int getHeadDeviceNumber() {
return headDeviceNumber;
}

/**
* Property field deviceCode
*/
public SlmpDeviceCode getDeviceCode() {
return deviceCode;
}

/**
* Property field numberOfPoints
*/
public int getNumberOfPoints() {
return numberOfPoints;
}

/**
* Property field writeData
*/
public byte[] getWriteData() {
return writeData;
}

public static SlmpRequestDataBuilder staticParseSlmpRequestDataBuilder(ReadBuffer readBuffer,
int command) throws BufferException {
readBuffer.pushContext(WithOption.WithName("SlmpWriteRequest"));
int startPos = readBuffer.getPositionInBits();
boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get();
// Simple Field: headDeviceNumber
int headDeviceNumber = FieldReaderFactory.readSimpleField(DataReaderFactory.readUnsignedInt(readBuffer, 24), WithOption.WithName("headDeviceNumber"));

// Simple Field (enum): deviceCode
SlmpDeviceCode deviceCode = FieldReaderFactory.readEnumField(DataReaderFactory.readEnum(SlmpDeviceCode::enumForValue, DataReaderFactory.readUnsignedShort(readBuffer, 8)), WithOption.WithName("deviceCode"));

// Simple Field: numberOfPoints
int numberOfPoints = FieldReaderFactory.readSimpleField(DataReaderFactory.readUnsignedInt(readBuffer, 16), WithOption.WithName("numberOfPoints"));

// Array Field: writeData
byte[] writeData = readBuffer.readBits(Math.toIntExact(((numberOfPoints) * (2)) * 8), WithOption.WithName("writeData"));

readBuffer.popContext();
return new SlmpRequestDataBuilderImpl(headDeviceNumber, deviceCode, numberOfPoints, writeData);
}

protected void serializeSlmpRequestDataChild(WriteBuffer writeBuffer) throws BufferException {
writeBuffer.pushContext(WithOption.WithName("SlmpWriteRequest"));
int startPos = writeBuffer.getPositionInBits();
boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get();
// Simple Field: headDeviceNumber
FieldWriterFactory.writeSimpleField((int) headDeviceNumber, DataWriterFactory.writeUnsignedInt(writeBuffer, 24), WithOption.WithName("headDeviceNumber"));

// Simple Field (enum): deviceCode
FieldWriterFactory.writeSimpleEnumField((SlmpDeviceCode) deviceCode, DataWriterFactory.writeEnum(SlmpDeviceCode::getValue, SlmpDeviceCode::name, DataWriterFactory.writeUnsignedShort(writeBuffer, 8)), WithOption.WithName("deviceCode"));

// Simple Field: numberOfPoints
FieldWriterFactory.writeSimpleField((int) numberOfPoints, DataWriterFactory.writeUnsignedInt(writeBuffer, 16), WithOption.WithName("numberOfPoints"));

// Array Field: writeData
FieldWriterFactory.writeByteArrayField(writeData, DataWriterFactory.writeByteArray(writeBuffer, (int) ((writeData != null) ? writeData.length : 0)), WithOption.WithName("writeData"));

writeBuffer.popContext();
}

@Override
public int getLengthInBytes() {
return (int) Math.ceil((float) getLengthInBits() / 8.0);
}

@Override
public int getLengthInBits() {
int lengthInBits = super.getLengthInBits();
SlmpWriteRequest _value = this;
boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get();
// Simple Field: headDeviceNumber
lengthInBits += 24;

// Simple Field: deviceCode
lengthInBits += 8;

// Simple Field: numberOfPoints
lengthInBits += 16;

// Array Field: writeData
lengthInBits += 8 * ((writeData != null) ? writeData.length : 0);

return lengthInBits;
}

public static class SlmpRequestDataBuilderImpl implements SlmpRequestData.SlmpRequestDataBuilder {
private final int headDeviceNumber;

private final SlmpDeviceCode deviceCode;

private final int numberOfPoints;

private final byte[] writeData;

public SlmpRequestDataBuilderImpl(int headDeviceNumber, SlmpDeviceCode deviceCode,
int numberOfPoints, byte[] writeData) {
this.headDeviceNumber = headDeviceNumber;
this.deviceCode = deviceCode;
this.numberOfPoints = numberOfPoints;
this.writeData = writeData;
}

public SlmpRequestData build() {
return new SlmpWriteRequest(headDeviceNumber, deviceCode, numberOfPoints, writeData);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import org.apache.plc4x.java.api.exceptions.PlcRuntimeException;
import org.apache.plc4x.java.api.messages.PlcReadRequest;
import org.apache.plc4x.java.api.messages.PlcReadResponse;
import org.apache.plc4x.java.api.messages.PlcWriteRequest;
import org.apache.plc4x.java.api.messages.PlcWriteResponse;
import org.apache.plc4x.java.api.types.ConnectionStateChangeType;
import org.apache.plc4x.java.api.types.PlcResponseCode;
import org.apache.plc4x.java.api.value.PlcValue;
Expand All @@ -30,12 +32,15 @@
import org.apache.plc4x.java.slmp.readwrite.SlmpReadRequest;
import org.apache.plc4x.java.slmp.readwrite.SlmpRequestFrame3E;
import org.apache.plc4x.java.slmp.readwrite.SlmpResponseFrame3E;
import org.apache.plc4x.java.slmp.readwrite.SlmpWriteRequest;
import org.apache.plc4x.java.slmp.tag.SlmpTag;
import org.apache.plc4x.java.slmp.tag.SlmpTagHandler;
import org.apache.plc4x.java.spi.drivers.ConnectionBase;
import org.apache.plc4x.java.spi.drivers.exceptions.MessageCodecException;
import org.apache.plc4x.java.spi.drivers.messages.DefaultPlcReadRequest;
import org.apache.plc4x.java.spi.drivers.messages.DefaultPlcReadResponse;
import org.apache.plc4x.java.spi.drivers.messages.DefaultPlcWriteRequest;
import org.apache.plc4x.java.spi.drivers.messages.DefaultPlcWriteResponse;
import org.apache.plc4x.java.spi.drivers.messages.items.DefaultPlcResponseItem;
import org.apache.plc4x.java.spi.drivers.messages.items.PlcResponseItem;
import org.apache.plc4x.java.spi.drivers.tags.PlcTagHandler;
Expand Down Expand Up @@ -168,8 +173,10 @@ private void failPending(Throwable cause) {
* <p>
* Caveat: if a request times out, a late response for it may arrive on the receiver thread
* after the next request has installed its own slot, and would then be mis-attributed to that
* next request. 3E cannot prevent this (no correlation key), so a timed-out read should be
* treated as unreliable by the caller.
* next request. 3E cannot prevent this (no correlation key), so a timed-out request should be
* treated as unreliable by the caller. For a timed-out WRITE in particular the device may
* nevertheless have applied the write — the driver cannot tell — so blindly retrying a write
* that returned REMOTE_ERROR may apply it twice.
* <p>
* All slot clean-up is identity-checked (compare-and-clear): the timeout callback runs on the
* delayer thread and may run after the throttle has already released the next request (OpenJDK
Expand Down Expand Up @@ -203,6 +210,14 @@ protected CompletableFuture<PlcReadResponse> onRead(PlcReadRequest readRequest)
LinkedHashMap<String, CompletableFuture<PlcResponseItem<PlcValue>>> tagFutures = new LinkedHashMap<>();
CompletableFuture<Void> chain = CompletableFuture.completedFuture(null);
for (String tagName : request.getTagNames()) {
PlcResponseCode buildCode = request.getTagResponseCode(tagName);
if (buildCode != null && buildCode != PlcResponseCode.OK) {
// The builder already rejected this tag (unparseable address): echo its code
// instead of dereferencing the null tag. Nothing is sent for this tag.
tagFutures.put(tagName,
CompletableFuture.completedFuture(new DefaultPlcResponseItem<>(buildCode, null)));
continue;
}
SlmpTag tag = (SlmpTag) request.getTag(tagName);
CompletableFuture<PlcResponseItem<PlcValue>> tagFuture =
chain.thenComposeAsync(v -> readSingleTag(tag));
Expand Down Expand Up @@ -244,4 +259,63 @@ private CompletableFuture<PlcResponseItem<PlcValue>> readSingleTag(SlmpTag tag)
sendRequest(frame).thenApply(response ->
SlmpResponseMapper.mapTag(tag, response.getEndCode(), response.getResponseData())));
}

@Override
protected CompletableFuture<PlcWriteResponse> onWrite(PlcWriteRequest writeRequest) {
// Structural mirror of onRead; see there for the handle-vs-thenApply and per-tag partial-failure-isolation rationale.
// A tag that maps to REMOTE_ERROR after a timeout may still have been applied by the device; see sendRequest().
DefaultPlcWriteRequest request = (DefaultPlcWriteRequest) writeRequest;
LinkedHashMap<String, CompletableFuture<PlcResponseCode>> tagFutures = new LinkedHashMap<>();
CompletableFuture<Void> chain = CompletableFuture.completedFuture(null);
for (String tagName : request.getTagNames()) {
PlcResponseCode buildCode = request.getTagResponseCode(tagName);
if (buildCode != null && buildCode != PlcResponseCode.OK) {
// The builder already rejected this tag (bad address or un-coercible value):
// echo its code instead of dereferencing the null tag/value. Nothing is sent
// for this tag.
tagFutures.put(tagName, CompletableFuture.completedFuture(buildCode));
continue;
}
SlmpTag tag = (SlmpTag) request.getTag(tagName);
PlcValue value = request.getPlcValue(tagName);
CompletableFuture<PlcResponseCode> tagFuture =
chain.thenComposeAsync(v -> writeSingleTag(tag, value));
tagFutures.put(tagName, tagFuture);
chain = tagFuture.handle((r, e) -> null);
}
CompletableFuture<Void> allDone =
CompletableFuture.allOf(tagFutures.values().toArray(new CompletableFuture[0]));
return allDone.handle((v, anyTagFailure) -> {
Map<String, PlcResponseCode> responseCodes = new LinkedHashMap<>();
for (Map.Entry<String, CompletableFuture<PlcResponseCode>> e : tagFutures.entrySet()) {
try {
responseCodes.put(e.getKey(), e.getValue().join());
} catch (Exception ex) {
Throwable cause = (ex instanceof CompletionException && ex.getCause() != null)
? ex.getCause() : ex;
PlcResponseCode code = (cause instanceof TimeoutException)
? PlcResponseCode.REMOTE_ERROR : PlcResponseCode.INTERNAL_ERROR;
responseCodes.put(e.getKey(), code);
}
}
return (PlcWriteResponse) new DefaultPlcWriteResponse(request, responseCodes);
});
}

private CompletableFuture<PlcResponseCode> writeSingleTag(SlmpTag tag, PlcValue value) {
byte[] payload = tag.getDataType().encode(value, tag.getQuantity());
if (payload == null) {
return CompletableFuture.completedFuture(PlcResponseCode.INVALID_DATA);
}
// Invariant: encode() returns exactly quantity*wordsPerElement*2 == numberOfPoints*2 bytes,
// so the SlmpWriteRequest header word-count and payload length always agree (the generated
// serialize path derives length from writeData.length, so this must hold by construction).
SlmpWriteRequest data = new SlmpWriteRequest(
tag.getDeviceNumber(), tag.getDeviceCode(), tag.getNumberOfPoints(), payload);
SlmpRequestFrame3E frame = new SlmpRequestFrame3E(
getConfiguration().getMonitoringTimer(), 0x1401, 0x0000, data);
return executeThrottled(() ->
sendRequest(frame).thenApply(response ->
SlmpResponseMapper.mapWriteTag(tag, response.getEndCode(), response.getResponseData())));
}
}
Loading
Loading