All Downloads are FREE. Search and download functionalities are using the official Maven repository.

tuwien.auto.calimero.link.medium.RFLData Maven / Gradle / Ivy

The newest version!
/*
    Calimero 2 - A library for KNX network access
    Copyright (c) 2015, 2023 B. Malinowsky

    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 2 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, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

    Linking this library statically or dynamically with other modules is
    making a combined work based on this library. Thus, the terms and
    conditions of the GNU General Public License cover the whole
    combination.

    As a special exception, the copyright holders of this library give you
    permission to link this library with independent modules to produce an
    executable, regardless of the license terms of these independent
    modules, and to copy and distribute the resulting executable under terms
    of your choice, provided that you also meet, for each linked independent
    module, the terms and conditions of the license of that module. An
    independent module is a module which is not derived from or based on
    this library. If you modify this library, you may extend this exception
    to your version of the library, but you are not obligated to do so. If
    you do not wish to do so, delete this exception statement from your
    version.
*/

package tuwien.auto.calimero.link.medium;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;

import tuwien.auto.calimero.DataUnitBuilder;
import tuwien.auto.calimero.GroupAddress;
import tuwien.auto.calimero.IndividualAddress;
import tuwien.auto.calimero.KNXAddress;
import tuwien.auto.calimero.KNXFormatException;
import tuwien.auto.calimero.LteHeeTag;
import tuwien.auto.calimero.cemi.RFMediumInfo.RSS;


/**
 * L-Data frame format on KNX RF communication medium.
 *
 * @author B. Malinowsky
 */
public class RFLData implements RawFrame
{
	// RawFrameBase is right now not the best fit as base type, because
	// RF frames do not use the priority field

	// Fast Ack frame: min length of having 1. and 2. block = [10 + CRC] [6 + CRC]
	// BiBat Sync frame: min length of having 1. and 2. block = [10 + CRC] [7 + CRC]
	// LTE HEE frame: min length of having 1. and 2. block = [10 + CRC] [12 + CRC]
	private static final int MinLength = 20;
	// magic value for future extensions
	private static final int ReservedLength = 0xff;
	// the KNX RF frame type
	private static final int Send_NoReply = 0x44;

	private static final int Escape = 0xff;

	private static final int TpduOffset = 15;
	private static final int Block2TpduSize = 10;

	/** RF frame TPCI. */
	public enum Tpci {
		UnnumberedData,
		NumberedData,
		UnnumberedCtrl,
		NumberedCtrl,
	}

	// TODO extend with other frame types, or remove
	// Indicates the frame type _group_ (not the actual frame type)
	enum FrameType {
		AsyncLData,
		RfMultiAsyncLData,
		RfMultiAsyncLDataAckReq,
		FastAck,
	}

	// length of frame starting from the C field, excluding CRCs
	private final int length;

	private final RSS rss;
	private final boolean batteryOk;
	private final boolean unidir;

	private final int ctrl;
//	private FrameType ft;

	/*
	   RF domain address is used for
	     - point to point (unicast), CL/CO
	     - point to all points (domain broadcast), CL
	   KNX device serial number
	     - point to multi point (multicast), CL
	     - point to system (system broadcast), CL
	*/

	private final byte[] doa;
	private final IndividualAddress src;
	private final KNXAddress dst;

	// counter shall be 6 for RF Ready and BiBat end devices
	// counter shall be 2 for RF multi end devices
	private final int maxRep;
	private final int lfn;
	private final boolean isDoA;
	private final byte[] tpdu;

	// transmit-only devices
	//   - shall all have IndAddr 0x05ff
	//   - shall use extended group addresses
	//   - datapoints shall be numbered as DP1 = GroupAddr 0x0001, DP2 = 0x0002, ...
	static RFLData newForTransmitOnlyDevice(final boolean batteryOk, final int frameType,
		final int frameNumber, final byte[] serial, final GroupAddress dst, final byte[] tpdu)
	{
		return new RFLData(batteryOk, true, frameType, frameNumber, serial, new IndividualAddress(
				0x05ff), dst, tpdu);
	}

	public RFLData(final boolean batteryOk, final boolean transmitOnlyDevice, final int frameType,
		final int frameNumber, final byte[] doa, final IndividualAddress src, final KNXAddress dst,
		final byte[] tpdu)
	{
		length = TpduOffset + tpdu.length;
		rss = RSS.Void;
		this.batteryOk = batteryOk;
		unidir = transmitOnlyDevice;
		this.doa = doa.clone();

		ctrl = frameType;
		this.src = src;
		this.dst = dst;
		maxRep = 6;
		lfn = frameNumber;

		final boolean grpbcast = dst instanceof GroupAddress && dst.getRawAddress() == 0;
		this.isDoA = grpbcast || dst instanceof IndividualAddress;

		this.tpdu = tpdu.clone();
	}

	public RFLData(final byte[] frame, final int offset) throws KNXFormatException
	{
		final ByteArrayInputStream is = new ByteArrayInputStream(frame, offset, frame.length - offset);

		// first data block has 10 bytes, following blocks contain 16 bytes, but last block may have less than 16 bytes
		if (is.available() < MinLength)
			throw new KNXFormatException("minimum data length < " + MinLength, is.available());

		//
		// 1st block, 10 octets
		//

		// frame _data_ length
		length = is.read();
		if (length == ReservedLength)
			throw new KNXFormatException("unsupported RF frame length", ReservedLength);

		final int c = is.read();
		if (c != Send_NoReply)
			throw new KNXFormatException("no KNX RF L-Data frame");

		final int esc = is.read();
		if (esc != Escape)
			throw new KNXFormatException("invalid Escape field", esc);

		// RF info
		final int info = is.read();
		final int hi = info & 0xf0;
		if (hi != 0)
			throw new KNXFormatException("invalid RF info field", info);
		final int rssvalue = (info >>> 2) & 0x03;
		rss = RSS.values()[rssvalue];
		batteryOk = (info & 0x02) == 0x02;
		unidir = (info & 0x01) == 0x01;

		doa = new byte[6];
		is.read(doa, 0, doa.length);

		// 1st block CRC
		final int crc1 = (is.read() << 8) | is.read();
		verifyCrc(crc1, frame, offset, 10);

		//
		// 2nd block, max. 16 octets
		//

		// KNX control field, contains the frame type
		ctrl = is.read();
		if (ctrl == Escape)
			throw new KNXFormatException("unsupported KNX control field (Escape)");
//		final int format = (ctrl >>> 4) & 0xf;
		// check sync/async frame
//		final boolean syncFrame = (ctrl >>> 6) == 1;
		// RF Multi frame
//		final boolean rfmulti = (ctrl >>> 7) == 1;
		final int extFormat = ctrl & 0xf;
		// check standard frame and LTE extended frame
		final boolean std = extFormat == 0;
		final boolean lteExt = (extFormat & 0x0c) == 0x04;
		if (!std && !lteExt)
			throw new KNXFormatException("unsupported frame format", extFormat);

		// KNX source address
		final byte[] addr = new byte[2];
		is.read(addr, 0, 2);
		src = new IndividualAddress(addr);

		// KNX destination address
		// read dst field, Ind/Group address is created below
		is.read(addr, 0, 2);

		// LPCI
		final int lpci = is.read();
		// is dst a group address for std frames
		final boolean group = (lpci & 0x80) == 0x80;
		// max allowed frame repetitions
		maxRep = (lpci >>> 4) & 0x07;
		lfn = (lpci >>> 1) & 0x07;
		isDoA = (lpci & 0x01) == 0x01;

		dst = group ? new GroupAddress(addr) : new IndividualAddress(addr);

		// allocate array for complete TPDU
		final int tpduSize = length - TpduOffset;
		if (tpduSize < 0)
			throw new KNXFormatException("invalid RF L-Data length, TPDU size < 0", length);
		tpdu = new byte[tpduSize];
		// read TPDU contained in 2nd block
		is.read(tpdu, 0, Math.min(Block2TpduSize, tpduSize));

		final int pci = tpdu[0] & 0xff;
		final int tpci = (pci >>> 6);
		// LTE has tpci always set 0
		if (lteExt && tpci != Tpci.UnnumberedData.ordinal())
			throw new KNXFormatException("RF LTE extended frame requires TPCI " + Tpci.UnnumberedData);

		// for LTE, seq is fixed to 1
//		final int seq = (pci >>> 2) & 0x0f;


		// 2nd block CRC
		final int crc2 = (is.read() << 8) | is.read();
		final int block2Size = Math.min(Block2TpduSize, tpduSize) + 6;
		verifyCrc(crc2, frame, offset + 12, block2Size);

		//
		// 3rd block ...
		//

		// read all the remaining blocks of [16 bytes data, 2 bytes CRC]
		// 1st block counts for 9 bytes (12 - 1 - CRC), 2nd block for 14 (16 - CRC)
		int block = 3;
		for (int got = Block2TpduSize; got < tpduSize; got += 16) {
			// last block may contain less than 16 bytes
			final int read = Math.min(16, tpduSize - got);
			final byte[] part = new byte[read];
			final int res = is.read(part, 0, part.length);
			if (res != read)
				throw new KNXFormatException("truncated RF frame in block " + block + ": length "
						+ got + " < expected total length " + length + " bytes");
			System.arraycopy(part, 0, tpdu, got, read);

			final int crcn = (is.read() << 8) | is.read();
			verifyCrc(crcn, frame, offset + (block - 1) * 18 - 6, read);
			block++;
		}
	}

	/**
	 * Returns the KNX individual source address.
	 *
	 * @return address of type IndividualAddress
	 */
	public final IndividualAddress getSource()
	{
		return src;
	}

	/**
	 * Returns the KNX destination address.
	 *
	 * @return destination address of type KNXAddress
	 */
	public final KNXAddress getDestination()
	{
		return dst;
	}

	@Override
	public final int getFrameType()
	{
		return ctrl;
	}

	public final RSS getRss()
	{
		return rss;
	}

	public final boolean isBatteryOk()
	{
		return batteryOk;
	}

	public final boolean isTransmitOnlyDevice()
	{
		return unidir;
	}

	// not part of the actual RF medium information structure
	public boolean isSystemBroadcast()
	{
		return !isDoA;
	}

	// SN or DoA according to System Broadcast flag
	// ??? maybe make two methods with dedicated names
	public final byte[] getDoAorSN()
	{
		return doa.clone();
	}

	/**
	 * {@return the data link layer frame number (LFN)}
	 */
	public final int getFrameNumber()
	{
		return lfn;
	}

	/**
	 * Returns a copy of the TPDU.
	 *
	 * @return TPDU as byte array
	 */
	public final byte[] getTpdu()
	{
		return tpdu.clone();
	}

	public final byte[] toByteArray()
	{
		final ByteArrayOutputStream os = new ByteArrayOutputStream();

		// 1st block
		os.write(length);
		os.write(Send_NoReply);
		os.write(Escape);

		final int info = (rss.ordinal() << 2) | (batteryOk ? 0x02 : 0x00) | (unidir ? 0x01 : 0x00);
		os.write(info);

		os.write(doa, 0, doa.length);
		os.write(crc(os.toByteArray(), 0), 0, 2);

		// 2nd block
		os.write(ctrl);
		os.write(src.toByteArray(), 0, 2);
		os.write(dst.toByteArray(), 0, 2);

		int lpci = dst instanceof GroupAddress ? 0x80 : 0x00;
		lpci |= (maxRep << 4) | (lfn << 1) | (isDoA ? 0x01 : 0x00);
		os.write(lpci);

		// fill remainder of 2nd block with TPDU
		final int min = Math.min(Block2TpduSize, tpdu.length);
		os.write(tpdu, 0, min);
		for (int i = min; i < Block2TpduSize; i++)
			os.write(0);
		final byte[] crc2 = crc(os.toByteArray(), 12);
		os.write(crc2, 0, 2);

		// 3rd block ...
		for (int written = Block2TpduSize; written < tpdu.length; written += 16) {
			final int write = Math.min(16, tpdu.length - written);
			os.write(tpdu, written, write);
			final byte[] crcn = crc(tpdu, written, write);
			os.write(crcn, 0, 2);

		}
		return os.toByteArray();
	}

	@Override
	public String toString()
	{
		final StringBuilder sb = new StringBuilder();

		final boolean lteExt = (ctrl & 0x0c) == 0x04;
		if (lteExt)
			sb.append("LTE ");
		sb.append(getFrameType(ctrl >>> 4));
		sb.append(" ").append(src).append("->");
		if (lteExt)
			sb.append(LteHeeTag.from(ctrl, (GroupAddress) dst));
		else
			sb.append(dst);

		sb.append(isSystemBroadcast() ? " SN " : " DoA ").append(
				DataUnitBuilder.toHex(getDoAorSN(), ""));
		sb.append(", RSS=").append(getRss());
		sb.append(" Battery ").append(isBatteryOk() ? "OK" : "weak");
		sb.append(", LFN ").append(getFrameNumber());

		sb.append(": ").append(DataUnitBuilder.toHex(tpdu, ""));
		return sb.toString();
	}

	private static String getFrameType(final int format)
	{
		return switch (format) {
			case 0 -> "L-Data (async)";
			case 1 -> "Fast ACK";
			case 4 -> "L-Data (sync)";
			case 5 -> "BiBat Sync";
			case 6 -> "BiBat Help Call";
			case 7 -> "BiBat Help Call Res";
			case 8 -> "RF Multi L-Data (async)";
			case 9 -> "RF Multi L-Data (async, ACK.req)";
			case 10 -> "RF Multi Repeater ACK";
			default -> "Reserved";
		};
	}

	private static void verifyCrc(final int crc, final byte[] data, final int offset,
		final int length) throws KNXFormatException
	{
		final int calc = crc16(data, offset, length);
		if (calc != crc)
			throw new KNXFormatException("CRC mismatch, expected 0x" + Integer.toHexString(crc)
					+ " vs calculated 0x" + Integer.toHexString(calc));
	}

	private static byte[] crc(final byte[] data, final int offset)
	{
		return crc(data, offset, data.length - offset);
	}

	private static byte[] crc(final byte[] data, final int offset, final int length)
	{
		final int crc = crc16(data, offset, length);
		return new byte[] { (byte) (crc >> 8), (byte) crc };
	}

	static int crc16(final byte[] data, final int offset, final int length)
	{
		// CRC-16-DNP
		// generator polynomial = 2^16 + 2^13 + 2^12 + 2^11 + 2^10 + 2^8 + 2^6 + 2^5 + 2^2 + 2^0
//		final int p = Integer.parseUnsignedInt("10011110101100101", 2);
//		System.out.println(Integer.toHexString(p));
		final int pn = 0x13d65; // 1 0011 1101 0110 0101

		// for much data, using a lookup table would be a way faster CRC calculation
		int crc = 0;
		for (int i = offset; i < offset + length; i++) {
			final int bite = data[i] & 0xff;
			for (int b = 8; b --> 0;) {
				final boolean bit = ((bite >> b) & 1) == 1;
				final boolean one = (crc >> 15 & 1) == 1;
				crc <<= 1;
				if (one ^ bit)
					crc ^= pn;
			}
		}
		return (~crc) & 0xffff;
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy