blob: 1aa8fe19b51e8a816883dff52630330876461af7 [file] [log] [blame]
/*
* 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
*
* http://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.modbus.base.protocol;
import org.apache.plc4x.java.api.exceptions.PlcRuntimeException;
import org.apache.plc4x.java.api.messages.*;
import org.apache.plc4x.java.api.value.*;
import org.apache.plc4x.java.api.model.PlcField;
import org.apache.plc4x.java.api.types.PlcResponseCode;
import org.apache.plc4x.java.modbus.tcp.config.ModbusTcpConfiguration;
import org.apache.plc4x.java.modbus.base.field.ModbusField;
import org.apache.plc4x.java.modbus.base.field.ModbusFieldCoil;
import org.apache.plc4x.java.modbus.base.field.ModbusFieldDiscreteInput;
import org.apache.plc4x.java.modbus.base.field.ModbusFieldHoldingRegister;
import org.apache.plc4x.java.modbus.base.field.ModbusFieldInputRegister;
import org.apache.plc4x.java.modbus.base.field.ModbusExtendedRegister;
import org.apache.plc4x.java.modbus.readwrite.*;
import org.apache.plc4x.java.spi.ConversationContext;
import org.apache.plc4x.java.spi.Plc4xProtocolBase;
import org.apache.plc4x.java.spi.configuration.HasConfiguration;
import org.apache.plc4x.java.spi.generation.*;
import org.apache.plc4x.java.spi.messages.DefaultPlcReadRequest;
import org.apache.plc4x.java.spi.messages.DefaultPlcReadResponse;
import org.apache.plc4x.java.spi.messages.DefaultPlcWriteRequest;
import org.apache.plc4x.java.spi.messages.DefaultPlcWriteResponse;
import org.apache.plc4x.java.spi.messages.utils.ResponseItem;
import org.apache.plc4x.java.spi.transaction.RequestTransactionManager;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.plc4x.java.spi.values.PlcBOOL;
import org.apache.plc4x.java.spi.values.PlcList;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
public abstract class ModbusProtocolLogic<T extends ModbusADU> extends Plc4xProtocolBase<T> {
protected final DriverType driverType;
protected Duration requestTimeout;
protected short unitIdentifier;
protected RequestTransactionManager tm;
protected final AtomicInteger transactionIdentifierGenerator = new AtomicInteger(1);
protected final static int FC_EXTENDED_REGISTERS_GROUP_HEADER_LENGTH = 2;
protected final static int FC_EXTENDED_REGISTERS_FILE_RECORD_LENGTH = 10000;
public ModbusProtocolLogic(DriverType driverType) {
this.driverType = driverType;
}
@Override
public void close(ConversationContext<T> context) {
// Nothing to do here ...
}
@Override
protected void decode(ConversationContext<T> context, T msg) throws Exception {
super.decode(context, msg);
}
protected PlcResponseCode getErrorCode(ModbusPDUError errorResponse) {
switch (errorResponse.getExceptionCode()) {
case ILLEGAL_FUNCTION:
return PlcResponseCode.UNSUPPORTED;
case ILLEGAL_DATA_ADDRESS:
return PlcResponseCode.INVALID_ADDRESS;
case ILLEGAL_DATA_VALUE:
return PlcResponseCode.INVALID_DATA;
case SLAVE_DEVICE_FAILURE:
return PlcResponseCode.REMOTE_ERROR;
case ACKNOWLEDGE:
return PlcResponseCode.OK;
case SLAVE_DEVICE_BUSY:
return PlcResponseCode.REMOTE_BUSY;
case NEGATIVE_ACKNOWLEDGE:
return PlcResponseCode.REMOTE_ERROR;
case MEMORY_PARITY_ERROR:
return PlcResponseCode.INTERNAL_ERROR;
case GATEWAY_PATH_UNAVAILABLE:
return PlcResponseCode.INTERNAL_ERROR;
case GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND:
return PlcResponseCode.REMOTE_ERROR;
default:
// This generally implies that something went wrong which we didn't anticipate.
return PlcResponseCode.INTERNAL_ERROR;
}
}
protected ModbusPDU getReadRequestPdu(PlcField field) {
if (field instanceof ModbusFieldDiscreteInput) {
ModbusFieldDiscreteInput discreteInput = (ModbusFieldDiscreteInput) field;
return new ModbusPDUReadDiscreteInputsRequest(discreteInput.getAddress(), discreteInput.getNumberOfElements());
} else if (field instanceof ModbusFieldCoil) {
ModbusFieldCoil coil = (ModbusFieldCoil) field;
return new ModbusPDUReadCoilsRequest(coil.getAddress(), coil.getNumberOfElements());
} else if (field instanceof ModbusFieldInputRegister) {
ModbusFieldInputRegister inputRegister = (ModbusFieldInputRegister) field;
return new ModbusPDUReadInputRegistersRequest(inputRegister.getAddress(), Math.max(inputRegister.getLengthWords(), 1));
} else if (field instanceof ModbusFieldHoldingRegister) {
ModbusFieldHoldingRegister holdingRegister = (ModbusFieldHoldingRegister) field;
return new ModbusPDUReadHoldingRegistersRequest(holdingRegister.getAddress(), Math.max(holdingRegister.getLengthWords(), 1));
} else if (field instanceof ModbusExtendedRegister) {
ModbusExtendedRegister extendedRegister = (ModbusExtendedRegister) field;
int group1Address = extendedRegister.getAddress() % 10000;
int group2Address = 0;
int group1Quantity;
int group2Quantity;
short group1FileNumber = (short) (Math.floor((float) extendedRegister.getAddress() / 10000) + 1);
short group2FileNumber;
List<ModbusPDUReadFileRecordRequestItem> itemArray;
if ((group1Address + extendedRegister.getLengthWords()) <= FC_EXTENDED_REGISTERS_FILE_RECORD_LENGTH) {
// If request doesn't span file records, use a single group
group1Quantity = extendedRegister.getLengthWords();
ModbusPDUReadFileRecordRequestItem group1 =
new ModbusPDUReadFileRecordRequestItem((short) 6, group1FileNumber, group1Address, group1Quantity);
itemArray = Collections.singletonList(group1);
} else {
// If it doesn't span a file record. e.g. 609998[10] request 2 words in first group and 8 in second.
group1Quantity = FC_EXTENDED_REGISTERS_FILE_RECORD_LENGTH - group1Address;
group2Quantity = extendedRegister.getLengthWords() - group1Quantity;
group2FileNumber = (short) (group1FileNumber + 1);
ModbusPDUReadFileRecordRequestItem group1 =
new ModbusPDUReadFileRecordRequestItem((short) 6, group1FileNumber, group1Address, group1Quantity);
ModbusPDUReadFileRecordRequestItem group2 =
new ModbusPDUReadFileRecordRequestItem((short) 6, group2FileNumber, group2Address, group2Quantity);
itemArray = Arrays.asList(group1, group2);
}
return new ModbusPDUReadFileRecordRequest(itemArray);
}
throw new PlcRuntimeException("Unsupported read field type " + field.getClass().getName());
}
protected ModbusPDU getWriteRequestPdu(PlcField field, PlcValue plcValue) {
if (field instanceof ModbusFieldCoil) {
ModbusFieldCoil coil = (ModbusFieldCoil) field;
ModbusPDUWriteMultipleCoilsRequest request =
new ModbusPDUWriteMultipleCoilsRequest(coil.getAddress(), coil.getNumberOfElements(),
fromPlcValue(field, plcValue));
if (request.getQuantity() == coil.getNumberOfElements()) {
return request;
} else {
throw new PlcRuntimeException("Number of requested bytes (" + request.getQuantity() +
") doesn't match number of requested addresses (" + coil.getNumberOfElements() + ")");
}
} else if (field instanceof ModbusFieldHoldingRegister) {
ModbusFieldHoldingRegister holdingRegister = (ModbusFieldHoldingRegister) field;
ModbusPDUWriteMultipleHoldingRegistersRequest request =
new ModbusPDUWriteMultipleHoldingRegistersRequest(holdingRegister.getAddress(),
holdingRegister.getLengthWords(), fromPlcValue(field, plcValue));
if (request.getValue().length == holdingRegister.getLengthWords() * 2) {
return request;
} else {
throw new PlcRuntimeException("Number of requested values (" + request.getValue().length / 2 +
") doesn't match number of requested addresses (" + holdingRegister.getLengthWords() + ")");
}
} else if (field instanceof ModbusExtendedRegister) {
ModbusExtendedRegister extendedRegister = (ModbusExtendedRegister) field;
int group1Address = extendedRegister.getAddress() % FC_EXTENDED_REGISTERS_FILE_RECORD_LENGTH;
int group2Address = 0;
int group1Quantity;
int group2Quantity;
byte[] plcValue1, plcValue2;
short group1FileNumber = (short)
(Math.floor((float) extendedRegister.getAddress() / FC_EXTENDED_REGISTERS_FILE_RECORD_LENGTH) + 1);
short group2FileNumber;
List<ModbusPDUWriteFileRecordRequestItem> itemArray;
if ((group1Address + extendedRegister.getLengthWords()) <= FC_EXTENDED_REGISTERS_FILE_RECORD_LENGTH) {
//If request doesn't span file records, use a single group
group1Quantity = extendedRegister.getLengthWords();
ModbusPDUWriteFileRecordRequestItem group1 = new ModbusPDUWriteFileRecordRequestItem(
(short) 6, group1FileNumber, group1Address, fromPlcValue(field, plcValue));
itemArray = Collections.singletonList(group1);
} else {
//If it doesn't span a file record. e.g. 609998[10] request 2 words in first group and 8 in second.
group1Quantity = FC_EXTENDED_REGISTERS_FILE_RECORD_LENGTH - group1Address;
group2Quantity = extendedRegister.getLengthWords() - group1Quantity;
group2FileNumber = (short) (group1FileNumber + 1);
plcValue1 = ArrayUtils.subarray(fromPlcValue(field, plcValue), 0, group1Quantity);
plcValue2 = ArrayUtils.subarray(
fromPlcValue(field, plcValue), group1Quantity, fromPlcValue(field, plcValue).length);
ModbusPDUWriteFileRecordRequestItem group1 = new ModbusPDUWriteFileRecordRequestItem(
(short) 6, group1FileNumber, group1Address, plcValue1);
ModbusPDUWriteFileRecordRequestItem group2 = new ModbusPDUWriteFileRecordRequestItem(
(short) 6, group2FileNumber, group2Address, plcValue2);
itemArray = Arrays.asList(group1, group2);
}
return new ModbusPDUWriteFileRecordRequest(itemArray);
}
throw new PlcRuntimeException("Unsupported write field type " + field.getClass().getName());
}
protected PlcValue toPlcValue(ModbusPDU request, ModbusPDU response, ModbusDataType dataType) throws ParseException {
short fieldDataTypeSize = dataType.getDataTypeSize();
if (request instanceof ModbusPDUReadDiscreteInputsRequest) {
if (!(response instanceof ModbusPDUReadDiscreteInputsResponse)) {
throw new PlcRuntimeException("Unexpected response type. " +
"Expected ModbusPDUReadDiscreteInputsResponse, but got " + response.getClass().getName());
}
ModbusPDUReadDiscreteInputsRequest req = (ModbusPDUReadDiscreteInputsRequest) request;
ModbusPDUReadDiscreteInputsResponse resp = (ModbusPDUReadDiscreteInputsResponse) response;
return readBooleanList(req.getQuantity(), resp.getValue());
} else if (request instanceof ModbusPDUReadCoilsRequest) {
if (!(response instanceof ModbusPDUReadCoilsResponse)) {
throw new PlcRuntimeException("Unexpected response type. " +
"Expected ModbusPDUReadCoilsResponse, but got " + response.getClass().getName());
}
ModbusPDUReadCoilsRequest req = (ModbusPDUReadCoilsRequest) request;
ModbusPDUReadCoilsResponse resp = (ModbusPDUReadCoilsResponse) response;
return readBooleanList(req.getQuantity(), resp.getValue());
} else if (request instanceof ModbusPDUReadInputRegistersRequest) {
if (!(response instanceof ModbusPDUReadInputRegistersResponse)) {
throw new PlcRuntimeException("Unexpected response type. " +
"Expected ModbusPDUReadInputRegistersResponse, but got " + response.getClass().getName());
}
ModbusPDUReadInputRegistersRequest req = (ModbusPDUReadInputRegistersRequest) request;
ModbusPDUReadInputRegistersResponse resp = (ModbusPDUReadInputRegistersResponse) response;
ReadBuffer io = new ReadBufferByteBased(resp.getValue());
if (fieldDataTypeSize < 2) {
io.readByte();
}
return DataItem.staticParse(io, dataType, Math.max(Math.round(req.getQuantity() / (fieldDataTypeSize / 2.0f)), 1));
} else if (request instanceof ModbusPDUReadHoldingRegistersRequest) {
if (!(response instanceof ModbusPDUReadHoldingRegistersResponse)) {
throw new PlcRuntimeException("Unexpected response type. " +
"Expected ModbusPDUReadHoldingRegistersResponse, but got " + response.getClass().getName());
}
ModbusPDUReadHoldingRegistersRequest req = (ModbusPDUReadHoldingRegistersRequest) request;
ModbusPDUReadHoldingRegistersResponse resp = (ModbusPDUReadHoldingRegistersResponse) response;
ReadBuffer io = new ReadBufferByteBased(resp.getValue());
if ((dataType != ModbusDataType.STRING) && fieldDataTypeSize < 2) {
io.readByte();
}
return DataItem.staticParse(io, dataType, Math.max(Math.round(req.getQuantity() / (fieldDataTypeSize / 2.0f)), 1));
} else if (request instanceof ModbusPDUReadFileRecordRequest) {
if (!(response instanceof ModbusPDUReadFileRecordResponse)) {
throw new PlcRuntimeException("Unexpected response type. " +
"Expected ModbusPDUReadFileRecordResponse, but got " + response.getClass().getName());
}
ModbusPDUReadFileRecordRequest req = (ModbusPDUReadFileRecordRequest) request;
ModbusPDUReadFileRecordResponse resp = (ModbusPDUReadFileRecordResponse) response;
ReadBuffer io;
short dataLength;
if (resp.getItems().size() == 2 && resp.getItems().size() == req.getItems().size()) {
//If request was split over file records, two groups in response should be received.
io = new ReadBufferByteBased(ArrayUtils.addAll(resp.getItems().get(0).getData(), resp.getItems().get(1).getData()));
dataLength = (short) (resp.getItems().get(0).getLengthInBytes() + resp.getItems().get(1).getLengthInBytes() - (2 * FC_EXTENDED_REGISTERS_GROUP_HEADER_LENGTH));
} else if (resp.getItems().size() == 1 && resp.getItems().size() == req.getItems().size()) {
//If request was within a single file record, one group should be received.
io = new ReadBufferByteBased(resp.getItems().get(0).getData());
dataLength = (short) (resp.getItems().get(0).getLengthInBytes() - FC_EXTENDED_REGISTERS_GROUP_HEADER_LENGTH);
} else {
throw new PlcRuntimeException("Unexpected number of groups in response. " +
"Expected " + req.getItems().size() + ", but got " + resp.getItems().size());
}
if (fieldDataTypeSize < 2) {
io.readByte();
}
return DataItem.staticParse(io, dataType, Math.round(Math.max(dataLength / 2.0f, 1) / Math.max(fieldDataTypeSize / 2.0f, 1)));
}
return null;
}
protected byte[] fromPlcValue(PlcField field, PlcValue plcValue) {
ModbusDataType fieldDataType = ((ModbusField) field).getDataType();
try {
if (plcValue instanceof PlcList) {
WriteBufferByteBased writeBuffer = new WriteBufferByteBased(DataItem.getLengthInBytes(plcValue, fieldDataType, plcValue.getLength()));
DataItem.staticSerialize(writeBuffer, plcValue, fieldDataType, plcValue.getLength(), ByteOrder.BIG_ENDIAN);
byte[] data = writeBuffer.getData();
if (((ModbusField) field).getDataType() == ModbusDataType.BOOL) {
//Reverse Bits in each byte as
//they should be ordered like this: 8 7 6 5 4 3 2 1 | 0 0 0 0 0 0 0 9
byte[] bytes = new byte[data.length];
for (int i = 0; i < data.length; i++) {
bytes[i] = reverseBitsOfByte(data[i]);
}
return bytes;
}
return data;
} else {
WriteBufferByteBased writeBuffer = new WriteBufferByteBased(DataItem.getLengthInBytes(plcValue, fieldDataType, plcValue.getLength()));
DataItem.staticSerialize(writeBuffer, plcValue, fieldDataType, plcValue.getLength(), ByteOrder.BIG_ENDIAN);
return writeBuffer.getData();
}
} catch (SerializationException e) {
throw new PlcRuntimeException("Unable to parse PlcValue :- " + e);
}
}
protected byte reverseBitsOfByte(byte b) {
BitSet bits = BitSet.valueOf(new byte[]{b});
BitSet reverse = BitSet.valueOf(new byte[]{(byte) 0xFF});
for (int j = 0; j < 8; j++) {
reverse.set(j, bits.get(7 - j));
}
//toByteArray returns an empty array if all the bits are set to 0.
return Arrays.copyOf(reverse.toByteArray(), 1)[0];
}
protected PlcValue readBooleanList(int count, byte[] data) throws ParseException {
ReadBuffer io = new ReadBufferByteBased(data);
if (count == 1) {
return DataItem.staticParse(io, ModbusDataType.BOOL, 1);
}
// Make sure we read in all the bytes. Unfortunately when requesting 9 bytes
// they are ordered like this: 8 7 6 5 4 3 2 1 | 0 0 0 0 0 0 0 9
// Luckily it turns out that this is exactly how BitSet parses byte[]
BitSet bits = BitSet.valueOf(data);
List<PlcValue> result = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
result.add(new PlcBOOL(bits.get(i)));
}
return new PlcList(result);
}
}