/*
|
* Copyright 2011-17 Fraunhofer ISE, energy & meteo Systems GmbH and other contributors
|
*
|
* Licensed 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.openmuc.openiec61850;
|
|
import java.io.ByteArrayInputStream;
|
import java.io.IOException;
|
import java.net.InetAddress;
|
import java.nio.ByteBuffer;
|
import java.util.ArrayList;
|
import java.util.Collection;
|
import java.util.Date;
|
import java.util.Iterator;
|
import java.util.LinkedList;
|
import java.util.List;
|
import java.util.concurrent.BlockingQueue;
|
import java.util.concurrent.LinkedBlockingQueue;
|
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeoutException;
|
|
import org.openmuc.jasn1.ber.ReverseByteArrayOutputStream;
|
import org.openmuc.jasn1.ber.types.BerBoolean;
|
import org.openmuc.jasn1.ber.types.BerInteger;
|
import org.openmuc.jasn1.ber.types.BerNull;
|
import org.openmuc.jasn1.ber.types.string.BerVisibleString;
|
import org.openmuc.josistack.AcseAssociation;
|
import org.openmuc.josistack.ByteBufferInputStream;
|
import org.openmuc.josistack.ClientAcseSap;
|
import org.openmuc.josistack.DecodingException;
|
import org.openmuc.openiec61850.internal.mms.asn1.AccessResult;
|
import org.openmuc.openiec61850.internal.mms.asn1.ConfirmedRequestPDU;
|
import org.openmuc.openiec61850.internal.mms.asn1.ConfirmedResponsePDU;
|
import org.openmuc.openiec61850.internal.mms.asn1.ConfirmedServiceRequest;
|
import org.openmuc.openiec61850.internal.mms.asn1.ConfirmedServiceResponse;
|
import org.openmuc.openiec61850.internal.mms.asn1.Data;
|
import org.openmuc.openiec61850.internal.mms.asn1.DefineNamedVariableListRequest;
|
import org.openmuc.openiec61850.internal.mms.asn1.DeleteNamedVariableListRequest;
|
import org.openmuc.openiec61850.internal.mms.asn1.DeleteNamedVariableListRequest.ListOfVariableListName;
|
import org.openmuc.openiec61850.internal.mms.asn1.DeleteNamedVariableListResponse;
|
import org.openmuc.openiec61850.internal.mms.asn1.GetNameListRequest;
|
import org.openmuc.openiec61850.internal.mms.asn1.GetNameListRequest.ObjectScope;
|
import org.openmuc.openiec61850.internal.mms.asn1.GetNameListResponse;
|
import org.openmuc.openiec61850.internal.mms.asn1.GetNamedVariableListAttributesRequest;
|
import org.openmuc.openiec61850.internal.mms.asn1.GetNamedVariableListAttributesResponse;
|
import org.openmuc.openiec61850.internal.mms.asn1.GetVariableAccessAttributesRequest;
|
import org.openmuc.openiec61850.internal.mms.asn1.Identifier;
|
import org.openmuc.openiec61850.internal.mms.asn1.InitiateRequestPDU;
|
import org.openmuc.openiec61850.internal.mms.asn1.InitiateResponsePDU;
|
import org.openmuc.openiec61850.internal.mms.asn1.Integer16;
|
import org.openmuc.openiec61850.internal.mms.asn1.Integer32;
|
import org.openmuc.openiec61850.internal.mms.asn1.Integer8;
|
import org.openmuc.openiec61850.internal.mms.asn1.MMSpdu;
|
import org.openmuc.openiec61850.internal.mms.asn1.ObjectClass;
|
import org.openmuc.openiec61850.internal.mms.asn1.ObjectName;
|
import org.openmuc.openiec61850.internal.mms.asn1.ParameterSupportOptions;
|
import org.openmuc.openiec61850.internal.mms.asn1.ReadRequest;
|
import org.openmuc.openiec61850.internal.mms.asn1.ReadResponse;
|
import org.openmuc.openiec61850.internal.mms.asn1.RejectPDU.RejectReason;
|
import org.openmuc.openiec61850.internal.mms.asn1.ServiceError.ErrorClass;
|
import org.openmuc.openiec61850.internal.mms.asn1.ServiceSupportOptions;
|
import org.openmuc.openiec61850.internal.mms.asn1.UnconfirmedPDU;
|
import org.openmuc.openiec61850.internal.mms.asn1.UnconfirmedService;
|
import org.openmuc.openiec61850.internal.mms.asn1.Unsigned32;
|
import org.openmuc.openiec61850.internal.mms.asn1.VariableAccessSpecification;
|
import org.openmuc.openiec61850.internal.mms.asn1.VariableDefs;
|
import org.openmuc.openiec61850.internal.mms.asn1.WriteRequest;
|
import org.openmuc.openiec61850.internal.mms.asn1.WriteRequest.ListOfData;
|
import org.openmuc.openiec61850.internal.mms.asn1.WriteResponse;
|
|
/**
|
* Represents an association/connection to an IEC 61850 MMS server. An instance of <code>ClientAssociation</code> is
|
* obtained using <code>ClientSap</code>. An association object can be used to execute the IEC 61850 ACSI services. Note
|
* that not all ACSI services have a corresponding function in this API. For example all GetDirectory and GetDefinition
|
* services are covered by <code>retrieveModel()</code>. The control services can be executed by using getDataValues and
|
* setDataValues on the control objects in the data model.
|
*
|
*/
|
public final class ClientAssociation {
|
|
private static final Integer16 version = new Integer16(new byte[] { (byte) 0x01, (byte) 0x01 });
|
private static final ParameterSupportOptions proposedParameterCbbBitString = new ParameterSupportOptions(
|
new byte[] { 0x03, 0x05, (byte) 0xf1, 0x00 });
|
|
private AcseAssociation acseAssociation = null;
|
private final ClientReceiver clientReceiver;
|
|
private final BlockingQueue<MMSpdu> incomingResponses = new LinkedBlockingQueue<>();
|
|
private final ReverseByteArrayOutputStream reverseOStream = new ReverseByteArrayOutputStream(500, true);
|
|
ServerModel serverModel;
|
|
private int responseTimeout;
|
|
private int invokeId = 0;
|
|
private int negotiatedMaxPduSize;
|
private ClientEventListener reportListener = null;
|
|
private boolean closed = false;
|
|
final class ClientReceiver extends Thread {
|
|
private Integer expectedResponseId;
|
private final ByteBuffer pduBuffer;
|
|
private IOException lastIOException = null;
|
|
public ClientReceiver(int maxMmsPduSize) {
|
pduBuffer = ByteBuffer.allocate(maxMmsPduSize + 400);
|
}
|
|
@Override
|
public void run() {
|
try {
|
while (true) {
|
|
pduBuffer.clear();
|
byte[] buffer;
|
try {
|
buffer = acseAssociation.receive(pduBuffer);
|
} catch (TimeoutException e) {
|
// Illegal state: A timeout exception was thrown.
|
throw new IllegalStateException();
|
} catch (DecodingException e) {
|
// Error decoding the OSI headers of the received packet
|
continue;
|
}
|
|
MMSpdu decodedResponsePdu = new MMSpdu();
|
try {
|
decodedResponsePdu.decode(new ByteArrayInputStream(buffer), null);
|
} catch (IOException e) {
|
// Error decoding the received MMS PDU
|
continue;
|
}
|
|
if (decodedResponsePdu.getUnconfirmedPDU() != null) {
|
if (decodedResponsePdu.getUnconfirmedPDU()
|
.getService()
|
.getInformationReport()
|
.getVariableAccessSpecification()
|
.getListOfVariable() != null) {
|
// Discarding LastApplError Report
|
}
|
else {
|
if (reportListener != null) {
|
final Report report = processReport(decodedResponsePdu);
|
|
Thread t1 = new Thread(new Runnable() {
|
@Override
|
public void run() {
|
reportListener.newReport(report);
|
}
|
});
|
t1.start();
|
}
|
else {
|
// discarding report because no ReportListener was registered.
|
}
|
}
|
}
|
else if (decodedResponsePdu.getRejectPDU() != null) {
|
synchronized (incomingResponses) {
|
if (expectedResponseId == null) {
|
// Discarding Reject MMS PDU because no listener for request was found.
|
continue;
|
}
|
else if (decodedResponsePdu.getRejectPDU().getOriginalInvokeID().value
|
.intValue() != expectedResponseId) {
|
// Discarding Reject MMS PDU because no listener with fitting invokeID was found.
|
continue;
|
}
|
else {
|
try {
|
incomingResponses.put(decodedResponsePdu);
|
} catch (InterruptedException e) {
|
}
|
}
|
}
|
}
|
else if (decodedResponsePdu.getConfirmedErrorPDU() != null) {
|
synchronized (incomingResponses) {
|
if (expectedResponseId == null) {
|
// Discarding ConfirmedError MMS PDU because no listener for request was found.
|
continue;
|
}
|
else if (decodedResponsePdu.getConfirmedErrorPDU().getInvokeID().value
|
.intValue() != expectedResponseId) {
|
// Discarding ConfirmedError MMS PDU because no listener with fitting invokeID was
|
// found.
|
continue;
|
}
|
else {
|
try {
|
incomingResponses.put(decodedResponsePdu);
|
} catch (InterruptedException e) {
|
}
|
}
|
}
|
}
|
else {
|
synchronized (incomingResponses) {
|
if (expectedResponseId == null) {
|
// Discarding ConfirmedResponse MMS PDU because no listener for request was found.
|
continue;
|
}
|
else if (decodedResponsePdu.getConfirmedResponsePDU().getInvokeID().value
|
.intValue() != expectedResponseId) {
|
// Discarding ConfirmedResponse MMS PDU because no listener with fitting invokeID was
|
// found.
|
continue;
|
}
|
else {
|
try {
|
incomingResponses.put(decodedResponsePdu);
|
} catch (InterruptedException e) {
|
}
|
}
|
}
|
|
}
|
}
|
} catch (IOException e) {
|
close(e);
|
} catch (Exception e) {
|
close(new IOException("unexpected exception while receiving", e));
|
}
|
}
|
|
public void setResponseExpected(int invokeId) {
|
expectedResponseId = invokeId;
|
}
|
|
private void disconnect() {
|
synchronized (this) {
|
if (closed == false) {
|
closed = true;
|
acseAssociation.disconnect();
|
lastIOException = new IOException("Connection disconnected by client");
|
if (reportListener != null) {
|
Thread t1 = new Thread(new Runnable() {
|
@Override
|
public void run() {
|
reportListener.associationClosed(lastIOException);
|
}
|
});
|
t1.start();
|
}
|
|
MMSpdu mmsPdu = new MMSpdu();
|
mmsPdu.setConfirmedRequestPDU(new ConfirmedRequestPDU());
|
try {
|
incomingResponses.put(mmsPdu);
|
} catch (InterruptedException e1) {
|
}
|
}
|
}
|
}
|
|
private void close(IOException e) {
|
synchronized (this) {
|
if (closed == false) {
|
closed = true;
|
acseAssociation.close();
|
lastIOException = e;
|
Thread t1 = new Thread(new Runnable() {
|
@Override
|
public void run() {
|
reportListener.associationClosed(lastIOException);
|
}
|
});
|
t1.start();
|
|
MMSpdu mmsPdu = new MMSpdu();
|
mmsPdu.setConfirmedRequestPDU(new ConfirmedRequestPDU());
|
try {
|
incomingResponses.put(mmsPdu);
|
} catch (InterruptedException e1) {
|
}
|
}
|
}
|
}
|
|
IOException getLastIOException() {
|
return lastIOException;
|
}
|
|
MMSpdu removeExpectedResponse() {
|
synchronized (incomingResponses) {
|
expectedResponseId = null;
|
return incomingResponses.poll();
|
}
|
}
|
|
}
|
|
ClientAssociation(InetAddress address, int port, InetAddress localAddr, int localPort,
|
String authenticationParameter, ClientAcseSap acseSap, int proposedMaxMmsPduSize,
|
int proposedMaxServOutstandingCalling, int proposedMaxServOutstandingCalled,
|
int proposedDataStructureNestingLevel, byte[] servicesSupportedCalling, int responseTimeout,
|
int messageFragmentTimeout, ClientEventListener reportListener) throws IOException {
|
|
this.responseTimeout = responseTimeout;
|
|
acseSap.tSap.setMessageFragmentTimeout(messageFragmentTimeout);
|
acseSap.tSap.setMessageTimeout(responseTimeout);
|
|
negotiatedMaxPduSize = proposedMaxMmsPduSize;
|
|
this.reportListener = reportListener;
|
|
associate(address, port, localAddr, localPort, authenticationParameter, acseSap, proposedMaxMmsPduSize,
|
proposedMaxServOutstandingCalling, proposedMaxServOutstandingCalled, proposedDataStructureNestingLevel,
|
servicesSupportedCalling);
|
|
acseAssociation.setMessageTimeout(0);
|
|
clientReceiver = new ClientReceiver(negotiatedMaxPduSize);
|
clientReceiver.start();
|
}
|
|
/**
|
* Sets the response timeout. The response timeout is used whenever a request is sent to the server. The client will
|
* wait for this amount of time for the server's response before throwing a ServiceError.TIMEOUT. Responses received
|
* after the timeout will be automatically discarded.
|
*
|
* @param timeout
|
* the response timeout in milliseconds.
|
*/
|
public void setResponseTimeout(int timeout) {
|
responseTimeout = timeout;
|
}
|
|
/**
|
* Gets the response timeout. The response timeout is used whenever a request is sent to the server. The client will
|
* wait for this amount of time for the server's response before throwing a ServiceError.TIMEOUT. Responses received
|
* after the timeout will be automatically discarded.
|
*
|
* @return the response timeout in milliseconds.
|
*/
|
public int getResponseTimeout() {
|
return responseTimeout;
|
}
|
|
private int getInvokeId() {
|
invokeId = (invokeId + 1) % 2147483647;
|
return invokeId;
|
}
|
|
private static ServiceError mmsDataAccessErrorToServiceError(BerInteger dataAccessError) {
|
|
switch (dataAccessError.value.intValue()) {
|
case 1:
|
return new ServiceError(ServiceError.FAILED_DUE_TO_SERVER_CONSTRAINT,
|
"MMS DataAccessError: hardware-fault");
|
case 2:
|
return new ServiceError(ServiceError.INSTANCE_LOCKED_BY_OTHER_CLIENT,
|
"MMS DataAccessError: temporarily-unavailable");
|
case 3:
|
return new ServiceError(ServiceError.ACCESS_VIOLATION, "MMS DataAccessError: object-access-denied");
|
case 5:
|
return new ServiceError(ServiceError.PARAMETER_VALUE_INCONSISTENT, "MMS DataAccessError: invalid-address");
|
case 7:
|
return new ServiceError(ServiceError.TYPE_CONFLICT, "MMS DataAccessError: type-inconsistent");
|
case 10:
|
return new ServiceError(ServiceError.INSTANCE_NOT_AVAILABLE, "MMS DataAccessError: object-non-existent");
|
case 11:
|
return new ServiceError(ServiceError.PARAMETER_VALUE_INCONSISTENT,
|
"MMS DataAccessError: object-value-invalid");
|
default:
|
return new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
"MMS DataAccessError: " + dataAccessError.value);
|
}
|
|
}
|
|
private static void testForErrorResponse(MMSpdu mmsResponsePdu) throws ServiceError {
|
if (mmsResponsePdu.getConfirmedErrorPDU() == null) {
|
return;
|
}
|
|
ErrorClass errClass = mmsResponsePdu.getConfirmedErrorPDU().getServiceError().getErrorClass();
|
if (errClass != null) {
|
if (errClass.getAccess() != null) {
|
if (errClass.getAccess().value.intValue() == 3) {
|
throw new ServiceError(ServiceError.ACCESS_VIOLATION,
|
"MMS confirmed error: class: \"access\", error code: \"object-access-denied\"");
|
}
|
else if (errClass.getAccess().value.intValue() == 2) {
|
|
throw new ServiceError(ServiceError.INSTANCE_NOT_AVAILABLE,
|
"MMS confirmed error: class: \"access\", error code: \"object-non-existent\"");
|
}
|
}
|
}
|
|
if (mmsResponsePdu.getConfirmedErrorPDU().getServiceError().getAdditionalDescription() != null) {
|
throw new ServiceError(ServiceError.UNKNOWN, "MMS confirmed error. Description: "
|
+ mmsResponsePdu.getConfirmedErrorPDU().getServiceError().getAdditionalDescription().toString());
|
}
|
throw new ServiceError(ServiceError.UNKNOWN, "MMS confirmed error.");
|
}
|
|
private static void testForRejectResponse(MMSpdu mmsResponsePdu) throws ServiceError {
|
if (mmsResponsePdu.getRejectPDU() == null) {
|
return;
|
}
|
|
RejectReason rejectReason = mmsResponsePdu.getRejectPDU().getRejectReason();
|
if (rejectReason != null) {
|
if (rejectReason.getPduError() != null) {
|
if (rejectReason.getPduError().value.intValue() == 1) {
|
throw new ServiceError(ServiceError.PARAMETER_VALUE_INCONSISTENT,
|
"MMS reject: type: \"pdu-error\", reject code: \"invalid-pdu\"");
|
}
|
}
|
}
|
throw new ServiceError(ServiceError.UNKNOWN, "MMS confirmed error.");
|
}
|
|
private static void testForInitiateErrorResponse(MMSpdu mmsResponsePdu) throws ServiceError {
|
if (mmsResponsePdu.getInitiateErrorPDU() != null) {
|
|
ErrorClass errClass = mmsResponsePdu.getInitiateErrorPDU().getErrorClass();
|
if (errClass != null) {
|
if (errClass.getVmdState() != null) {
|
throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
"error class \"vmd_state\" with val: " + errClass.getVmdState().value);
|
}
|
if (errClass.getApplicationReference() != null) {
|
throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
"error class \"application_reference\" with val: "
|
+ errClass.getApplicationReference().value);
|
}
|
if (errClass.getDefinition() != null) {
|
throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
"error class \"definition\" with val: " + errClass.getDefinition().value);
|
}
|
if (errClass.getResource() != null) {
|
throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
"error class \"resource\" with val: " + errClass.getResource().value);
|
}
|
if (errClass.getService() != null) {
|
throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
"error class \"service\" with val: " + errClass.getService().value);
|
}
|
if (errClass.getServicePreempt() != null) {
|
throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
"error class \"service_preempt\" with val: " + errClass.getServicePreempt().value);
|
}
|
if (errClass.getTimeResolution() != null) {
|
throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
"error class \"time_resolution\" with val: " + errClass.getTimeResolution().value);
|
}
|
if (errClass.getAccess() != null) {
|
throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
"error class \"access\" with val: " + errClass.getAccess().value);
|
}
|
if (errClass.getInitiate() != null) {
|
throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
"error class \"initiate\" with val: " + errClass.getInitiate().value);
|
}
|
if (errClass.getConclude() != null) {
|
throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
"error class \"conclude\" with val: " + errClass.getConclude());
|
}
|
if (errClass.getCancel() != null) {
|
throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
"error class \"cancel\" with val: " + errClass.getCancel().value);
|
}
|
if (errClass.getFile() != null) {
|
throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
"error class \"file\" with val: " + errClass.getFile().value);
|
}
|
if (errClass.getOthers() != null) {
|
throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
"error class \"others\" with val: " + errClass.getOthers().value);
|
}
|
}
|
|
throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT, "unknown error class");
|
}
|
}
|
|
private ConfirmedServiceResponse encodeWriteReadDecode(ConfirmedServiceRequest serviceRequest)
|
throws ServiceError, IOException {
|
|
int currentInvokeId = getInvokeId();
|
|
ConfirmedRequestPDU confirmedRequestPdu = new ConfirmedRequestPDU();
|
confirmedRequestPdu.setInvokeID(new Unsigned32(currentInvokeId));
|
confirmedRequestPdu.setService(serviceRequest);
|
|
MMSpdu requestPdu = new MMSpdu();
|
requestPdu.setConfirmedRequestPDU(confirmedRequestPdu);
|
|
reverseOStream.reset();
|
|
try {
|
requestPdu.encode(reverseOStream);
|
} catch (Exception e) {
|
IOException e2 = new IOException("Error encoding MmsPdu.", e);
|
clientReceiver.close(e2);
|
throw e2;
|
}
|
|
clientReceiver.setResponseExpected(currentInvokeId);
|
try {
|
acseAssociation.send(reverseOStream.getByteBuffer());
|
} catch (IOException e) {
|
IOException e2 = new IOException("Error sending packet.", e);
|
clientReceiver.close(e2);
|
throw e2;
|
}
|
|
MMSpdu decodedResponsePdu = null;
|
|
try {
|
if (responseTimeout == 0) {
|
decodedResponsePdu = incomingResponses.take();
|
}
|
else {
|
decodedResponsePdu = incomingResponses.poll(responseTimeout, TimeUnit.MILLISECONDS);
|
}
|
} catch (InterruptedException e) {
|
}
|
|
if (decodedResponsePdu == null) {
|
decodedResponsePdu = clientReceiver.removeExpectedResponse();
|
if (decodedResponsePdu == null) {
|
throw new ServiceError(ServiceError.TIMEOUT);
|
}
|
}
|
|
if (decodedResponsePdu.getConfirmedRequestPDU() != null) {
|
incomingResponses.add(decodedResponsePdu);
|
throw clientReceiver.getLastIOException();
|
}
|
|
testForInitiateErrorResponse(decodedResponsePdu);
|
testForErrorResponse(decodedResponsePdu);
|
testForRejectResponse(decodedResponsePdu);
|
|
ConfirmedResponsePDU confirmedResponsePdu = decodedResponsePdu.getConfirmedResponsePDU();
|
if (confirmedResponsePdu == null) {
|
throw new IllegalStateException("Response PDU is not a confirmed response pdu");
|
}
|
|
return confirmedResponsePdu.getService();
|
|
}
|
|
private void associate(InetAddress address, int port, InetAddress localAddr, int localPort,
|
String authenticationParameter, ClientAcseSap acseSap, int proposedMaxPduSize,
|
int proposedMaxServOutstandingCalling, int proposedMaxServOutstandingCalled,
|
int proposedDataStructureNestingLevel, byte[] servicesSupportedCalling) throws IOException {
|
|
MMSpdu initiateRequestMMSpdu = constructInitRequestPdu(proposedMaxPduSize, proposedMaxServOutstandingCalling,
|
proposedMaxServOutstandingCalled, proposedDataStructureNestingLevel, servicesSupportedCalling);
|
|
ReverseByteArrayOutputStream reverseOStream = new ReverseByteArrayOutputStream(500, true);
|
initiateRequestMMSpdu.encode(reverseOStream);
|
|
try {
|
acseAssociation = acseSap.associate(address, port, localAddr, localPort, authenticationParameter,
|
reverseOStream.getByteBuffer());
|
|
ByteBuffer initResponse = acseAssociation.getAssociateResponseAPdu();
|
|
MMSpdu initiateResponseMmsPdu = new MMSpdu();
|
|
initiateResponseMmsPdu.decode(new ByteBufferInputStream(initResponse), null);
|
|
handleInitiateResponse(initiateResponseMmsPdu, proposedMaxPduSize, proposedMaxServOutstandingCalling,
|
proposedMaxServOutstandingCalled, proposedDataStructureNestingLevel);
|
} catch (IOException e) {
|
if (acseAssociation != null) {
|
acseAssociation.close();
|
}
|
throw e;
|
}
|
}
|
|
private static MMSpdu constructInitRequestPdu(int proposedMaxPduSize, int proposedMaxServOutstandingCalling,
|
int proposedMaxServOutstandingCalled, int proposedDataStructureNestingLevel,
|
byte[] servicesSupportedCalling) {
|
|
InitiateRequestPDU.InitRequestDetail initRequestDetail = new InitiateRequestPDU.InitRequestDetail();
|
initRequestDetail.setProposedVersionNumber(version);
|
initRequestDetail.setProposedParameterCBB(proposedParameterCbbBitString);
|
initRequestDetail.setServicesSupportedCalling(new ServiceSupportOptions(servicesSupportedCalling, 85));
|
|
InitiateRequestPDU initiateRequestPdu = new InitiateRequestPDU();
|
initiateRequestPdu.setLocalDetailCalling(new Integer32(proposedMaxPduSize));
|
initiateRequestPdu.setProposedMaxServOutstandingCalling(new Integer16(proposedMaxServOutstandingCalling));
|
initiateRequestPdu.setProposedMaxServOutstandingCalled(new Integer16(proposedMaxServOutstandingCalled));
|
initiateRequestPdu.setProposedDataStructureNestingLevel(new Integer8(proposedDataStructureNestingLevel));
|
initiateRequestPdu.setInitRequestDetail(initRequestDetail);
|
|
MMSpdu initiateRequestMMSpdu = new MMSpdu();
|
initiateRequestMMSpdu.setInitiateRequestPDU(initiateRequestPdu);
|
|
return initiateRequestMMSpdu;
|
}
|
|
private void handleInitiateResponse(MMSpdu responsePdu, int proposedMaxPduSize,
|
int proposedMaxServOutstandingCalling, int proposedMaxServOutstandingCalled,
|
int proposedDataStructureNestingLevel) throws IOException {
|
|
if (responsePdu.getInitiateErrorPDU() != null) {
|
throw new IOException("Got response error of class: " + responsePdu.getInitiateErrorPDU().getErrorClass());
|
}
|
|
if (responsePdu.getInitiateResponsePDU() == null) {
|
acseAssociation.disconnect();
|
throw new IOException("Error decoding InitiateResponse Pdu");
|
}
|
|
InitiateResponsePDU initiateResponsePdu = responsePdu.getInitiateResponsePDU();
|
|
if (initiateResponsePdu.getLocalDetailCalled() != null) {
|
negotiatedMaxPduSize = initiateResponsePdu.getLocalDetailCalled().intValue();
|
}
|
|
int negotiatedMaxServOutstandingCalling = initiateResponsePdu.getNegotiatedMaxServOutstandingCalling()
|
.intValue();
|
int negotiatedMaxServOutstandingCalled = initiateResponsePdu.getNegotiatedMaxServOutstandingCalled().intValue();
|
|
int negotiatedDataStructureNestingLevel;
|
if (initiateResponsePdu.getNegotiatedDataStructureNestingLevel() != null) {
|
negotiatedDataStructureNestingLevel = initiateResponsePdu.getNegotiatedDataStructureNestingLevel()
|
.intValue();
|
}
|
else {
|
negotiatedDataStructureNestingLevel = proposedDataStructureNestingLevel;
|
}
|
|
if (negotiatedMaxPduSize < ClientSap.MINIMUM_MMS_PDU_SIZE || negotiatedMaxPduSize > proposedMaxPduSize
|
|| negotiatedMaxServOutstandingCalling > proposedMaxServOutstandingCalling
|
|| negotiatedMaxServOutstandingCalling < 0
|
|| negotiatedMaxServOutstandingCalled > proposedMaxServOutstandingCalled
|
|| negotiatedMaxServOutstandingCalled < 0
|
|| negotiatedDataStructureNestingLevel > proposedDataStructureNestingLevel
|
|| negotiatedDataStructureNestingLevel < 0) {
|
acseAssociation.disconnect();
|
throw new IOException("Error negotiating parameters");
|
}
|
|
int version = initiateResponsePdu.getInitResponseDetail().getNegotiatedVersionNumber().intValue();
|
if (version != 1) {
|
throw new IOException("Unsupported version number was negotiated.");
|
}
|
|
byte[] servicesSupported = initiateResponsePdu.getInitResponseDetail().getServicesSupportedCalled().value;
|
if ((servicesSupported[0] & 0x40) != 0x40) {
|
throw new IOException("Obligatory services are not supported by the server.");
|
}
|
}
|
|
/**
|
* Parses the given SCL File and returns the server model that is described by it. This function can be used instead
|
* of <code>retrieveModel</code> in order to get the server model that is needed to call the other ACSI services.
|
*
|
* @param sclFilePath
|
* the path to the SCL file that is to be parsed.
|
* @return The ServerNode that is the root node of the complete server model.
|
* @throws SclParseException
|
* if any kind of fatal error occurs in the parsing process.
|
*/
|
public ServerModel getModelFromSclFile(String sclFilePath) throws SclParseException {
|
List<ServerSap> serverSaps = ServerSap.getSapsFromSclFile(sclFilePath);
|
if (serverSaps == null || serverSaps.size() == 0) {
|
throw new SclParseException("No AccessPoint found in SCL file.");
|
}
|
serverModel = serverSaps.get(0).serverModel;
|
return serverModel;
|
}
|
|
/**
|
* Triggers all GetDirectory and GetDefinition ACSI services needed to get the complete server model. Because in MMS
|
* SubDataObjects cannot be distinguished from Constructed Data Attributes they will always be represented as
|
* Constructed Data Attributes in the returned model.
|
*
|
* @return the ServerModel that is the root node of the complete server model.
|
* @throws ServiceError
|
* if a ServiceError occurs while calling any of the ASCI services.
|
* @throws IOException
|
* if a fatal association error occurs. The association object will be closed and can no longer be used
|
* after this exception is thrown.
|
*/
|
public ServerModel retrieveModel() throws ServiceError, IOException {
|
|
List<String> ldNames = retrieveLogicalDevices();
|
List<List<String>> lnNames = new ArrayList<>(ldNames.size());
|
//System.out.println("List<String> ldNames = retrieveLogicalDevices();");
|
for (int i = 0; i < ldNames.size(); i++) {
|
lnNames.add(retrieveLogicalNodeNames(ldNames.get(i)));
|
//System.out.println(ldNames.get(i));
|
}
|
List<LogicalDevice> lds = new ArrayList<>();
|
for (int i = 0; i < ldNames.size(); i++) {
|
List<LogicalNode> lns = new ArrayList<>();
|
for (int j = 0; j < lnNames.get(i).size(); j++) {
|
lns.add(retrieveDataDefinitions(new ObjectReference(ldNames.get(i) + "/" + lnNames.get(i).get(j))));
|
}
|
lds.add(new LogicalDevice(new ObjectReference(ldNames.get(i)), lns));
|
}
|
|
serverModel = new ServerModel(lds, null);
|
|
updateDataSets();
|
|
return serverModel;
|
}
|
|
private List<String> retrieveLogicalDevices() throws ServiceError, IOException {
|
ConfirmedServiceRequest serviceRequest = constructGetServerDirectoryRequest();
|
ConfirmedServiceResponse confirmedServiceResponse = encodeWriteReadDecode(serviceRequest);
|
return decodeGetServerDirectoryResponse(confirmedServiceResponse);
|
}
|
|
private ConfirmedServiceRequest constructGetServerDirectoryRequest() {
|
ObjectClass objectClass = new ObjectClass();
|
objectClass.setBasicObjectClass(new BerInteger(9));
|
|
GetNameListRequest.ObjectScope objectScope = new GetNameListRequest.ObjectScope();
|
objectScope.setVmdSpecific(new BerNull());
|
|
GetNameListRequest getNameListRequest = new GetNameListRequest();
|
getNameListRequest.setObjectClass(objectClass);
|
getNameListRequest.setObjectScope(objectScope);
|
|
ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
|
confirmedServiceRequest.setGetNameList(getNameListRequest);
|
|
return confirmedServiceRequest;
|
}
|
|
private List<String> decodeGetServerDirectoryResponse(ConfirmedServiceResponse confirmedServiceResponse)
|
throws ServiceError {
|
|
if (confirmedServiceResponse.getGetNameList() == null) {
|
throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
"Error decoding Get Server Directory Response Pdu");
|
}
|
|
List<Identifier> identifiers = confirmedServiceResponse.getGetNameList().getListOfIdentifier().getIdentifier();
|
ArrayList<String> objectRefs = new ArrayList<>(); // ObjectReference[identifiers.size()];
|
|
for (BerVisibleString identifier : identifiers) {
|
objectRefs.add(identifier.toString());
|
}
|
|
return objectRefs;
|
}
|
|
private List<String> retrieveLogicalNodeNames(String ld) throws ServiceError, IOException {
|
List<String> lns = new LinkedList<>();
|
String continueAfterRef = "";
|
do {
|
ConfirmedServiceRequest serviceRequest = constructGetDirectoryRequest(ld, continueAfterRef, true);
|
ConfirmedServiceResponse confirmedServiceResponse = encodeWriteReadDecode(serviceRequest);
|
continueAfterRef = decodeGetDirectoryResponse(confirmedServiceResponse, lns);
|
|
} while (continueAfterRef != "");
|
return lns;
|
}
|
|
private ConfirmedServiceRequest constructGetDirectoryRequest(String ldRef, String continueAfter,
|
boolean logicalDevice) {
|
|
ObjectClass objectClass = new ObjectClass();
|
|
if (logicalDevice) {
|
objectClass.setBasicObjectClass(new BerInteger(0));
|
}
|
else { // for data sets
|
objectClass.setBasicObjectClass(new BerInteger(2));
|
}
|
|
GetNameListRequest getNameListRequest = null;
|
|
ObjectScope objectScopeChoiceType = new ObjectScope();
|
objectScopeChoiceType.setDomainSpecific(new Identifier(ldRef.getBytes()));
|
|
getNameListRequest = new GetNameListRequest();
|
getNameListRequest.setObjectClass(objectClass);
|
getNameListRequest.setObjectScope(objectScopeChoiceType);
|
if (continueAfter != "") {
|
getNameListRequest.setContinueAfter(new Identifier(continueAfter.getBytes()));
|
}
|
|
ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
|
confirmedServiceRequest.setGetNameList(getNameListRequest);
|
return confirmedServiceRequest;
|
|
}
|
|
/**
|
* Decodes an MMS response which contains the structure of a LD and its LNs including names of DOs.
|
*/
|
private String decodeGetDirectoryResponse(ConfirmedServiceResponse confirmedServiceResponse, List<String> lns)
|
throws ServiceError {
|
|
if (confirmedServiceResponse.getGetNameList() == null) {
|
throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
"decodeGetLDDirectoryResponse: Error decoding server response");
|
}
|
|
GetNameListResponse getNameListResponse = confirmedServiceResponse.getGetNameList();
|
|
List<Identifier> identifiers = getNameListResponse.getListOfIdentifier().getIdentifier();
|
|
if (identifiers.size() == 0) {
|
throw new ServiceError(ServiceError.INSTANCE_NOT_AVAILABLE,
|
"decodeGetLDDirectoryResponse: Instance not available");
|
}
|
|
BerVisibleString identifier = null;
|
Iterator<Identifier> it = identifiers.iterator();
|
|
String idString;
|
|
while (it.hasNext()) {
|
identifier = it.next();
|
idString = identifier.toString();
|
|
if (idString.indexOf('$') == -1) {
|
lns.add(idString);
|
}
|
}
|
|
if (getNameListResponse.getMoreFollows() != null && getNameListResponse.getMoreFollows().value == false) {
|
return "";
|
}
|
else {
|
return identifier.toString();
|
}
|
}
|
|
private LogicalNode retrieveDataDefinitions(ObjectReference lnRef) throws ServiceError, IOException {
|
ConfirmedServiceRequest serviceRequest = constructGetDataDefinitionRequest(lnRef);
|
ConfirmedServiceResponse confirmedServiceResponse = encodeWriteReadDecode(serviceRequest);
|
return decodeGetDataDefinitionResponse(confirmedServiceResponse, lnRef);
|
}
|
|
private ConfirmedServiceRequest constructGetDataDefinitionRequest(ObjectReference lnRef) {
|
|
ObjectName.DomainSpecific domainSpec = new ObjectName.DomainSpecific();
|
domainSpec.setDomainID(new Identifier(lnRef.get(0).getBytes()));
|
domainSpec.setItemID(new Identifier(lnRef.get(1).getBytes()));
|
|
ObjectName objectName = new ObjectName();
|
objectName.setDomainSpecific(domainSpec);
|
|
GetVariableAccessAttributesRequest getVariableAccessAttributesRequest = new GetVariableAccessAttributesRequest();
|
getVariableAccessAttributesRequest.setName(objectName);
|
|
ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
|
confirmedServiceRequest.setGetVariableAccessAttributes(getVariableAccessAttributesRequest);
|
|
return confirmedServiceRequest;
|
}
|
|
private LogicalNode decodeGetDataDefinitionResponse(ConfirmedServiceResponse confirmedServiceResponse,
|
ObjectReference lnRef) throws ServiceError {
|
|
return DataDefinitionResParser.parseGetDataDefinitionResponse(confirmedServiceResponse, lnRef);
|
}
|
|
/**
|
* The implementation of the GetDataValues ACSI service. Will send an MMS read request for the given model node.
|
* After a successful return, the Basic Data Attributes of the passed model node will contain the values read. If
|
* one of the Basic Data Attributes cannot be read then none of the values will be read and a
|
* <code>ServiceError</code> will be thrown.
|
*
|
* @param modelNode
|
* the functionally constrained model node that is to be read.
|
* @throws ServiceError
|
* if a ServiceError is returned by the server.
|
* @throws IOException
|
* if a fatal association error occurs. The association object will be closed and can no longer be used
|
* after this exception is thrown.
|
*/
|
public void getDataValues(FcModelNode modelNode) throws ServiceError, IOException {
|
ConfirmedServiceRequest serviceRequest = constructGetDataValuesRequest(modelNode);
|
ConfirmedServiceResponse confirmedServiceResponse = encodeWriteReadDecode(serviceRequest);
|
decodeGetDataValuesResponse(confirmedServiceResponse, modelNode);
|
}
|
|
/**
|
* Will update all data inside the model except for control variables (those that have FC=CO). Control variables are
|
* not meant to be read. Update is done by calling getDataValues on the FCDOs below the Logical Nodes.
|
*
|
* @throws ServiceError
|
* if a ServiceError is returned by the server.
|
* @throws IOException
|
* if a fatal association error occurs. The association object will be closed and can no longer be used
|
* after this exception is thrown.
|
*/
|
public void getAllDataValues() throws ServiceError, IOException {
|
for (ModelNode logicalDevice : serverModel.getChildren()) {
|
for (ModelNode logicalNode : logicalDevice.getChildren()) {
|
for (ModelNode dataObject : logicalNode.getChildren()) {
|
FcModelNode fcdo = (FcModelNode) dataObject;
|
if (fcdo.getFc() != Fc.CO && fcdo.getFc() != Fc.SE) {
|
try {
|
getDataValues(fcdo);
|
} catch (ServiceError e) {
|
throw new ServiceError(e.getErrorCode(), "service error retrieving " + fcdo.getReference()
|
+ "[" + fcdo.getFc() + "]" + ", " + e.getMessage(), e);
|
}
|
}
|
}
|
}
|
}
|
}
|
|
private ConfirmedServiceRequest constructGetDataValuesRequest(FcModelNode modelNode) {
|
VariableAccessSpecification varAccessSpec = constructVariableAccessSpecification(modelNode);
|
|
ReadRequest readRequest = new ReadRequest();
|
readRequest.setVariableAccessSpecification(varAccessSpec);
|
|
ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
|
confirmedServiceRequest.setRead(readRequest);
|
|
return confirmedServiceRequest;
|
}
|
|
private void decodeGetDataValuesResponse(ConfirmedServiceResponse confirmedServiceResponse, ModelNode modelNode)
|
throws ServiceError {
|
|
if (confirmedServiceResponse.getRead() == null) {
|
throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
"Error decoding GetDataValuesReponsePdu");
|
}
|
|
List<AccessResult> listOfAccessResults = confirmedServiceResponse.getRead()
|
.getListOfAccessResult()
|
.getAccessResult();
|
|
if (listOfAccessResults.size() != 1) {
|
throw new ServiceError(ServiceError.PARAMETER_VALUE_INAPPROPRIATE, "Multiple results received.");
|
}
|
|
AccessResult accRes = listOfAccessResults.get(0);
|
|
if (accRes.getFailure() != null) {
|
throw mmsDataAccessErrorToServiceError(accRes.getFailure());
|
}
|
/*
|
BdaFloat32 b_32 = new BdaFloat32(modelNode.getReference(), Fc.SP, null, false, false);
|
b_32.setValue(accRes.getSuccess().getFloatingPoint().value);
|
System.out.println(modelNode.getReference().toString() + " = " + b_32.getFloat());
|
*/
|
modelNode.setValueFromMmsDataObj(accRes.getSuccess());
|
}
|
|
/**
|
* The implementation of the SetDataValues ACSI service. Will send an MMS write request with the values of all Basic
|
* Data Attributes of the given model node. Will simply return if all values have been successfully written. If one
|
* of the Basic Data Attributes could not be written then a <code>ServiceError</code> will be thrown. In this case
|
* it is not possible to find out which of several Basic Data Attributes could not be written.
|
*
|
* @param modelNode
|
* the functionally constrained model node that is to be written.
|
* @throws ServiceError
|
* if a ServiceError is returned by the server.
|
* @throws IOException
|
* if a fatal association error occurs. The association object will be closed and can no longer be used
|
* after this exception is thrown.
|
*/
|
public void setDataValues(FcModelNode modelNode) throws ServiceError, IOException {
|
ConfirmedServiceRequest serviceRequest = constructSetDataValuesRequest(modelNode);
|
ConfirmedServiceResponse confirmedServiceResponse = encodeWriteReadDecode(serviceRequest);
|
decodeSetDataValuesResponse(confirmedServiceResponse);
|
}
|
|
private ConfirmedServiceRequest constructSetDataValuesRequest(FcModelNode modelNode) throws ServiceError {
|
|
VariableAccessSpecification variableAccessSpecification = constructVariableAccessSpecification(modelNode);
|
|
ListOfData listOfData = new ListOfData();
|
List<Data> dataList = listOfData.getData();
|
dataList.add(modelNode.getMmsDataObj());
|
|
WriteRequest writeRequest = new WriteRequest();
|
writeRequest.setListOfData(listOfData);
|
writeRequest.setVariableAccessSpecification(variableAccessSpecification);
|
|
ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
|
confirmedServiceRequest.setWrite(writeRequest);
|
|
return confirmedServiceRequest;
|
}
|
|
private VariableAccessSpecification constructVariableAccessSpecification(FcModelNode modelNode) {
|
VariableDefs listOfVariable = new VariableDefs();
|
|
List<VariableDefs.SEQUENCE> variableDefsSeqOf = listOfVariable.getSEQUENCE();
|
variableDefsSeqOf.add(modelNode.getMmsVariableDef());
|
|
VariableAccessSpecification variableAccessSpecification = new VariableAccessSpecification();
|
variableAccessSpecification.setListOfVariable(listOfVariable);
|
|
return variableAccessSpecification;
|
}
|
|
private void decodeSetDataValuesResponse(ConfirmedServiceResponse confirmedServiceResponse) throws ServiceError {
|
|
WriteResponse writeResponse = confirmedServiceResponse.getWrite();
|
|
if (writeResponse == null) {
|
throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
"SetDataValuesResponse: improper response");
|
}
|
|
WriteResponse.CHOICE subChoice = writeResponse.getCHOICE().get(0);
|
|
if (subChoice.getFailure() != null) {
|
throw mmsDataAccessErrorToServiceError(subChoice.getFailure());
|
}
|
}
|
|
/**
|
* This function will get the definition of all persistent DataSets from the server and update the DataSets in the
|
* ServerModel that were returned by the retrieveModel() or getModelFromSclFile() functions. It will delete DataSets
|
* that have been deleted since the last update and add any new DataSets
|
*
|
* @throws ServiceError
|
* if a ServiceError is returned by the server.
|
* @throws IOException
|
* if a fatal association error occurs. The association object will be closed and can no longer be used
|
* after this exception is thrown.
|
*/
|
public void updateDataSets() throws ServiceError, IOException {
|
|
if (serverModel == null) {
|
throw new IllegalStateException(
|
"Before calling this function you have to get the ServerModel using the retrieveModel() function");
|
}
|
|
Collection<ModelNode> lds = serverModel.getChildren();
|
|
for (ModelNode ld : lds) {
|
ConfirmedServiceRequest serviceRequest = constructGetDirectoryRequest(ld.getName(), "", false);
|
ConfirmedServiceResponse confirmedServiceResponse = encodeWriteReadDecode(serviceRequest);
|
decodeAndRetrieveDsNamesAndDefinitions(confirmedServiceResponse, (LogicalDevice) ld);
|
}
|
}
|
|
private void decodeAndRetrieveDsNamesAndDefinitions(ConfirmedServiceResponse confirmedServiceResponse,
|
LogicalDevice ld) throws ServiceError, IOException {
|
|
if (confirmedServiceResponse.getGetNameList() == null) {
|
throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
"decodeGetDataSetResponse: Error decoding server response");
|
}
|
|
GetNameListResponse getNameListResponse = confirmedServiceResponse.getGetNameList();
|
|
List<Identifier> identifiers = getNameListResponse.getListOfIdentifier().getIdentifier();
|
|
if (identifiers.size() == 0) {
|
return;
|
}
|
|
for (Identifier identifier : identifiers) {
|
// TODO delete DataSets that no longer exist
|
getDataSetDirectory(identifier, ld);
|
}
|
|
if (getNameListResponse.getMoreFollows() != null && getNameListResponse.getMoreFollows().value == true) {
|
throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT);
|
}
|
}
|
|
private void getDataSetDirectory(Identifier dsId, LogicalDevice ld) throws ServiceError, IOException {
|
ConfirmedServiceRequest serviceRequest = constructGetDataSetDirectoryRequest(dsId, ld);
|
ConfirmedServiceResponse confirmedServiceResponse = encodeWriteReadDecode(serviceRequest);
|
decodeGetDataSetDirectoryResponse(confirmedServiceResponse, dsId, ld);
|
}
|
|
private ConfirmedServiceRequest constructGetDataSetDirectoryRequest(Identifier dsId, LogicalDevice ld)
|
throws ServiceError {
|
ObjectName.DomainSpecific domainSpecificObjectName = new ObjectName.DomainSpecific();
|
domainSpecificObjectName.setDomainID(new Identifier(ld.getName().getBytes()));
|
domainSpecificObjectName.setItemID(dsId);
|
|
GetNamedVariableListAttributesRequest dataSetObj = new GetNamedVariableListAttributesRequest();
|
dataSetObj.setDomainSpecific(domainSpecificObjectName);
|
|
ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
|
confirmedServiceRequest.setGetNamedVariableListAttributes(dataSetObj);
|
|
return confirmedServiceRequest;
|
}
|
|
private void decodeGetDataSetDirectoryResponse(ConfirmedServiceResponse confirmedServiceResponse,
|
BerVisibleString dsId, LogicalDevice ld) throws ServiceError {
|
|
if (confirmedServiceResponse.getGetNamedVariableListAttributes() == null) {
|
throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
"decodeGetDataSetDirectoryResponse: Error decoding server response");
|
}
|
|
GetNamedVariableListAttributesResponse getNamedVariableListAttResponse = confirmedServiceResponse
|
.getGetNamedVariableListAttributes();
|
boolean deletable = getNamedVariableListAttResponse.getMmsDeletable().value;
|
List<VariableDefs.SEQUENCE> variables = getNamedVariableListAttResponse.getListOfVariable().getSEQUENCE();
|
|
if (variables.size() == 0) {
|
throw new ServiceError(ServiceError.INSTANCE_NOT_AVAILABLE,
|
"decodeGetDataSetDirectoryResponse: Instance not available");
|
}
|
|
List<FcModelNode> dsMems = new ArrayList<>();
|
|
for (VariableDefs.SEQUENCE variableDef : variables) {
|
|
FcModelNode member;
|
// TODO remove this try catch statement once all possible FCs are
|
// supported
|
// it is only there so that Functional Constraints such as GS will
|
// be ignored and DataSet cotaining elements with these FCs are
|
// ignored and not created.
|
try {
|
member = serverModel.getNodeFromVariableDef(variableDef);
|
} catch (ServiceError e) {
|
return;
|
}
|
if (member == null) {
|
throw new ServiceError(ServiceError.INSTANCE_NOT_AVAILABLE,
|
"decodeGetDataSetDirectoryResponse: data set memeber does not exist, you might have to call retrieveModel first");
|
}
|
dsMems.add(member);
|
}
|
|
String dsObjRef = ld.getName() + "/" + dsId.toString().replace('$', '.');
|
|
DataSet dataSet = new DataSet(dsObjRef, dsMems, deletable);
|
|
if (ld.getChild(dsId.toString().substring(0, dsId.toString().indexOf('$'))) == null) {
|
throw new ServiceError(ServiceError.INSTANCE_NOT_AVAILABLE,
|
"decodeGetDataSetDirectoryResponse: LN for returned DataSet is not available");
|
}
|
|
DataSet existingDs = serverModel.getDataSet(dsObjRef);
|
if (existingDs == null) {
|
serverModel.addDataSet(dataSet);
|
}
|
else if (!existingDs.isDeletable()) {
|
return;
|
}
|
else {
|
serverModel.removeDataSet(dsObjRef.toString());
|
serverModel.addDataSet(dataSet);
|
}
|
|
}
|
|
/**
|
* The client should create the data set first and add it to either the non-persistent list or to the model. Then it
|
* should call this method for creation on the server side
|
*
|
* @param dataSet
|
* the data set to be created on the server side
|
* @throws ServiceError
|
* if a ServiceError is returned by the server.
|
* @throws IOException
|
* if a fatal IO error occurs. The association object will be closed and can no longer be used after
|
* this exception is thrown.
|
*/
|
public void createDataSet(DataSet dataSet) throws ServiceError, IOException {
|
ConfirmedServiceRequest serviceRequest = constructCreateDataSetRequest(dataSet);
|
encodeWriteReadDecode(serviceRequest);
|
handleCreateDataSetResponse(dataSet);
|
}
|
|
/**
|
* dsRef = either LD/LN.DataSetName (persistent) or @DataSetname (non-persistent) Names in dsMemberRef should be in
|
* the form: LD/LNName.DoName or LD/LNName.DoName.DaName
|
*/
|
private ConfirmedServiceRequest constructCreateDataSetRequest(DataSet dataSet) throws ServiceError {
|
|
VariableDefs listOfVariable = new VariableDefs();
|
|
List<VariableDefs.SEQUENCE> variableDefs = listOfVariable.getSEQUENCE();
|
for (FcModelNode dsMember : dataSet) {
|
variableDefs.add(dsMember.getMmsVariableDef());
|
}
|
|
DefineNamedVariableListRequest createDSRequest = new DefineNamedVariableListRequest();
|
createDSRequest.setVariableListName(dataSet.getMmsObjectName());
|
createDSRequest.setListOfVariable(listOfVariable);
|
|
ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
|
confirmedServiceRequest.setDefineNamedVariableList(createDSRequest);
|
|
return confirmedServiceRequest;
|
}
|
|
private void handleCreateDataSetResponse(DataSet dataSet) throws ServiceError {
|
serverModel.addDataSet(dataSet);
|
}
|
|
public void deleteDataSet(DataSet dataSet) throws ServiceError, IOException {
|
ConfirmedServiceRequest serviceRequest = constructDeleteDataSetRequest(dataSet);
|
ConfirmedServiceResponse confirmedServiceResponse = encodeWriteReadDecode(serviceRequest);
|
decodeDeleteDataSetResponse(confirmedServiceResponse, dataSet);
|
}
|
|
private ConfirmedServiceRequest constructDeleteDataSetRequest(DataSet dataSet) throws ServiceError {
|
|
ListOfVariableListName listOfVariableListName = new ListOfVariableListName();
|
|
List<ObjectName> objectList = listOfVariableListName.getObjectName();
|
objectList.add(dataSet.getMmsObjectName());
|
|
DeleteNamedVariableListRequest requestDeleteDS = new DeleteNamedVariableListRequest();
|
requestDeleteDS.setListOfVariableListName(listOfVariableListName);
|
|
ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
|
confirmedServiceRequest.setDeleteNamedVariableList(requestDeleteDS);
|
|
return confirmedServiceRequest;
|
}
|
|
private void decodeDeleteDataSetResponse(ConfirmedServiceResponse confirmedServiceResponse, DataSet dataSet)
|
throws ServiceError {
|
|
if (confirmedServiceResponse.getDeleteNamedVariableList() == null) {
|
throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
"decodeDeleteDataSetResponse: Error decoding server response");
|
}
|
|
DeleteNamedVariableListResponse deleteNamedVariableListResponse = confirmedServiceResponse
|
.getDeleteNamedVariableList();
|
|
if (deleteNamedVariableListResponse.getNumberDeleted().intValue() != 1) {
|
throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT, "number deleted not 1");
|
}
|
|
if (serverModel.removeDataSet(dataSet.getReferenceStr()) == null) {
|
throw new ServiceError(ServiceError.UNKNOWN, "unable to delete dataset locally");
|
}
|
|
}
|
|
/**
|
* The implementation of the GetDataSetValues ACSI service. After a successful return, the Basic Data Attributes of
|
* the data set members will contain the values read. If one of the data set members could not be read, this will be
|
* indicated in the returned list. The returned list will have the same size as the member list of the data set. For
|
* each member it will contain <code>null</code> if reading was successful and a ServiceError if reading of this
|
* member failed.
|
*
|
* @param dataSet
|
* the DataSet that is to be read.
|
* @return a list indicating ServiceErrors that may have occurred.
|
* @throws IOException
|
* if a fatal IO error occurs. The association object will be closed and can no longer be used after
|
* this exception is thrown.
|
*/
|
public List<ServiceError> getDataSetValues(DataSet dataSet) throws IOException {
|
|
ConfirmedServiceResponse confirmedServiceResponse;
|
try {
|
ConfirmedServiceRequest serviceRequest = constructGetDataSetValuesRequest(dataSet);
|
confirmedServiceResponse = encodeWriteReadDecode(serviceRequest);
|
} catch (ServiceError e) {
|
int dataSetSize = dataSet.getMembers().size();
|
List<ServiceError> serviceErrors = new ArrayList<>(dataSetSize);
|
for (int i = 0; i < dataSetSize; i++) {
|
serviceErrors.add(e);
|
}
|
return serviceErrors;
|
}
|
return decodeGetDataSetValuesResponse(confirmedServiceResponse, dataSet);
|
}
|
|
private ConfirmedServiceRequest constructGetDataSetValuesRequest(DataSet dataSet) throws ServiceError {
|
|
VariableAccessSpecification varAccSpec = new VariableAccessSpecification();
|
varAccSpec.setVariableListName(dataSet.getMmsObjectName());
|
|
ReadRequest getDataSetValuesRequest = new ReadRequest();
|
getDataSetValuesRequest.setSpecificationWithResult(new BerBoolean(true));
|
getDataSetValuesRequest.setVariableAccessSpecification(varAccSpec);
|
|
ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
|
confirmedServiceRequest.setRead(getDataSetValuesRequest);
|
|
return confirmedServiceRequest;
|
}
|
|
private List<ServiceError> decodeGetDataSetValuesResponse(ConfirmedServiceResponse confirmedServiceResponse,
|
DataSet ds) {
|
|
int dataSetSize = ds.getMembers().size();
|
List<ServiceError> serviceErrors = new ArrayList<>(dataSetSize);
|
|
if (confirmedServiceResponse.getRead() == null) {
|
ServiceError serviceError = new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
"Error decoding GetDataValuesReponsePdu");
|
for (int i = 0; i < dataSetSize; i++) {
|
serviceErrors.add(serviceError);
|
}
|
return serviceErrors;
|
}
|
|
ReadResponse readResponse = confirmedServiceResponse.getRead();
|
List<AccessResult> listOfAccessResults = readResponse.getListOfAccessResult().getAccessResult();
|
|
if (listOfAccessResults.size() != ds.getMembers().size()) {
|
ServiceError serviceError = new ServiceError(ServiceError.PARAMETER_VALUE_INAPPROPRIATE,
|
"Number of AccessResults does not match the number of DataSet members.");
|
for (int i = 0; i < dataSetSize; i++) {
|
serviceErrors.add(serviceError);
|
}
|
return serviceErrors;
|
}
|
|
Iterator<AccessResult> accessResultIterator = listOfAccessResults.iterator();
|
|
for (FcModelNode dsMember : ds) {
|
AccessResult accessResult = accessResultIterator.next();
|
if (accessResult.getSuccess() != null) {
|
try {
|
dsMember.setValueFromMmsDataObj(accessResult.getSuccess());
|
} catch (ServiceError e) {
|
serviceErrors.add(e);
|
}
|
serviceErrors.add(null);
|
}
|
else {
|
serviceErrors.add(mmsDataAccessErrorToServiceError(accessResult.getFailure()));
|
}
|
}
|
|
return serviceErrors;
|
}
|
|
public List<ServiceError> setDataSetValues(DataSet dataSet) throws ServiceError, IOException {
|
ConfirmedServiceRequest serviceRequest = constructSetDataSetValues(dataSet);
|
ConfirmedServiceResponse confirmedServiceResponse = encodeWriteReadDecode(serviceRequest);
|
return decodeSetDataSetValuesResponse(confirmedServiceResponse);
|
}
|
|
private ConfirmedServiceRequest constructSetDataSetValues(DataSet dataSet) throws ServiceError {
|
VariableAccessSpecification varAccessSpec = new VariableAccessSpecification();
|
varAccessSpec.setVariableListName(dataSet.getMmsObjectName());
|
|
ListOfData listOfData = new ListOfData();
|
List<Data> dataList = listOfData.getData();
|
|
for (ModelNode member : dataSet) {
|
dataList.add(member.getMmsDataObj());
|
}
|
|
WriteRequest writeRequest = new WriteRequest();
|
writeRequest.setVariableAccessSpecification(varAccessSpec);
|
writeRequest.setListOfData(listOfData);
|
|
ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
|
confirmedServiceRequest.setWrite(writeRequest);
|
|
return confirmedServiceRequest;
|
}
|
|
private List<ServiceError> decodeSetDataSetValuesResponse(ConfirmedServiceResponse confirmedServiceResponse)
|
throws ServiceError {
|
|
if (confirmedServiceResponse.getWrite() == null) {
|
throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
"Error decoding SetDataSetValuesReponsePdu");
|
}
|
|
WriteResponse writeResponse = confirmedServiceResponse.getWrite();
|
List<WriteResponse.CHOICE> writeResChoiceType = writeResponse.getCHOICE();
|
List<ServiceError> serviceErrors = new ArrayList<>(writeResChoiceType.size());
|
|
for (WriteResponse.CHOICE accessResult : writeResChoiceType) {
|
if (accessResult.getSuccess() != null) {
|
serviceErrors.add(null);
|
}
|
else {
|
serviceErrors.add(mmsDataAccessErrorToServiceError(accessResult.getFailure()));
|
}
|
}
|
return serviceErrors;
|
|
}
|
|
public void getRcbValues(Rcb rcb) throws ServiceError, IOException {
|
getDataValues(rcb);
|
}
|
|
public void reserveUrcb(Urcb urcb) throws ServiceError, IOException {
|
BdaBoolean resvBda = urcb.getResv();
|
resvBda.setValue(true);
|
setDataValues(resvBda);
|
}
|
|
public void reserveBrcb(Brcb brcb, short resvTime) throws ServiceError, IOException {
|
BdaInt16 resvTmsBda = brcb.getResvTms();
|
resvTmsBda.setValue(resvTime);
|
setDataValues(resvTmsBda);
|
}
|
|
public void cancelUrcbReservation(Urcb urcb) throws ServiceError, IOException {
|
BdaBoolean resvBda = urcb.getResv();
|
resvBda.setValue(false);
|
setDataValues(resvBda);
|
}
|
|
public void enableReporting(Rcb rcb) throws ServiceError, IOException {
|
BdaBoolean rptEnaBda = rcb.getRptEna();
|
rptEnaBda.setValue(true);
|
setDataValues(rptEnaBda);
|
}
|
|
public void disableReporting(Rcb rcb) throws ServiceError, IOException {
|
BdaBoolean rptEnaBda = rcb.getRptEna();
|
rptEnaBda.setValue(false);
|
setDataValues(rptEnaBda);
|
}
|
|
public void startGi(Rcb rcb) throws ServiceError, IOException {
|
BdaBoolean rptGiBda = (BdaBoolean) rcb.getChild("GI");
|
rptGiBda.setValue(true);
|
setDataValues(rptGiBda);
|
}
|
|
/**
|
* Sets the selected values of the given report control block. Note that all these parameters may only be set if the
|
* RCB has been reserved but reporting has not been enabled yet.
|
* <p>
|
* The data set reference as it is set in an RCB must contain a dollar sign instead of a dot to separate the logical
|
* node from the data set name, e.g.: 'LDevice1/LNode$DataSetName'. Therefore his method will check the reference
|
* for a dot and if necessary convert it to a '$' sign before sending the request to the server.
|
* <p>
|
* The parameters PurgeBuf, EntryId are only applicable if the given rcb is of type BRCB.
|
*
|
*
|
* @param rcb
|
* the report control block
|
* @param setRptId
|
* whether to set the report ID
|
* @param setDatSet
|
* whether to set the data set
|
* @param setOptFlds
|
* whether to set the optional fields
|
* @param setBufTm
|
* whether to set the buffer time
|
* @param setTrgOps
|
* whether to set the trigger options
|
* @param setIntgPd
|
* whether to set the integrity period
|
* @param setPurgeBuf
|
* whether to set purge buffer
|
* @param setEntryId
|
* whether to set the entry ID
|
* @return a list indicating ServiceErrors that may have occurred.
|
* @throws IOException
|
* if a fatal IO error occurs. The association object will be closed and can no longer be used after
|
* this exception is thrown.
|
*/
|
public List<ServiceError> setRcbValues(Rcb rcb, boolean setRptId, boolean setDatSet, boolean setOptFlds,
|
boolean setBufTm, boolean setTrgOps, boolean setIntgPd, boolean setPurgeBuf, boolean setEntryId)
|
throws IOException {
|
|
List<FcModelNode> parametersToSet = new ArrayList<>(6);
|
|
if (setRptId == true) {
|
parametersToSet.add(rcb.getRptId());
|
}
|
if (setDatSet == true) {
|
rcb.getDatSet().setValue(rcb.getDatSet().getStringValue().replace('.', '$'));
|
parametersToSet.add(rcb.getDatSet());
|
}
|
if (setOptFlds == true) {
|
parametersToSet.add(rcb.getOptFlds());
|
}
|
if (setBufTm == true) {
|
parametersToSet.add(rcb.getBufTm());
|
}
|
if (setTrgOps == true) {
|
parametersToSet.add(rcb.getTrgOps());
|
}
|
if (setIntgPd == true) {
|
parametersToSet.add(rcb.getIntgPd());
|
}
|
if (rcb instanceof Brcb) {
|
Brcb brcb = (Brcb) rcb;
|
if (setPurgeBuf == true) {
|
parametersToSet.add(brcb.getPurgeBuf());
|
}
|
if (setEntryId == true) {
|
parametersToSet.add(brcb.getEntryId());
|
}
|
}
|
|
List<ServiceError> serviceErrors = new ArrayList<>(parametersToSet.size());
|
|
for (FcModelNode child : parametersToSet) {
|
try {
|
setDataValues(child);
|
serviceErrors.add(null);
|
} catch (ServiceError e) {
|
serviceErrors.add(e);
|
}
|
}
|
|
return serviceErrors;
|
}
|
|
private Report processReport(MMSpdu mmsPdu) throws ServiceError {
|
|
if (mmsPdu.getUnconfirmedPDU() == null) {
|
throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
"getReport: Error decoding server response");
|
}
|
|
UnconfirmedPDU unconfirmedRes = mmsPdu.getUnconfirmedPDU();
|
|
if (unconfirmedRes.getService() == null) {
|
throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
"getReport: Error decoding server response");
|
}
|
|
UnconfirmedService unconfirmedServ = unconfirmedRes.getService();
|
|
if (unconfirmedServ.getInformationReport() == null) {
|
throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
"getReport: Error decoding server response");
|
}
|
|
List<AccessResult> listRes = unconfirmedServ.getInformationReport().getListOfAccessResult().getAccessResult();
|
|
int index = 0;
|
|
if (listRes.get(index).getSuccess().getVisibleString() == null) {
|
throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
"processReport: report does not contain RptID");
|
}
|
|
String rptId = listRes.get(index++).getSuccess().getVisibleString().toString();
|
|
if (listRes.get(index).getSuccess().getBitString() == null) {
|
throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
"processReport: report does not contain OptFlds");
|
}
|
|
BdaOptFlds optFlds = new BdaOptFlds(new ObjectReference("none"), null);
|
optFlds.setValue(listRes.get(index++).getSuccess().getBitString().value);
|
|
Integer sqNum = null;
|
if (optFlds.isSequenceNumber()) {
|
sqNum = listRes.get(index++).getSuccess().getUnsigned().intValue();
|
}
|
|
BdaEntryTime timeOfEntry = null;
|
if (optFlds.isReportTimestamp()) {
|
timeOfEntry = new BdaEntryTime(new ObjectReference("none"), null, "", false, false);
|
timeOfEntry.setValueFromMmsDataObj(listRes.get(index++).getSuccess());
|
}
|
|
String dataSetRef = null;
|
if (optFlds.isDataSetName()) {
|
dataSetRef = (listRes.get(index++).getSuccess().getVisibleString().toString());
|
}
|
else {
|
for (Urcb urcb : serverModel.getUrcbs()) {
|
if ((urcb.getRptId() != null && urcb.getRptId().getStringValue().equals(rptId))
|
|| urcb.getReference().toString().equals(rptId)) {
|
dataSetRef = urcb.getDatSet().getStringValue();
|
break;
|
}
|
}
|
if (dataSetRef == null) {
|
for (Brcb brcb : serverModel.getBrcbs()) {
|
if ((brcb.getRptId() != null && brcb.getRptId().getStringValue().equals(rptId))
|
|| brcb.getReference().toString().equals(rptId)) {
|
dataSetRef = brcb.getDatSet().getStringValue();
|
break;
|
}
|
}
|
}
|
}
|
if (dataSetRef == null) {
|
throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
"unable to find RCB that matches the given RptID in the report.");
|
}
|
dataSetRef = dataSetRef.replace('$', '.');
|
|
DataSet dataSet = serverModel.getDataSet(dataSetRef);
|
if (dataSet == null) {
|
throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
"unable to find data set that matches the given data set reference of the report.");
|
}
|
|
Boolean bufOvfl = null;
|
if (optFlds.isBufferOverflow()) {
|
bufOvfl = (listRes.get(index++).getSuccess().getBool().value);
|
}
|
|
BdaOctetString entryId = null;
|
if (optFlds.isEntryId()) {
|
entryId = new BdaOctetString(new ObjectReference("none"), null, "", 8, false, false);
|
entryId.setValue(listRes.get(index++).getSuccess().getOctetString().value);
|
}
|
|
Long confRev = null;
|
if (optFlds.isConfigRevision()) {
|
confRev = listRes.get(index++).getSuccess().getUnsigned().longValue();
|
}
|
|
Integer subSqNum = null;
|
boolean moreSegmentsFollow = false;
|
if (optFlds.isSegmentation()) {
|
subSqNum = listRes.get(index++).getSuccess().getUnsigned().intValue();
|
moreSegmentsFollow = listRes.get(index++).getSuccess().getBool().value;
|
}
|
|
boolean[] inclusionBitString = listRes.get(index++).getSuccess().getBitString().getValueAsBooleans();
|
int numMembersReported = 0;
|
for (boolean bit : inclusionBitString) {
|
if (bit) {
|
numMembersReported++;
|
}
|
}
|
|
if (optFlds.isDataReference()) {
|
// this is just to move the index to the right place
|
// The next part will process the changes to the values
|
// without the dataRefs
|
index += numMembersReported;
|
}
|
|
List<FcModelNode> reportedDataSetMembers = new ArrayList<>(numMembersReported);
|
int dataSetIndex = 0;
|
for (FcModelNode dataSetMember : dataSet.getMembers()) {
|
if (inclusionBitString[dataSetIndex]) {
|
AccessResult accessRes = listRes.get(index++);
|
FcModelNode dataSetMemberCopy = (FcModelNode) dataSetMember.copy();
|
dataSetMemberCopy.setValueFromMmsDataObj(accessRes.getSuccess());
|
reportedDataSetMembers.add(dataSetMemberCopy);
|
}
|
dataSetIndex++;
|
}
|
|
List<BdaReasonForInclusion> reasonCodes = null;
|
if (optFlds.isReasonForInclusion()) {
|
reasonCodes = new ArrayList<>(dataSet.getMembers().size());
|
for (int i = 0; i < dataSet.getMembers().size(); i++) {
|
if (inclusionBitString[i]) {
|
BdaReasonForInclusion reasonForInclusion = new BdaReasonForInclusion(null);
|
reasonCodes.add(reasonForInclusion);
|
byte[] reason = listRes.get(index++).getSuccess().getBitString().value;
|
reasonForInclusion.setValue(reason);
|
}
|
|
}
|
}
|
|
return new Report(rptId, sqNum, subSqNum, moreSegmentsFollow, dataSetRef, bufOvfl, confRev, timeOfEntry,
|
entryId, inclusionBitString, reportedDataSetMembers, reasonCodes);
|
|
}
|
|
/**
|
* Performs the Select ACSI Service of the control model on the given controllable Data Object (DO). By selecting a
|
* controllable DO you can reserve it for exclusive control/operation. This service is only applicable if the
|
* ctlModel Data Attribute is set to "sbo-with-normal-security" (2).
|
*
|
* The selection is canceled in one of the following events:
|
* <ul>
|
* <li>The "Cancel" ACSI service is issued.</li>
|
* <li>The sboTimemout (select before operate timeout) runs out. If the given controlDataObject contains a
|
* sboTimeout Data Attribute it is possible to change the timeout after which the selection/reservation is
|
* automatically canceled by the server. Otherwise the timeout is a local issue of the server.</li>
|
* <li>The connection to the server is closed.</li>
|
* <li>An operate service failed because of some error</li>
|
* <li>The sboClass is set to "operate-once" then the selection is also canceled after a successful operate service.
|
* </li>
|
* </ul>
|
*
|
* @param controlDataObject
|
* needs to be a controllable Data Object that contains a Data Attribute named "SBO".
|
* @return false if the selection/reservation was not successful (because it is already selected by another client).
|
* Otherwise true is returned.
|
* @throws ServiceError
|
* if a ServiceError is returned by the server.
|
* @throws IOException
|
* if a fatal IO error occurs. The association object will be closed and can no longer be used after
|
* this exception is thrown.
|
*/
|
public boolean select(FcModelNode controlDataObject) throws ServiceError, IOException {
|
BdaVisibleString sbo;
|
try {
|
sbo = (BdaVisibleString) controlDataObject.getChild("SBO");
|
} catch (Exception e) {
|
throw new IllegalArgumentException("ModelNode needs to conain a child node named SBO in order to select");
|
}
|
|
getDataValues(sbo);
|
|
if (sbo.getValue().length == 0) {
|
return false;
|
}
|
return true;
|
|
}
|
|
/**
|
* Executes the Operate ACSI Service on the given controllable Data Object (DO). The following subnodes of the given
|
* control DO should be set according your needs before calling this function. (Note that you can probably leave
|
* most attributes with their default value):
|
* <ul>
|
* <li>Oper.ctlVal - has to be set to actual control value that is to be written using the operate service.</li>
|
* <li>Oper.operTm (type: BdaTimestamp) - is an optional sub data attribute of Oper (thus it may not exist). If it
|
* exists it can be used to set the timestamp when the operation shall be performed by the server. Thus the server
|
* will delay execution of the operate command until the given date is reached. Can be set to an empty byte array
|
* (new byte[0]) or null so that the server executes the operate command immediately. This is also the default.</li>
|
* <li>Oper.check (type: BdaCheck) is used to tell the server whether to perform the synchrocheck and
|
* interlockcheck. By default they are turned off.</li>
|
* <li>Oper.orign - contains the two data attributes orCat (origin category, type: BdaInt8) and orIdent (origin
|
* identifier, type BdaOctetString). Origin is optionally reflected in the status Data Attribute controlDO.origin.
|
* By reading this data attribute other clients can see who executed the last operate command. The default value for
|
* orCat is 0 ("not-supported") and the default value for orIdent is ""(the empty string).</li>
|
* <li>Oper.Test (BdaBoolean) - if true this operate command is sent for test purposes only. Default is false.</li>
|
* </ul>
|
*
|
* All other operate parameters are automatically handled by this function.
|
*
|
* @param controlDataObject
|
* needs to be a controllable Data Object that contains a Data Attribute named "Oper".
|
* @throws ServiceError
|
* if a ServiceError is returned by the server
|
* @throws IOException
|
* if a fatal IO error occurs. The association object will be closed and can no longer be used after
|
* this exception is thrown.
|
*/
|
public void operate(FcModelNode controlDataObject) throws ServiceError, IOException {
|
ConstructedDataAttribute oper;
|
try {
|
oper = (ConstructedDataAttribute) controlDataObject.getChild("Oper");
|
} catch (Exception e) {
|
throw new IllegalArgumentException("ModelNode needs to conain a child node named \"Oper\".");
|
}
|
|
((BdaInt8U) oper.getChild("ctlNum")).setValue((short) 1);
|
((BdaTimestamp) oper.getChild("T")).setDate(new Date(System.currentTimeMillis()));
|
|
setDataValues(oper);
|
}
|
|
/**
|
* Will close the connection simply by closing the TCP socket.
|
*/
|
public void close() {
|
clientReceiver.close(new IOException("Connection closed by client"));
|
}
|
|
/**
|
* Will send a disconnect request first and then close the TCP socket.
|
*/
|
public void disconnect() {
|
clientReceiver.disconnect();
|
}
|
|
}
|