package com.serotonin.bacnet4j.transport; import java.util.LinkedList; import java.util.logging.Level; import java.util.logging.Logger; import com.serotonin.bacnet4j.LocalDevice; import com.serotonin.bacnet4j.apdu.APDU; import com.serotonin.bacnet4j.apdu.Abort; import com.serotonin.bacnet4j.apdu.AckAPDU; import com.serotonin.bacnet4j.apdu.ComplexACK; import com.serotonin.bacnet4j.apdu.ConfirmedRequest; import com.serotonin.bacnet4j.apdu.Error; import com.serotonin.bacnet4j.apdu.Reject; import com.serotonin.bacnet4j.apdu.SegmentACK; import com.serotonin.bacnet4j.apdu.Segmentable; import com.serotonin.bacnet4j.apdu.SimpleACK; import com.serotonin.bacnet4j.apdu.UnconfirmedRequest; import com.serotonin.bacnet4j.enums.MaxSegments; import com.serotonin.bacnet4j.event.ExceptionDispatch; import com.serotonin.bacnet4j.exception.AbortAPDUException; import com.serotonin.bacnet4j.exception.BACnetErrorException; import com.serotonin.bacnet4j.exception.BACnetException; import com.serotonin.bacnet4j.exception.BACnetRejectException; import com.serotonin.bacnet4j.exception.BACnetTimeoutException; import com.serotonin.bacnet4j.exception.ErrorAPDUException; import com.serotonin.bacnet4j.exception.NotImplementedException; import com.serotonin.bacnet4j.exception.ReflectionException; import com.serotonin.bacnet4j.exception.RejectAPDUException; import com.serotonin.bacnet4j.npdu.Network; import com.serotonin.bacnet4j.npdu.NetworkIdentifier; import com.serotonin.bacnet4j.npdu.ip.SegmentWindow; import com.serotonin.bacnet4j.service.acknowledgement.AcknowledgementService; import com.serotonin.bacnet4j.service.confirmed.ConfirmedRequestService; import com.serotonin.bacnet4j.service.unconfirmed.UnconfirmedRequestService; import com.serotonin.bacnet4j.type.constructed.Address; import com.serotonin.bacnet4j.type.constructed.BACnetError; import com.serotonin.bacnet4j.type.enumerated.ErrorClass; import com.serotonin.bacnet4j.type.enumerated.ErrorCode; import com.serotonin.bacnet4j.type.enumerated.Segmentation; import com.serotonin.bacnet4j.type.error.BaseError; import com.serotonin.bacnet4j.type.primitive.OctetString; import org.free.bacnet4j.util.ByteQueue; /** * Provides segmentation support for all data link types. * * @author Matthew */ public class Transport { public static final int DEFAULT_TIMEOUT = 6000; public static final int DEFAULT_SEG_TIMEOUT = 5000; public static final int DEFAULT_SEG_WINDOW = 5; public static final int DEFAULT_RETRIES = 2; private static final Logger LOG = Logger.getLogger(Transport.class.getName()); private static final MaxSegments MAX_SEGMENTS = MaxSegments.MORE_THAN_64; private LocalDevice localDevice; private final Network network; private final WaitingRoom waitingRoom = new WaitingRoom(); private int timeout = DEFAULT_TIMEOUT; private int segTimeout = DEFAULT_SEG_TIMEOUT; private int segWindow = DEFAULT_SEG_WINDOW; private int retries = DEFAULT_RETRIES; public Transport(Network network) { this.network = network; } public NetworkIdentifier getNetworkIdentifier() { return network.getNetworkIdentifier(); } public void setTimeout(int timeout) { this.timeout = timeout; } public int getTimeout() { return timeout; } public void setSegTimeout(int segTimeout) { this.segTimeout = segTimeout; } public int getSegTimeout() { return segTimeout; } public void setRetries(int retries) { this.retries = retries; } public int getRetries() { return retries; } public void setSegWindow(int segWindow) { this.segWindow = segWindow; } public int getSegWindow() { return segWindow; } public Network getNetwork() { return network; } public LocalDevice getLocalDevice() { return localDevice; } public void setLocalDevice(LocalDevice localDevice) { this.localDevice = localDevice; } public void initialize() throws Exception { network.initialize(this); } public void terminate() { network.terminate(); } // // // Sending messages - messages are initiated by the application layer // /** * Sends a confirmed service, and blocks until a reply is received, or timeout. * * @param address * @param linkService * @param maxAPDULengthAccepted * @param segmentationSupported * @param service * @return * @throws BACnetException */ public AcknowledgementService send(Address address, OctetString linkService, int maxAPDULengthAccepted, Segmentation segmentationSupported, ConfirmedRequestService service) throws BACnetException { if (address == null) throw new IllegalArgumentException("address cannot be null"); network.checkSendThread(); if (address.equals(linkService)) linkService = null; // Serialize the service request. ByteQueue serviceData = new ByteQueue(); service.write(serviceData); AckAPDU ack; // Check if we need to segment the message. if (serviceData.size() > maxAPDULengthAccepted - ConfirmedRequest.getHeaderSize(false)) { int maxServiceData = maxAPDULengthAccepted - ConfirmedRequest.getHeaderSize(true); // Check if the device can accept what we want to send. if (segmentationSupported.intValue() == Segmentation.noSegmentation.intValue() || segmentationSupported.intValue() == Segmentation.segmentedTransmit.intValue()) throw new BACnetException("Request too big to send to device without segmentation"); int segmentsRequired = serviceData.size() / maxServiceData + 1; if (segmentsRequired > 128) throw new BACnetException("Request too big to send to device; too many segments required"); WaitingRoomKey key = waitingRoom.enterClient(address, linkService); try { ConfirmedRequest template = new ConfirmedRequest(true, true, true, MAX_SEGMENTS, network.getMaxApduLength(), key.getInvokeId(), 0, segWindow, service.getChoiceId(), (ByteQueue) null); ack = sendSegmented(key, maxAPDULengthAccepted, maxServiceData, serviceData, template); if (ack == null) // Go to the waiting room to wait for a response. ack = waitForAck(key, timeout, true); } finally { waitingRoom.leave(key); } } else { // We can send the whole APDU in one shot. WaitingRoomKey key = waitingRoom.enterClient(address, linkService); ConfirmedRequest apdu = new ConfirmedRequest(false, false, true, MAX_SEGMENTS, network.getMaxApduLength(), key.getInvokeId(), (byte) 0, 0, service.getChoiceId(), serviceData); try { ack = sendSegments(key, timeout, new APDU[] { apdu }); } finally { waitingRoom.leave(key); } } if (ack instanceof SimpleACK) return null; if (ack instanceof ComplexACK) return ((ComplexACK) ack).getService(); if (ack instanceof Error) throw new ErrorAPDUException((Error) ack); if (ack instanceof Reject) throw new RejectAPDUException((Reject) ack); if (ack instanceof Abort) throw new AbortAPDUException((Abort) ack); throw new BACnetException("Unexpected ack APDU: " + ack); } public Address getLocalBroadcastAddress() { return network.getLocalBroadcastAddress(); } public void sendUnconfirmed(Address address, OctetString linkService, UnconfirmedRequestService serviceRequest, boolean broadcast) throws BACnetException { if (address == null) throw new IllegalArgumentException("address cannot be null"); if (address.equals(linkService)) linkService = null; // Unconfirmed services will never have to be segmented, so just send it. network.sendAPDU(address, linkService, new UnconfirmedRequest(serviceRequest), broadcast); } // // // Receiving messages - messages are initiated externally, and received from the data link. Just call the handler // method in the service. // private AcknowledgementService handleConfirmedRequest(Address from, OctetString linkService, byte invokeId, ConfirmedRequestService service) throws BACnetException { try { return service.handle(localDevice, from, linkService); } catch (NotImplementedException e) { System.out.println("Unsupported confirmed request: invokeId=" + invokeId + ", from=" + from + ", request=" + service.getClass().getName()); throw new BACnetErrorException(ErrorClass.services, ErrorCode.serviceRequestDenied); } catch (BACnetErrorException e) { throw e; } catch (Exception e) { throw new BACnetErrorException(ErrorClass.device, ErrorCode.operationalProblem); } } // // // Details // private AckAPDU sendSegments(WaitingRoomKey key, int timeout, APDU[] apdu) throws BACnetException { AckAPDU response = null; int attempts = retries + 1; // The retry loop. while (true) { for (int i = 0; i < apdu.length; i++) network.sendAPDU(key.getAddress(), key.getLinkService(), apdu[i], false); response = waitForAck(key, timeout, false); if (response == null) { attempts--; if (attempts > 0) // Try again continue; // Give up throw new BACnetTimeoutException("Timeout while waiting for response for id " + key.getInvokeId()); } // We got the response break; } return response; } void sendResponse(Address address, OctetString linkService, ConfirmedRequest request, AcknowledgementService response) throws BACnetException { if (response == null) network.sendAPDU(address, linkService, new SimpleACK(request.getInvokeId(), request.getServiceRequest() .getChoiceId()), false); else { // A complex ack response. Serialize the data. ByteQueue serviceData = new ByteQueue(); response.write(serviceData); // Check if we need to segment the message. if (serviceData.size() > request.getMaxApduLengthAccepted().getMaxLength() - ComplexACK.getHeaderSize(false)) { int maxServiceData = request.getMaxApduLengthAccepted().getMaxLength() - ComplexACK.getHeaderSize(true); // Check if the device can accept what we want to send. if (!request.isSegmentedResponseAccepted()) throw new BACnetException("Response too big to send to device without segmentation"); int segmentsRequired = serviceData.size() / maxServiceData + 1; if (segmentsRequired > request.getMaxSegmentsAccepted().getMaxSegments() || segmentsRequired > 128) throw new BACnetException("Response too big to send to device; too many segments required"); WaitingRoomKey key = waitingRoom.enterServer(address, linkService, request.getInvokeId()); try { ComplexACK template = new ComplexACK(true, true, key.getInvokeId(), (byte) 0, segWindow, response.getChoiceId(), (ByteQueue) null); AckAPDU ack = sendSegmented(key, request.getMaxApduLengthAccepted().getMaxLength(), maxServiceData, serviceData, template); if (ack != null) throw new BACnetException("Invalid response from client while sending segmented response: " + ack); } finally { waitingRoom.leave(key); } } else // We can send the whole APDU in one shot. network.sendAPDU(address, linkService, new ComplexACK(false, false, request.getInvokeId(), 0, 0, response), false); } } private AckAPDU sendSegmented(WaitingRoomKey key, int maxAPDULengthAccepted, int maxServiceData, ByteQueue serviceData, Segmentable segmentTemplate) throws BACnetException { // System.out.println("Send segmented: "+ (serviceData.size() / maxServiceData + 1) +" segments required"); // Send an initial message to negotiate communication terms. ByteQueue segData = new ByteQueue(maxServiceData); byte[] data = new byte[maxServiceData]; serviceData.pop(data); segData.push(data); APDU apdu = segmentTemplate.clone(true, 0, segmentTemplate.getProposedWindowSize(), segData); // System.out.println("Sending segment 0"); AckAPDU response = sendSegments(key, segTimeout, new APDU[] { apdu }); if (!(response instanceof SegmentACK)) return response; SegmentACK ack = (SegmentACK) response; int actualSegWindow = ack.getActualWindowSize(); // Create a queue of messages to send. LinkedList<APDU> apduQueue = new LinkedList<APDU>(); boolean finalMessage; byte sequenceNumber = 1; while (serviceData.size() > 0) { finalMessage = serviceData.size() <= maxAPDULengthAccepted; segData = new ByteQueue(); int count = serviceData.pop(data); segData.push(data, 0, count); apdu = segmentTemplate.clone(!finalMessage, sequenceNumber++, actualSegWindow, segData); apduQueue.add(apdu); } // Send the data in windows of size given by the recipient. while (apduQueue.size() > 0) { APDU[] window = new APDU[Math.min(apduQueue.size(), actualSegWindow)]; // System.out.println("Sending "+ window.length +" segments"); for (int i = 0; i < window.length; i++) window[i] = apduQueue.poll(); response = sendSegments(key, segTimeout, window); if (response instanceof SegmentACK) { ack = (SegmentACK) response; // if (ack.isNegativeAck()) // System.out.println("NAK received: "+ ack); int receivedSeq = ack.getSequenceNumber() & 0xff; while (apduQueue.size() > 0 && (((Segmentable) apduQueue.peek()).getSequenceNumber() & 0xff) <= receivedSeq) apduQueue.poll(); } else return response; } return null; } /** * Waits for an acknowledgement. If the ack is complex and segmented, all of the segments will be read. * * @param key * @param timeout * @param throwTimeout * @return * @throws BACnetException */ private AckAPDU waitForAck(WaitingRoomKey key, long timeout, boolean throwTimeout) throws BACnetException { AckAPDU ack = waitingRoom.getAck(key, timeout, throwTimeout); if (!(ack instanceof ComplexACK)) return ack; ComplexACK firstPart = (ComplexACK) ack; if (firstPart.isSegmentedMessage()) receiveSegmented(key, firstPart); ByteQueue copy = (ByteQueue) firstPart.getServiceData().clone(); try { firstPart.parseServiceData(); } catch (ReflectionException e) { // Detect runtime exceptions in order to provide the original service data that, we presume, // caused the problem. throw new RuntimeException("Exception while parsing service data: '" + copy + "'", e); } return firstPart; } /** * Manages the receipt of a segmented message. * * @param key * @param firstPart * @throws BACnetException */ void receiveSegmented(WaitingRoomKey key, Segmentable firstPart) throws BACnetException { if (LOG.isLoggable(Level.FINER)) LOG.finer("receiveSegmented: start"); boolean server = !key.isFromServer(); byte id = firstPart.getInvokeId(); int window = firstPart.getProposedWindowSize(); int lastSeq = firstPart.getSequenceNumber() & 0xff; // System.out.println("Receiving segmented: window="+ window); // Send a segment ack. Go with whatever window size was proposed. network.sendAPDU(key.getAddress(), key.getLinkService(), new SegmentACK(false, server, id, lastSeq, window, true), false); // The loop for receiving windows of request parts. Segmentable segment; SegmentWindow segmentWindow = new SegmentWindow(window, lastSeq + 1); while (true) { segment = segmentWindow.getSegment(lastSeq + 1); if (segment == null) { // Wait for the next part of the message to arrive. segment = waitingRoom.getSegmentable(key, segTimeout * 4, false); if (segment == null) { // We timed out waiting for a segment. if (segmentWindow.isEmpty()) // We didn't receive anything, so throw a timeout exception. throw new BACnetTimeoutException("Timeout while waiting for segment part: invokeId=" + id + ", sequenceId=" + (lastSeq + 1)); // Return a NAK with the last sequence id received in order and start over. network.sendAPDU(key.getAddress(), key.getLinkService(), new SegmentACK(true, server, id, lastSeq, window, true), false); segmentWindow.clear(lastSeq + 1); } else if (segmentWindow.fitsInWindow(segment)) segmentWindow.setSegment(segment); continue; } // We have the required segment. Append the part to the first part. // System.out.println("Received segment "+ segment.getSequenceNumber()); firstPart.appendServiceData(segment.getServiceData()); if (LOG.isLoggable(Level.FINER)) LOG.finer("receiveSegmented: got segment " + lastSeq); lastSeq++; // Do we need to send an ack? if (!segment.isMoreFollows() || segmentWindow.isLastSegment(lastSeq)) { // Return an acknowledgement network.sendAPDU(key.getAddress(), key.getLinkService(), new SegmentACK(false, server, id, lastSeq, window, segment.isMoreFollows()), false); segmentWindow.clear(lastSeq + 1); } if (!segment.isMoreFollows()) // We're done. break; } if (LOG.isLoggable(Level.FINER)) LOG.finer("receiveSegmented: done"); } public void incomingApdu(APDU apdu, Address address, OctetString linkService) throws BACnetException { // if (apdu.expectsReply() != npci.isExpectingReply()) // throw new MessageValidationAssertionException("Inconsistent message: APDU expectsReply="+ // apdu.expectsReply() +" while NPCI isExpectingReply="+ npci.isExpectingReply()); if (apdu instanceof ConfirmedRequest) { ConfirmedRequest confAPDU = (ConfirmedRequest) apdu; byte invokeId = confAPDU.getInvokeId(); if (confAPDU.isSegmentedMessage() && confAPDU.getSequenceNumber() > 0) // This is a subsequent part of a segmented message. Notify the waiting room. waitingRoom.notifyMember(address, linkService, invokeId, false, confAPDU); else { if (confAPDU.isSegmentedMessage()) { // This is the initial part of a segmented message. Go and receive the subsequent parts. WaitingRoomKey key = waitingRoom.enterServer(address, linkService, invokeId); try { receiveSegmented(key, confAPDU); } finally { waitingRoom.leave(key); } } // Handle the request. try { confAPDU.parseServiceData(); AcknowledgementService ackService = handleConfirmedRequest(address, linkService, invokeId, confAPDU.getServiceRequest()); sendResponse(address, linkService, confAPDU, ackService); } catch (BACnetErrorException e) { network.sendAPDU(address, linkService, new Error(invokeId, e.getError()), false); } catch (BACnetRejectException e) { network.sendAPDU(address, linkService, new Reject(invokeId, e.getRejectReason()), false); } catch (BACnetException e) { Error error = new Error(confAPDU.getInvokeId(), new BaseError((byte) 127, new BACnetError( ErrorClass.services, ErrorCode.inconsistentParameters))); network.sendAPDU(address, linkService, error, false); ExceptionDispatch.fireReceivedException(e); } } } else if (apdu instanceof UnconfirmedRequest) { UnconfirmedRequest ur = (UnconfirmedRequest) apdu; try { ur.getService().handle(localDevice, address, linkService); } catch (BACnetException e) { ExceptionDispatch.fireReceivedException(e); } } else { // An acknowledgement. AckAPDU ack = (AckAPDU) apdu; // Used for testing only. This is required to test the parsing of service data in an ack. // ((ComplexACK) ack).parseServiceData(); waitingRoom.notifyMember(address, linkService, ack.getOriginalInvokeId(), ack.isServer(), ack); } } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((network == null) ? 0 : network.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Transport other = (Transport) obj; if (network == null) { if (other.network != null) return false; } else if (!network.equals(other.network)) return false; return true; } }