/*
* ============================================================================
* GNU General Public License
* ============================================================================
*
* Copyright (C) 2006-2011 Serotonin Software Technologies Inc. http://serotoninsoftware.com
* @author Matthew Lohbihler
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* When signing a commercial license with Serotonin Software Technologies Inc.,
* the following extension to GPL is made. A special exception to the GPL is
* included to allow you to distribute a combined work that includes BAcnet4J
* without being obliged to provide the source code for any proprietary components.
*/
package com.serotonin.bacnet4j.npdu.ip;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import com.serotonin.bacnet4j.LocalDevice;
import com.serotonin.bacnet4j.apdu.APDU;
import com.serotonin.bacnet4j.base.BACnetUtils;
import com.serotonin.bacnet4j.enums.MaxApduLength;
import com.serotonin.bacnet4j.exception.BACnetException;
import com.serotonin.bacnet4j.npdu.IncomingRequestParser;
import com.serotonin.bacnet4j.npdu.MessageValidationAssertionException;
import com.serotonin.bacnet4j.npdu.Network;
import com.serotonin.bacnet4j.npdu.NetworkIdentifier;
import com.serotonin.bacnet4j.transport.Transport;
import com.serotonin.bacnet4j.type.constructed.Address;
import com.serotonin.bacnet4j.type.primitive.OctetString;
import org.free.bacnet4j.util.ByteQueue;
public class IpNetwork extends Network implements Runnable {
public static final String DEFAULT_BROADCAST_IP = "255.255.255.255";
public static final int DEFAULT_PORT = 0xBAC0; // == 47808
public static final String DEFAULT_BIND_IP = "0.0.0.0";
private static final int MESSAGE_LENGTH = 2048;
LocalDevice localDevice;
private final int port;
private final String localBindAddress;
private final String broadcastIp;
// Runtime
private Thread thread;
private DatagramSocket socket;
private Address broadcastAddress;
public IpNetwork() {
this(DEFAULT_BROADCAST_IP);
}
public IpNetwork(String broadcastIp) {
this(broadcastIp, DEFAULT_PORT);
}
public IpNetwork(String broadcastIp, int port) {
this(broadcastIp, port, DEFAULT_BIND_IP);
}
public IpNetwork(String broadcastIp, int port, String localBindAddress) {
this(broadcastIp, port, localBindAddress, 0);
}
public IpNetwork(String broadcastIp, int port, String localBindAddress, int localNetworkNumber) {
super(localNetworkNumber);
this.broadcastIp = broadcastIp;
this.port = port;
this.localBindAddress = localBindAddress;
}
@Override
public NetworkIdentifier getNetworkIdentifier() {
return new IpNetworkIdentifier(port, localBindAddress);
}
@Override
public MaxApduLength getMaxApduLength() {
return MaxApduLength.UP_TO_1476;
}
public int getPort() {
return port;
}
public String getLocalBindAddress() {
return localBindAddress;
}
public String getBroadcastIp() {
return broadcastIp;
}
@Override
public void initialize(Transport transport) throws Exception {
super.initialize(transport);
this.localDevice = transport.getLocalDevice();
if (localBindAddress.equals("0.0.0.0"))
socket = new DatagramSocket(port);
else
socket = new DatagramSocket(port, InetAddress.getByName(localBindAddress));
socket.setBroadcast(true);
// broadcastAddress = new Address(broadcastIp, port, new Network(0xffff, new byte[0]));
broadcastAddress = new Address(BACnetUtils.dottedStringToBytes(broadcastIp), port);
thread = new Thread(this);
thread.start();
}
@Override
public void terminate() {
if (socket != null)
socket.close();
}
@Override
public Address getLocalBroadcastAddress() {
return broadcastAddress;
}
public Address getBroadcastAddress(int port) {
return new Address(BACnetUtils.dottedStringToBytes(broadcastIp), port);
}
@Override
public void checkSendThread() {
if (Thread.currentThread() == thread)
throw new IllegalStateException("Cannot send a request in the socket listener thread.");
}
/** The total length of the foreign device registration package. */
private static final int REGISTER_FOREIGN_DEVICE_LENGTH = 6;
/**
* Sends a foreign device registration request to addr. On successful registration (AKN), we are added
* in the foreign device table FDT.
*
* @param addr
* The address of the device where our device wants to be registered as foreign device
* @param timeToLive
* The time until we are automatically removed out of the FDT.
* @throws BACnetException
*/
public void sendRegisterForeignDeviceMessage(InetSocketAddress addr, int timeToLive) throws BACnetException {
ByteQueue queue = new ByteQueue();
// BACnet/IP
queue.push(0x81);
// Register foreign device
queue.push(0x05);
BACnetUtils.pushShort(queue, REGISTER_FOREIGN_DEVICE_LENGTH);
BACnetUtils.pushShort(queue, timeToLive);
sendPacket(addr, queue.popAll());
}
@Override
public void sendAPDU(Address recipient, OctetString link, APDU apdu, boolean broadcast) throws BACnetException {
ByteQueue queue = new ByteQueue();
// BACnet virtual link layer detail
// BACnet/IP
queue.push(0x81);
// Original-Unicast-NPDU, or Original-Broadcast-NPDU
queue.push(broadcast ? 0xb : 0xa);
// NPCI
ByteQueue postLength = new ByteQueue();
writeNpci(postLength, recipient, link, apdu);
// APDU
apdu.write(postLength);
// Length
BACnetUtils.pushShort(queue, queue.size() + postLength.size() + 2);
// Combine the queues
queue.push(postLength);
InetSocketAddress isa;
if (recipient.isGlobal())
isa = getLocalBroadcastAddress().getMacAddress().getInetSocketAddress();
else if (link != null)
isa = link.getInetSocketAddress();
else
isa = recipient.getMacAddress().getInetSocketAddress();
sendPacket(isa, queue.popAll());
}
private void sendPacket(InetSocketAddress addr, byte[] data) throws BACnetException {
try {
DatagramPacket packet = new DatagramPacket(data, data.length, addr);
socket.send(packet);
}
catch (Exception e) {
throw new BACnetException(e);
}
}
//
// For receiving
@Override
public void run() {
byte[] buffer = new byte[MESSAGE_LENGTH];
DatagramPacket p = new DatagramPacket(buffer, buffer.length);
while (!socket.isClosed()) {
try {
socket.receive(p);
ByteQueue queue = new ByteQueue(p.getData(), 0, p.getLength());
OctetString link = new OctetString(p.getAddress().getAddress(), p.getPort());
new IncomingMessageExecutor(this, queue, link).run();
p.setData(buffer);
}
catch (IOException e) {
// no op. This happens if the socket gets closed by the destroy method.
}
}
}
public void testDecoding(byte[] message) {
IncomingMessageExecutor ime = new IncomingMessageExecutor(null, new ByteQueue(message), null);
ime.run();
}
public APDU createApdu(byte[] message) throws Exception {
IncomingMessageExecutor ime = new IncomingMessageExecutor(null, new ByteQueue(message), null);
return ime.parseApdu();
}
class IncomingMessageExecutor extends IncomingRequestParser {
public IncomingMessageExecutor(Network network, ByteQueue queue, OctetString localFrom) {
super(network, queue, localFrom);
}
@Override
protected void parseFrame() throws MessageValidationAssertionException {
// Initial parsing of IP message.
// BACnet/IP
if (queue.pop() != (byte) 0x81)
throw new MessageValidationAssertionException("Protocol id is not BACnet/IP (0x81)");
byte function = queue.pop();
if (function != 0xa && function != 0xb && function != 0x4 && function != 0x0)
throw new MessageValidationAssertionException("Function is not unicast, broadcast, forward"
+ " or foreign device reg anwser (0xa, 0xb, 0x4 or 0x0)");
int length = BACnetUtils.popShort(queue);
if (length != queue.size() + 4)
throw new MessageValidationAssertionException("Length field does not match data: given=" + length
+ ", expected=" + (queue.size() + 4));
// answer to foreign device registration
if (function == 0x0) {
int result = BACnetUtils.popShort(queue);
if (result != 0)
System.out.println("Foreign device registration not successful! result: " + result);
// not APDU received, bail
return;
}
if (function == 0x4) {
// A forward. Use the address/port as the link service address.
byte[] addr = new byte[6];
queue.pop(addr);
linkService = new OctetString(addr);
}
}
}
//
//
// Convenience methods
//
public Address getAddress(InetAddress inetAddress) {
try {
return new Address(getLocalNetworkNumber(), inetAddress.getAddress(), port);
}
catch (Exception e) {
// Should never happen, so just wrap in a RuntimeException
throw new RuntimeException(e);
}
}
public static InetAddress getDefaultLocalInetAddress() throws UnknownHostException, SocketException {
for (NetworkInterface iface : Collections.list(NetworkInterface.getNetworkInterfaces())) {
for (InetAddress addr : Collections.list(iface.getInetAddresses())) {
if (!addr.isLoopbackAddress() && addr.isSiteLocalAddress())
return addr;
}
}
return InetAddress.getLocalHost();
}
@Override
public Address[] getAllLocalAddresses() {
try {
ArrayList<Address> result = new ArrayList<Address>();
for (NetworkInterface iface : Collections.list(NetworkInterface.getNetworkInterfaces())) {
for (InetAddress addr : Collections.list(iface.getInetAddresses())) {
if (!addr.isLoopbackAddress() && addr.isSiteLocalAddress())
result.add(getAddress(addr));
}
}
return result.toArray(new Address[result.size()]);
}
catch (Exception e) {
// Should never happen, so just wrap in a RuntimeException
throw new RuntimeException(e);
}
}
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + ((broadcastIp == null) ? 0 : broadcastIp.hashCode());
result = prime * result + ((localBindAddress == null) ? 0 : localBindAddress.hashCode());
result = prime * result + port;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (!super.equals(obj))
return false;
if (getClass() != obj.getClass())
return false;
IpNetwork other = (IpNetwork) obj;
if (broadcastIp == null) {
if (other.broadcastIp != null)
return false;
}
else if (!broadcastIp.equals(other.broadcastIp))
return false;
if (localBindAddress == null) {
if (other.localBindAddress != null)
return false;
}
else if (!localBindAddress.equals(other.localBindAddress))
return false;
if (port != other.port)
return false;
return true;
}
}