/*
* 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.eip.readwrite.protocol;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import org.apache.plc4x.java.api.exceptions.PlcRuntimeException;
import org.apache.plc4x.java.api.messages.*;
import org.apache.plc4x.java.api.model.PlcField;
import org.apache.plc4x.java.api.types.PlcResponseCode;
import org.apache.plc4x.java.api.value.*;
import org.apache.plc4x.java.eip.readwrite.*;
import org.apache.plc4x.java.eip.readwrite.configuration.EIPConfiguration;
import org.apache.plc4x.java.eip.readwrite.field.EipField;
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.ParseException;
import org.apache.plc4x.java.spi.generation.ReadBuffer;
import org.apache.plc4x.java.spi.generation.ReadBufferByteBased;
import org.apache.plc4x.java.spi.messages.*;
import org.apache.plc4x.java.spi.messages.utils.ResponseItem;
import org.apache.plc4x.java.spi.transaction.RequestTransactionManager;
import org.apache.plc4x.java.spi.values.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
public class EipProtocolLogic extends Plc4xProtocolBase<EipPacket> implements HasConfiguration<EIPConfiguration> {
private static final Logger logger = LoggerFactory.getLogger(EipProtocolLogic.class);
public static final Duration REQUEST_TIMEOUT = Duration.ofMillis(10000);
private static final List<Short> emptySenderContext = Arrays.asList((short) 0x00, (short) 0x00, (short) 0x00,
(short) 0x00, (short) 0x00, (short) 0x00, (short) 0x00, (short) 0x00);
private List<Short> senderContext;
private EIPConfiguration configuration;
private final AtomicInteger transactionCounterGenerator = new AtomicInteger(10);
private RequestTransactionManager tm;
private long sessionHandle;
@Override
public void setConfiguration(EIPConfiguration configuration) {
this.configuration = configuration;
// Set the transaction manager to allow only one message at a time.
this.tm = new RequestTransactionManager(1);
}
@Override
public void onConnect(ConversationContext<EipPacket> context) {
logger.debug("Sending RegisterSession EIP Package");
EipConnectionRequest connectionRequest =
new EipConnectionRequest(0L, 0L, emptySenderContext, 0L);
context.sendRequest(connectionRequest)
.expectResponse(EipPacket.class, REQUEST_TIMEOUT).unwrap(p -> p)
.check(p -> p instanceof EipConnectionRequest)
.handle(p -> {
if (p.getStatus() == 0L) {
sessionHandle = p.getSessionHandle();
senderContext = p.getSenderContext();
logger.debug("Got assigned with Session {}", sessionHandle);
// Send an event that connection setup is complete.
context.fireConnected();
} else {
logger.warn("Got status code [{}]", p.getStatus());
}
});
}
@Override
public CompletableFuture<PlcReadResponse> read(PlcReadRequest readRequest) {
DefaultPlcReadRequest request = (DefaultPlcReadRequest) readRequest;
List<CipReadRequest> requests = new ArrayList<>(request.getNumberOfFields());
for (PlcField field : request.getFields()) {
EipField plcField = (EipField) field;
String tag = plcField.getTag();
int elements = 1;
if (plcField.getElementNb() > 1) {
elements = plcField.getElementNb();
}
CipReadRequest req = new CipReadRequest(getRequestSize(tag), toAnsi(tag), elements, -1);
requests.add(req);
}
return toPlcReadResponse(readRequest, readInternal(requests));
}
private byte getRequestSize(String tag) {
//We need the size of the request in words (0x91, tagLength, ... tag + possible pad)
// Taking half to get word size
boolean isArray = false;
boolean isStruct = false;
String tagIsolated = tag;
if (tag.contains("[")) {
isArray = true;
tagIsolated = tag.substring(0, tag.indexOf("["));
}
if (tag.contains(".")) {
isStruct = true;
tagIsolated = tagIsolated.replace(".", "");
}
int dataLength = (tagIsolated.length() + 2)
+ (tagIsolated.length() % 2)
+ (isArray ? 2 : 0)
+ (isStruct ? 2 : 0);
byte requestPathSize = (byte) (dataLength / 2);
return requestPathSize;
}
private byte[] toAnsi(String tag) {
int arrayIndex = 0;
boolean isArray = false;
boolean isStruct = false;
String tagFinal = tag;
if (tag.contains("[")) {
isArray = true;
String index = tag.substring(tag.indexOf("[") + 1, tag.indexOf("]"));
arrayIndex = Integer.parseInt(index);
tagFinal = tag.substring(0, tag.indexOf("["));
}
if (tag.contains(".")) {
tagFinal = tag.substring(0, tag.indexOf("."));
isStruct = true;
}
boolean isPadded = tagFinal.length() % 2 != 0;
int dataSegLength = 2 + tagFinal.length()
+ (isPadded ? 1 : 0)
+ (isArray ? 2 : 0);
if (isStruct) {
for (String subStr : tag.substring(tag.indexOf(".") + 1).split("\\.", -1)) {
dataSegLength += 2 + subStr.length() + subStr.length() % 2;
}
}
ByteBuffer buffer = ByteBuffer.allocate(dataSegLength).order(ByteOrder.LITTLE_ENDIAN);
buffer.put((byte) 0x91);
buffer.put((byte) tagFinal.length());
byte[] tagBytes = null;
tagBytes = tagFinal.getBytes(StandardCharsets.US_ASCII);
buffer.put(tagBytes);
buffer.position(2 + tagBytes.length);
if (isPadded) {
buffer.put((byte) 0x00);
}
if (isArray) {
buffer.put((byte) 0x28);
buffer.put((byte) arrayIndex);
}
if (isStruct) {
buffer.put(toAnsi(tag.substring(tag.indexOf(".") + 1, tag.length())));
}
return buffer.array();
}
private CompletableFuture<PlcReadResponse> toPlcReadResponse(PlcReadRequest readRequest, CompletableFuture<CipService> response) {
return response
.thenApply(p -> {
return ((PlcReadResponse) decodeReadResponse(p, readRequest));
});
}
private CompletableFuture<CipService> readInternal(List<CipReadRequest> request) {
CompletableFuture<CipService> future = new CompletableFuture<>();
RequestTransactionManager.RequestTransaction transaction = tm.startRequest();
if (request.size() > 1) {
short nb = (short) request.size();
List<Integer> offsets = new ArrayList<>(nb);
int offset = 2 + nb * 2;
for (int i = 0; i < nb; i++) {
offsets.add(offset);
offset += request.get(i).getLengthInBytes();
}
List<CipService> serviceArr = new ArrayList<>(nb);
for (int i = 0; i < nb; i++) {
serviceArr.add(request.get(i));
}
Services data = new Services(nb, offsets, serviceArr, -1);
//Encapsulate the data
CipRRData pkt = new CipRRData(sessionHandle, 0L, emptySenderContext, 0L,
new CipExchange(
new CipUnconnectedRequest(
new MultipleServiceRequest(data, -1),
(byte) configuration.getBackplane(),
(byte) configuration.getSlot(),
-1
),
-1
),
-1
);
transaction.submit(() -> context.sendRequest(pkt)
.expectResponse(EipPacket.class, REQUEST_TIMEOUT)
.onTimeout(future::completeExceptionally)
.onError((p, e) -> future.completeExceptionally(e))
.check(p -> p instanceof CipRRData)
.check(p -> p.getSessionHandle() == sessionHandle)
//.check(p -> p.getSenderContext() == senderContext)
.unwrap(p -> (CipRRData) p)
.unwrap(p -> p.getExchange().getService()).check(p -> p instanceof MultipleServiceResponse)
.unwrap(p -> (MultipleServiceResponse) p)
.check(p -> p.getServiceNb() == nb)
.handle(p -> {
future.complete(p);
// Finish the request-transaction.
transaction.endRequest();
}));
} else if (request.size() == 1) {
CipExchange exchange = new CipExchange(
new CipUnconnectedRequest(
request.get(0), (byte) configuration.getBackplane(), (byte) configuration.getSlot(), -1
),
-1
);
CipRRData pkt = new CipRRData(sessionHandle, 0L, emptySenderContext, 0L, exchange, -1);
transaction.submit(() -> context.sendRequest(pkt)
.expectResponse(EipPacket.class, REQUEST_TIMEOUT)
.onTimeout(future::completeExceptionally)
.onError((p, e) -> future.completeExceptionally(e))
.check(p -> p instanceof CipRRData)
.check(p -> p.getSessionHandle() == sessionHandle)
//.check(p -> p.getSenderContext() == senderContext)
.unwrap(p -> (CipRRData) p)
.unwrap(p -> p.getExchange().getService()).check(p -> p instanceof CipReadResponse)
.unwrap(p -> (CipReadResponse) p)
.handle(p -> {
future.complete(p);
// Finish the request-transaction.
transaction.endRequest();
}));
}
return future;
}
private PlcResponse decodeReadResponse(CipService p, PlcReadRequest readRequest) {
Map<String, ResponseItem<PlcValue>> values = new HashMap<>();
// only 1 field
if (p instanceof CipReadResponse) {
CipReadResponse resp = (CipReadResponse) p;
String fieldName = readRequest.getFieldNames().iterator().next();
EipField field = (EipField) readRequest.getField(fieldName);
PlcResponseCode code = decodeResponseCode(resp.getStatus());
PlcValue plcValue = null;
CIPDataTypeCode type = resp.getDataType();
ByteBuf data = Unpooled.wrappedBuffer(resp.getData());
if (code == PlcResponseCode.OK) {
plcValue = parsePlcValue(field, data, type);
}
ResponseItem<PlcValue> result = new ResponseItem<>(code, plcValue);
values.put(fieldName, result);
}
//Multiple response
else if (p instanceof MultipleServiceResponse) {
MultipleServiceResponse responses = (MultipleServiceResponse) p;
int nb = responses.getServiceNb();
List<CipService> arr = new ArrayList<>(nb);
ReadBufferByteBased read = new ReadBufferByteBased(responses.getServicesData(), org.apache.plc4x.java.spi.generation.ByteOrder.LITTLE_ENDIAN);
int total = read.getTotalBytes();
for (int i = 0; i < nb; i++) {
int length = 0;
int offset = responses.getOffsets().get(i) - responses.getOffsets().get(0); //Substract first offset as we only have the service in the buffer (not servicesNb and offsets)
if (i == nb - 1) {
length = total - offset; //Get the rest if last
} else {
length = responses.getOffsets().get(i + 1) - offset - responses.getOffsets().get(0); //Calculate length with offsets (substracting first offset)
}
ReadBuffer serviceBuf = new ReadBufferByteBased(read.getBytes(offset, offset + length), org.apache.plc4x.java.spi.generation.ByteOrder.LITTLE_ENDIAN);
CipService service = null;
try {
service = CipService.staticParse(read, length);
arr.add(service);
} catch (ParseException e) {
throw new PlcRuntimeException(e);
}
}
Services services = new Services(nb, responses.getOffsets(), arr, -1);
Iterator<String> it = readRequest.getFieldNames().iterator();
for (int i = 0; i < nb && it.hasNext(); i++) {
String fieldName = it.next();
EipField field = (EipField) readRequest.getField(fieldName);
PlcValue plcValue = null;
if (services.getServices().get(i) instanceof CipReadResponse) {
CipReadResponse readResponse = (CipReadResponse) services.getServices().get(i);
PlcResponseCode code;
if (readResponse.getStatus() == 0) {
code = PlcResponseCode.OK;
} else {
code = PlcResponseCode.INTERNAL_ERROR;
}
CIPDataTypeCode type = readResponse.getDataType();
ByteBuf data = Unpooled.wrappedBuffer(readResponse.getData());
if (code == PlcResponseCode.OK) {
plcValue = parsePlcValue(field, data, type);
}
ResponseItem<PlcValue> result = new ResponseItem<>(code, plcValue);
values.put(fieldName, result);
}
}
}
return new DefaultPlcReadResponse(readRequest, values);
}
private PlcValue parsePlcValue(EipField field, ByteBuf data, CIPDataTypeCode type) {
final int STRING_LEN_OFFSET = 2, STRING_DATA_OFFSET = 6;
int nb = field.getElementNb();
if (nb > 1) {
int index = 0;
List<PlcValue> list = new ArrayList<>();
for (int i = 0; i < nb; i++) {
switch (type) {
case DINT:
list.add(new PlcDINT(Integer.reverseBytes(data.getInt(index))));
index += type.getSize();
break;
case INT:
list.add(new PlcINT(Integer.reverseBytes(data.getInt(index))));
index += type.getSize();
break;
case SINT:
list.add(new PlcSINT(Integer.reverseBytes(data.getInt(index))));
index += type.getSize();
break;
case REAL:
list.add(new PlcLREAL(swap(data.getFloat(index))));
index += type.getSize();
break;
case LINT:
list.add(new PlcLINT(Long.reverseBytes(data.getLong(index))));
index += type.getSize();
break;
case BOOL:
list.add(new PlcBOOL(data.getBoolean(index)));
index += type.getSize();
break;
case STRUCTURED: {
Short structuredType = Short.reverseBytes(data.getShort(0));
Short structuredLen = Short.reverseBytes(data.getShort(STRING_LEN_OFFSET));
if (structuredType == CIPStructTypeCode.STRING.getValue()) {
// Length offset is 2, data offset is 6
list.add(new PlcSTRING(StandardCharsets
.UTF_8.decode(data.nioBuffer(STRING_DATA_OFFSET, structuredLen)).toString()));
index += type.getSize();
}
else {
// This is a different type of STRUCTURED data
// TODO: return as type STRUCT with structuredType to let user
// apps/progs handle it.
}
}
default:
return null;
}
}
return new PlcList(list);
} else {
switch (type) {
case SINT:
return new PlcSINT(data.getByte(0));
case INT:
return new PlcINT(Short.reverseBytes(data.getShort(0)));
case DINT:
return new PlcDINT(Integer.reverseBytes(data.getInt(0)));
case LINT:
return new PlcLINT(Long.reverseBytes(data.getLong(0)));
case REAL:
return new PlcREAL(swap(data.getFloat(0)));
case BOOL:
return new PlcBOOL(data.getBoolean(0));
case STRING36:
case STRING:
case STRUCTURED: {
Short structuredType = Short.reverseBytes(data.getShort(0));
Short structuredLen = Short.reverseBytes(data.getShort(STRING_LEN_OFFSET));
if (structuredType == CIPStructTypeCode.STRING.getValue()) {
// Length offset is 2, data offset is 6
return new PlcSTRING(StandardCharsets
.UTF_8.decode(data.nioBuffer(STRING_DATA_OFFSET, structuredLen)).toString());
}
else {
// This is a different type of STRUCTURED data
}
}
default:
return null;
}
}
}
public float swap(float value) {
int bytes = Float.floatToIntBits(value);
int b1 = (bytes) & 0xff;
int b2 = (bytes >> 8) & 0xff;
int b3 = (bytes >> 16) & 0xff;
int b4 = (bytes >> 24) & 0xff;
return Float.intBitsToFloat(b1 << 24 | b2 << 16 | b3 << 8 | b4);
}
@Override
public CompletableFuture<PlcWriteResponse> write(PlcWriteRequest writeRequest) {
CompletableFuture<PlcWriteResponse> future = new CompletableFuture<>();
DefaultPlcWriteRequest request = (DefaultPlcWriteRequest) writeRequest;
List<CipWriteRequest> items = new ArrayList<>(writeRequest.getNumberOfFields());
for (String fieldName : request.getFieldNames()) {
final EipField field = (EipField) request.getField(fieldName);
final PlcValue value = request.getPlcValue(fieldName);
String tag = field.getTag();
int elements = 1;
if (field.getElementNb() > 1) {
elements = field.getElementNb();
}
//We need the size of the request in words (0x91, tagLength, ... tag + possible pad)
// Taking half to get word size
boolean isArray = false;
boolean isStruct = false;
String tagIsolated = tag;
if (tag.contains("[")) {
isArray = true;
tagIsolated = tag.substring(0, tag.indexOf("["));
}
if (tag.contains(".")) {
isStruct = true;
tagIsolated = tagIsolated.replace(".", "");
}
int dataLength = (tagIsolated.length() + 2 + ((tagIsolated.length() % 2) * 2) + (isArray ? 2 : 0) + (isStruct ? 2 : 0));
byte requestPathSize = (byte) (dataLength / 2);
byte[] data = encodeValue(value, field.getType(), (short) elements);
CipWriteRequest writeReq = new CipWriteRequest(requestPathSize, toAnsi(tag), field.getType(), elements, data, -1);
items.add(writeReq);
}
RequestTransactionManager.RequestTransaction transaction = tm.startRequest();
if (items.size() == 1) {
tm.startRequest();
CipRRData rrdata = new CipRRData(sessionHandle, 0L, senderContext, 0L,
new CipExchange(
new CipUnconnectedRequest(
items.get(0), (byte) configuration.getBackplane(), (byte) configuration.getSlot(), -1
),
-1
),
-1
);
transaction.submit(() -> context.sendRequest(rrdata)
.expectResponse(EipPacket.class, REQUEST_TIMEOUT)
.onTimeout(future::completeExceptionally)
.onError((p, e) -> future.completeExceptionally(e))
.check(p -> p instanceof CipRRData).unwrap(p -> (CipRRData) p)
.check(p -> p.getSessionHandle() == sessionHandle)
//.check(p -> p.getSenderContext() == senderContext)
.check(p -> p.getExchange().getService() instanceof CipWriteResponse)
.unwrap(p -> (CipWriteResponse) p.getExchange().getService())
.handle(p -> {
future.complete((PlcWriteResponse) decodeWriteResponse(p, writeRequest));
transaction.endRequest();
})
);
} else {
tm.startRequest();
short nb = (short) items.size();
List<Integer> offsets = new ArrayList<>(nb);
int offset = 2 + nb * 2;
for (int i = 0; i < nb; i++) {
offsets.add(offset);
offset += items.get(i).getLengthInBytes();
}
List<CipService> serviceArr = new ArrayList<>(nb);
for (int i = 0; i < nb; i++) {
serviceArr.add(items.get(i));
}
Services data = new Services(nb, offsets, serviceArr, -1);
//Encapsulate the data
CipRRData pkt = new CipRRData(sessionHandle, 0L, emptySenderContext, 0L,
new CipExchange(
new CipUnconnectedRequest(
new MultipleServiceRequest(data, -1),
(byte) configuration.getBackplane(),
(byte) configuration.getSlot(),
-1
),
-1
),
-1
);
transaction.submit(() -> context.sendRequest(pkt)
.expectResponse(EipPacket.class, REQUEST_TIMEOUT)
.onTimeout(future::completeExceptionally)
.onError((p, e) -> future.completeExceptionally(e))
.check(p -> p instanceof CipRRData)
.check(p -> p.getSessionHandle() == sessionHandle)
//.check(p -> p.getSenderContext() == senderContext)
.unwrap(p -> (CipRRData) p)
.unwrap(p -> p.getExchange().getService()).check(p -> p instanceof MultipleServiceResponse)
.unwrap(p -> (MultipleServiceResponse) p)
.check(p -> p.getServiceNb() == nb)
.handle(p -> {
future.complete((PlcWriteResponse) decodeWriteResponse(p, writeRequest));
// Finish the request-transaction.
transaction.endRequest();
}));
}
return future;
}
private PlcResponse decodeWriteResponse(CipService p, PlcWriteRequest writeRequest) {
Map<String, PlcResponseCode> responses = new HashMap<>();
if (p instanceof CipWriteResponse) {
CipWriteResponse resp = (CipWriteResponse) p;
String fieldName = writeRequest.getFieldNames().iterator().next();
EipField field = (EipField) writeRequest.getField(fieldName);
responses.put(fieldName, decodeResponseCode(resp.getStatus()));
return new DefaultPlcWriteResponse(writeRequest, responses);
} else if (p instanceof MultipleServiceResponse) {
MultipleServiceResponse resp = (MultipleServiceResponse) p;
int nb = resp.getServiceNb();
List<CipService> arr = new ArrayList<>(nb);
ReadBufferByteBased read = new ReadBufferByteBased(resp.getServicesData());
int total = read.getTotalBytes();
for (int i = 0; i < nb; i++) {
int length = 0;
int offset = resp.getOffsets().get(i);
if (offset == nb - 1) {
length = total - offset; //Get the rest if last
} else {
length = resp.getOffsets().get(i + 1) - offset; //Calculate length with offsets
}
ReadBuffer serviceBuf = new ReadBufferByteBased(read.getBytes(offset, length), org.apache.plc4x.java.spi.generation.ByteOrder.LITTLE_ENDIAN);
CipService service = null;
try {
service = CipService.staticParse(read, length);
arr.add(service);
} catch (ParseException e) {
throw new PlcRuntimeException(e);
}
}
Services services = new Services(nb, resp.getOffsets(), arr, -1);
Iterator<String> it = writeRequest.getFieldNames().iterator();
for (int i = 0; i < nb && it.hasNext(); i++) {
String fieldName = it.next();
EipField field = (EipField) writeRequest.getField(fieldName);
PlcValue plcValue = null;
if (services.getServices().get(i) instanceof CipWriteResponse) {
CipWriteResponse writeResponse = (CipWriteResponse) services.getServices().get(i);
PlcResponseCode code = decodeResponseCode(writeResponse.getStatus());
responses.put(fieldName, code);
}
}
return new DefaultPlcWriteResponse(writeRequest, responses);
}
return null;
}
private byte[] encodeValue(PlcValue value, CIPDataTypeCode type, short elements) {
//ByteBuffer buffer = ByteBuffer.allocate(4+type.getSize()).order(ByteOrder.LITTLE_ENDIAN);
ByteBuffer buffer = ByteBuffer.allocate(type.getSize()).order(ByteOrder.LITTLE_ENDIAN);
switch (type) {
case BOOL:
buffer.put(value.getByte());
break;
case SINT:
buffer.put(value.getByte());
break;
case INT:
buffer.putShort(value.getShort());
break;
case DINT:
buffer.putInt(value.getInteger());
break;
case REAL:
buffer.putDouble(value.getDouble());
break;
case LINT:
buffer.putLong(value.getLong());
break;
case STRING:
case STRING36:
buffer.putInt(value.getString().length());
buffer.put(value.getString().getBytes(), 0, value.getString().length());
break;
case STRUCTURED:
// Need to handle
break;
default:
break;
}
return buffer.array();
}
private PlcResponseCode decodeResponseCode(int status) {
//TODO other status
switch (status) {
case 0:
return PlcResponseCode.OK;
default:
return PlcResponseCode.INTERNAL_ERROR;
}
}
@Override
public void close(ConversationContext<EipPacket> context) {
logger.debug("Sending UnregisterSession EIP Pakcet");
context.sendRequest(new EipDisconnectRequest(sessionHandle, 0L, emptySenderContext, 0L)); //Unregister gets no response
logger.debug("Unregistred Session {}", sessionHandle);
}
}
↑ V6013 Numbers 'structuredType' and 'CIPStructTypeCode.STRING.getValue()' are compared by reference. Possibly an equality comparison was intended.
↑ V6022 Parameter 'elements' is not used inside method body.
↑ V6009 The 'substring' function could receive the '-1' value while non-negative value is expected. Inspect argument: 2.
↑ V6021 The value is assigned to the 'length' variable but is not used.
↑ V6067 Two or more case-branches perform the same actions.
↑ V6021 The value is assigned to the 'index' variable but is not used.
↑ V6021 Variable 'field' is not used.
↑ V6021 The value is assigned to the 'length' variable but is not used.
↑ V6013 Numbers 'structuredType' and 'CIPStructTypeCode.STRING.getValue()' are compared by reference. Possibly an equality comparison was intended.