From 5612d29b74a1c5ded13955565cf1b48f2307813c Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Sun, 18 Sep 2022 11:31:41 -0700 Subject: Refactor G1RUS protocol --- .../org/traccar/protocol/G1rusFrameDecoder.java | 49 +++ .../java/org/traccar/protocol/G1rusProtocol.java | 18 +- .../org/traccar/protocol/G1rusProtocolDecoder.java | 412 +++++---------------- 3 files changed, 165 insertions(+), 314 deletions(-) create mode 100644 src/main/java/org/traccar/protocol/G1rusFrameDecoder.java (limited to 'src') diff --git a/src/main/java/org/traccar/protocol/G1rusFrameDecoder.java b/src/main/java/org/traccar/protocol/G1rusFrameDecoder.java new file mode 100644 index 000000000..8c67207ad --- /dev/null +++ b/src/main/java/org/traccar/protocol/G1rusFrameDecoder.java @@ -0,0 +1,49 @@ +/* + * Copyright 2022 Anton Tananaev (anton@traccar.org) + * + * 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.traccar.protocol; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import org.traccar.BaseFrameDecoder; + +public class G1rusFrameDecoder extends BaseFrameDecoder { + + @Override + protected Object decode( + ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception { + + ByteBuf result = Unpooled.buffer(); + + while (buf.isReadable()) { + int b = buf.readUnsignedByte(); + if (b == 0x1B) { + int ext = buf.readUnsignedByte(); + if (ext == 0x00) { + result.writeByte(0x1B); + } else { + result.writeByte(0xF8); + } + } else { + result.writeByte(b); + } + } + + return result; + } + +} diff --git a/src/main/java/org/traccar/protocol/G1rusProtocol.java b/src/main/java/org/traccar/protocol/G1rusProtocol.java index 0d77a8add..f1823762d 100644 --- a/src/main/java/org/traccar/protocol/G1rusProtocol.java +++ b/src/main/java/org/traccar/protocol/G1rusProtocol.java @@ -1,6 +1,20 @@ +/* + * Copyright 2022 Anton Tananaev (anton@traccar.org) + * + * 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.traccar.protocol; -import io.netty.handler.codec.string.StringDecoder; import io.netty.handler.codec.string.StringEncoder; import org.traccar.BaseProtocol; import org.traccar.PipelineBuilder; @@ -16,9 +30,11 @@ public class G1rusProtocol extends BaseProtocol { addServer(new TrackerServer(config, getName(), false) { @Override protected void addProtocolHandlers(PipelineBuilder pipeline, Config config) { + pipeline.addLast(new G1rusFrameDecoder()); pipeline.addLast(new StringEncoder()); pipeline.addLast(new G1rusProtocolDecoder(G1rusProtocol.this)); } }); } + } diff --git a/src/main/java/org/traccar/protocol/G1rusProtocolDecoder.java b/src/main/java/org/traccar/protocol/G1rusProtocolDecoder.java index b1b7230e1..e974e446d 100644 --- a/src/main/java/org/traccar/protocol/G1rusProtocolDecoder.java +++ b/src/main/java/org/traccar/protocol/G1rusProtocolDecoder.java @@ -1,20 +1,30 @@ +/* + * Copyright 2022 Anton Tananaev (anton@traccar.org) + * + * 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.traccar.protocol; -import com.google.common.primitives.Ints; -import com.google.common.primitives.Longs; -import com.google.common.primitives.Shorts; import io.netty.buffer.ByteBuf; import io.netty.channel.Channel; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.traccar.BaseProtocolDecoder; import org.traccar.Protocol; +import org.traccar.helper.BitUtil; import org.traccar.model.Position; -import org.traccar.session.ConnectionManager; import org.traccar.session.DeviceSession; import java.net.SocketAddress; -import java.text.SimpleDateFormat; +import java.nio.charset.StandardCharsets; import java.util.Date; import java.util.LinkedList; import java.util.List; @@ -24,352 +34,128 @@ public class G1rusProtocolDecoder extends BaseProtocolDecoder { super(protocol); } - private static final Logger LOGGER = LoggerFactory.getLogger(ConnectionManager.class); + public static final int MSG_HEARTBEAT = 0; + public static final int MSG_REGULAR = 1; + public static final int MSG_SMS_FORWARD = 2; + public static final int MSG_SERIAL = 3; + public static final int MSG_MIXED = 4; - /* Constants */ - private static final int G1RUS_HEAD_TAIL = 0xF8; - - private static final int G1RUS_TYPE_HEARTBEAT = 0; - - private static final int G1RUS_TYPE_BCD_MASK = 0b00111111; - private static final int G1RUS_TYPE_REGULAR = 1; - private static final int G1RUS_TYPE_SMS_FORWARD = 2; - private static final int G1RUS_TYPE_SERIAL_PASS_THROUGH = 3; - private static final int G1RUS_TYPE_MIXED = 4; - - private static final int G1RUS_TYPE_EVENT_MASK = 0b01000000; - private static final int G1RUS_TYPE_NON_EVENT = 0; - private static final int G1RUS_TYPE_EVENT = 1; - - private static final int G1RUS_TYPE_IMEI_MASK = 0b10000000; - private static final int G1RUS_TYPE_IMEI_LONG = 0; - private static final int G1RUS_TYPE_IMEI_SHORT = 1; - - private static final int G1RUS_DATA_SYS_MASK = 0b00000001; - private static final int G1RUS_DATA_GPS_MASK = 0b00000010; - private static final int G1RUS_DATA_GSM_MASK = 0b00000100; - private static final int G1RUS_DATA_COT_MASK = 0b00001000; - private static final int G1RUS_DATA_ADC_MASK = 0b00010000; - private static final int G1RUS_DATA_DTT_MASK = 0b00100000; - /* Reserved */ - private static final int G1RUS_DATA_ETD_MASK = 0b10000000; - - private static final int G1RUS_GPS_SIGN_MASK = 0b00000001; - private static final int G1RUS_GPS_POS_MASK = 0b00000010; - private static final int G1RUS_GPS_SPD_MASK = 0b00000100; - private static final int G1RUS_GPS_AZTH_MASK = 0b00001000; - private static final int G1RUS_GPS_ALT_MASK = 0b00010000; - private static final int G1RUS_GPS_HDOP_MASK = 0b00100000; - private static final int G1RUS_GPS_VDOP_MASK = 0b01000000; - private static final int G1RUS_GPS_STAT_MASK = 0b10000000; - - private static final int G1RUS_ADC_DATA_MASK = 0b0000111111111111; + private String readString(ByteBuf buf) { + int length = buf.readUnsignedByte() & 0xF; + return buf.readCharSequence(length, StandardCharsets.US_ASCII).toString(); + } - private static final int G1RUS_ESCAPE_CHAR = 0x1B; + private Position decodeRegular(DeviceSession deviceSession, ByteBuf buf, int type) { + Position position = new Position(getProtocolName()); + position.setDeviceId(deviceSession.getDeviceId()); + position.setTime(new Date((buf.readUnsignedIntLE() + 946684800) * 1000L)); - private short readUnsignedByteUnescaped(ByteBuf buf) { - short first = buf.readUnsignedByte(); - if (first != G1RUS_ESCAPE_CHAR) { - return first; - } else { /* first == 0x1B */ - byte second = (byte) buf.readUnsignedByte(); - if (second == 0x00) { - return first; - } else { /* second == 0xE3 */ - return (short) 0xF8; - } + if (BitUtil.check(type, 6)) { + position.set(Position.KEY_EVENT, buf.readUnsignedByte()); } - } + int dataMask = buf.readUnsignedShort(); - private void skipBytesUnescaped(ByteBuf buf, int howMany) { - for (int i = 0; i < howMany; ++i) { - readUnsignedByteUnescaped(buf); + if (BitUtil.check(dataMask, 0)) { + buf.readUnsignedByte(); // length + readString(buf); // device name + position.set(Position.KEY_VERSION_FW, readString(buf)); + position.set(Position.KEY_VERSION_HW, readString(buf)); } - } - - private void readBytesUnescaped(ByteBuf buf, byte[] to) { - for (int i = 0; i < to.length; ++i) { - to[i] = (byte) readUnsignedByteUnescaped(buf); + if (BitUtil.check(dataMask, 1)) { + buf.readUnsignedByte(); // length + int locationMask = buf.readUnsignedShort(); + if (BitUtil.check(locationMask, 0)) { + int validity = buf.readUnsignedByte(); + position.set(Position.KEY_SATELLITES, BitUtil.to(validity, 5)); + position.setValid(BitUtil.between(validity, 5, 7) == 2); + } + if (BitUtil.check(locationMask, 1)) { + position.setLatitude(buf.readInt() / 1000000.0); + position.setLongitude(buf.readInt() / 1000000.0); + } + if (BitUtil.check(locationMask, 2)) { + position.setSpeed(buf.readUnsignedShort()); + } + if (BitUtil.check(locationMask, 3)) { + position.setCourse(buf.readUnsignedShort()); + } + if (BitUtil.check(locationMask, 4)) { + position.setAltitude(buf.readShort()); + } + if (BitUtil.check(locationMask, 5)) { + position.set(Position.KEY_HDOP, buf.readUnsignedShort()); + } + if (BitUtil.check(locationMask, 6)) { + position.set(Position.KEY_VDOP, buf.readUnsignedShort()); + } } - } - private void readBytesUnescaped(ByteBuf buf, byte[] to, int dstIndex, int length) { - for (int i = dstIndex; i < length; ++i) { - to[i] = (byte) readUnsignedByteUnescaped(buf); + if (BitUtil.check(dataMask, 2)) { + buf.skipBytes(buf.readUnsignedByte()); } - } - - - private int readUnsignedShortUnescaped(ByteBuf buf) { - byte[] shortBuf = new byte[2]; - readBytesUnescaped(buf, shortBuf); - return Shorts.fromByteArray(shortBuf); - } - - private int readIntUnescaped(ByteBuf buf) { - byte[] intBuf = new byte[4]; - readBytesUnescaped(buf, intBuf); - return Ints.fromByteArray(intBuf); - } - - - private void decodeSYSSub(ByteBuf buf) { - LOGGER.debug(""); - - skipBytesUnescaped(buf, 1); /* Total length */ - - /* NOTE: assuming order: - * Device name -> Firmware version -> Hardware version. - * TODO: actually check it. - */ - - /* Device name */ - short devNameLen = readUnsignedByteUnescaped(buf); - byte[] devName = new byte[devNameLen & 0xF]; - readBytesUnescaped(buf, devName); - String devNameString = new String(devName); - LOGGER.debug("Device name: " + devNameString); - - /* Firmware version */ - short firmwareLen = readUnsignedByteUnescaped(buf); - byte[] firmware = new byte[firmwareLen & 0xF]; - readBytesUnescaped(buf, firmware); - String firmwareString = new String(firmware); - LOGGER.debug("Firmware version: " + firmwareString); - - /* Hardware version */ - short hardwareLen = readUnsignedByteUnescaped(buf); - byte[] hardware = new byte[hardwareLen & 0xF]; - readBytesUnescaped(buf, hardware); - String hardwareString = new String(hardware); - LOGGER.debug("Hardware version: " + hardwareString); - - LOGGER.debug(""); - } - - - private void decodeGPSSub(ByteBuf buf, Position position) { - LOGGER.debug(""); - - skipBytesUnescaped(buf, 1); /* Total length */ - - int subMask = readUnsignedShortUnescaped(buf); - if ((subMask & G1RUS_GPS_SIGN_MASK) == G1RUS_GPS_SIGN_MASK) { - short signValid = readUnsignedByteUnescaped(buf); - LOGGER.debug("Fix sign: " + ((signValid & 0b1100000) >> 5)); - LOGGER.debug("Satellite number: " + (signValid & 0b0011111)); - position.setValid(((signValid & 0b1100000) >> 5) == 2); - position.set(Position.KEY_SATELLITES, signValid & 0b0011111); - } - if ((subMask & G1RUS_GPS_POS_MASK) == G1RUS_GPS_POS_MASK) { - byte[] posBuf = new byte[4]; - readBytesUnescaped(buf, posBuf); - position.setLatitude((float) Ints.fromByteArray(posBuf) / 1000000); - LOGGER.debug("Latitude: " + position.getLatitude()); - - readBytesUnescaped(buf, posBuf); - position.setLongitude((float) Ints.fromByteArray(posBuf) / 1000000); - LOGGER.debug("Longitude: " + position.getLongitude()); - } - if ((subMask & G1RUS_GPS_SPD_MASK) == G1RUS_GPS_SPD_MASK) { - position.setSpeed(readUnsignedShortUnescaped(buf)); - LOGGER.debug("Speed: " + position.getSpeed()); + if (BitUtil.check(dataMask, 3)) { + buf.skipBytes(buf.readUnsignedByte()); } - if ((subMask & G1RUS_GPS_AZTH_MASK) == G1RUS_GPS_AZTH_MASK) { - position.setCourse(readUnsignedShortUnescaped(buf)); - LOGGER.debug("Course: " + position.getCourse()); - } - if ((subMask & G1RUS_GPS_ALT_MASK) == G1RUS_GPS_ALT_MASK) { - position.setAltitude(readUnsignedShortUnescaped(buf)); - LOGGER.debug("Altitude: " + position.getAltitude()); - } - if ((subMask & G1RUS_GPS_HDOP_MASK) == G1RUS_GPS_HDOP_MASK) { - position.set(Position.KEY_HDOP, readUnsignedShortUnescaped(buf)); - LOGGER.debug("HDOP: " + position.getAttributes().get(Position.KEY_HDOP)); - } - if ((subMask & G1RUS_GPS_VDOP_MASK) == G1RUS_GPS_VDOP_MASK) { - position.set(Position.KEY_VDOP, readUnsignedShortUnescaped(buf)); - LOGGER.debug("VDOP: " + position.getAttributes().get(Position.KEY_VDOP)); - } - - LOGGER.debug(""); - } - - - private int getADValue(int rawValue) { - final int AD_MIN = -10; - final int AD_MAX = 100; - - return rawValue * (AD_MAX - AD_MIN) / 4096 + AD_MIN; - } - - - private void decodeADCSub(ByteBuf buf, Position position) { - LOGGER.debug(""); - - skipBytesUnescaped(buf, 1); - - /* NOTE: assuming order: - * External battery voltage -> Backup battery voltage -> Device temperature voltage. - * TODO: actually check this. - */ - - int externalVoltage = readUnsignedShortUnescaped(buf) & G1RUS_ADC_DATA_MASK; - LOGGER.debug("External voltage: " + getADValue(externalVoltage) + "V [" + externalVoltage + "]"); - - int backupVoltage = readUnsignedShortUnescaped(buf) & G1RUS_ADC_DATA_MASK; - LOGGER.debug("Backup voltage: " + getADValue(backupVoltage) + "V [" + backupVoltage + "]"); - position.set(Position.KEY_BATTERY, getADValue(backupVoltage)); - - int temperature = readUnsignedShortUnescaped(buf) & G1RUS_ADC_DATA_MASK; - LOGGER.debug("Device temperature: " + getADValue(temperature) + "°C [" + temperature + "]"); - position.set(Position.KEY_DEVICE_TEMP, getADValue(temperature)); - LOGGER.debug(""); - } - - - private Position decodeRegular(Channel channel, SocketAddress remoteAddress, ByteBuf buf, long imei, short packetType) { - int timestamp_ = readIntUnescaped(buf); - long timestamp = (946684800 + timestamp_) * 1000L; /* Convert received time to proper UNIX timestamp */ - LOGGER.debug("Date and time: " + new SimpleDateFormat("yyyy.MM.dd HH:mm:ss").format(new Date(timestamp))); - - if ((packetType & G1RUS_TYPE_EVENT_MASK) != G1RUS_TYPE_NON_EVENT) { - skipBytesUnescaped(buf, 1); /* Event ID */ + if (BitUtil.check(dataMask, 4)) { + buf.readUnsignedByte(); // length + position.set(Position.KEY_POWER, buf.readUnsignedShort() * 110 / 4096 - 10); + position.set(Position.KEY_BATTERY, buf.readUnsignedShort() * 110 / 4096 - 10); + position.set(Position.KEY_DEVICE_TEMP, buf.readUnsignedShort() * 110 / 4096 - 10); } - DeviceSession deviceSession = null; - Position position = null; - - int dataUploadingMask = readUnsignedShortUnescaped(buf); - if ((dataUploadingMask & G1RUS_DATA_SYS_MASK) == G1RUS_DATA_SYS_MASK) { - decodeSYSSub(buf); + if (BitUtil.check(dataMask, 5)) { + buf.skipBytes(buf.readUnsignedByte()); } - if ((dataUploadingMask & G1RUS_DATA_GPS_MASK) == G1RUS_DATA_GPS_MASK) { - deviceSession = getDeviceSession(channel, remoteAddress, String.valueOf(imei)); - if (deviceSession == null) { - return null; - } - position = new Position(getProtocolName()); - position.setDeviceId(deviceSession.getDeviceId()); - position.setTime(new Date(timestamp)); - decodeGPSSub(buf, position); - } - if ((dataUploadingMask & G1RUS_DATA_GSM_MASK) == G1RUS_DATA_GSM_MASK) { - skipBytesUnescaped(buf, readUnsignedByteUnescaped(buf)); - } - if ((dataUploadingMask & G1RUS_DATA_COT_MASK) == G1RUS_DATA_COT_MASK) { - skipBytesUnescaped(buf, readUnsignedByteUnescaped(buf)); - } - if ((dataUploadingMask & G1RUS_DATA_ADC_MASK) == G1RUS_DATA_ADC_MASK) { - if (deviceSession == null) { - skipBytesUnescaped(buf, readUnsignedByteUnescaped(buf)); - } else { - decodeADCSub(buf, position); - } - } - if ((dataUploadingMask & G1RUS_DATA_DTT_MASK) == G1RUS_DATA_DTT_MASK) { - skipBytesUnescaped(buf, readUnsignedByteUnescaped(buf)); - } - if ((dataUploadingMask & G1RUS_DATA_ETD_MASK) == G1RUS_DATA_ETD_MASK) { - skipBytesUnescaped(buf, readUnsignedByteUnescaped(buf)); + if (BitUtil.check(dataMask, 7)) { + buf.skipBytes(buf.readUnsignedByte()); } return position; } - - private Object decodeSMSForward(ByteBuf buf) { - return null; - } - - - private Object decodeSerialPassThrough(ByteBuf buf) { - return null; - } - - - private void printPacketType(short packetType) { - LOGGER.debug("Packet type: " + (packetType == G1RUS_TYPE_HEARTBEAT ? "HEARTBEAT" : - "[" + ((packetType & G1RUS_TYPE_IMEI_MASK) == G1RUS_TYPE_IMEI_LONG ? "IMEI_LONG" : "IMEI_SHORT") + "]" + - "[" + ((packetType & G1RUS_TYPE_EVENT_MASK) == G1RUS_TYPE_NON_EVENT ? "NON-EVENT" : "EVENT") + "]" + - "[" + ((packetType & G1RUS_TYPE_BCD_MASK) == G1RUS_TYPE_REGULAR ? "REGULAR" : (packetType & G1RUS_TYPE_BCD_MASK) == G1RUS_TYPE_SMS_FORWARD ? "SMS FORWARD" : (packetType & G1RUS_TYPE_BCD_MASK) == G1RUS_TYPE_SERIAL_PASS_THROUGH ? "PASS THROUGH" : (packetType & G1RUS_TYPE_BCD_MASK) == G1RUS_TYPE_MIXED ? "MIXED PACKED" : "RESERVED/INVALID") + "]")); - } - - @Override protected Object decode(Channel channel, SocketAddress remoteAddress, Object msg) throws Exception { + ByteBuf buf = (ByteBuf) msg; - if (buf.readUnsignedByte() != G1RUS_HEAD_TAIL) { + int type = buf.readUnsignedByte(); + String imei = String.valueOf(buf.readLong()); + DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, imei); + if (deviceSession == null) { return null; } - LOGGER.debug("Protocol version: " + readUnsignedByteUnescaped(buf)); - - short packetType = readUnsignedByteUnescaped(buf); - printPacketType(packetType); - - byte[] imei = new byte[8]; - readBytesUnescaped(buf, imei, 0, 7); - long imeiLong = Longs.fromByteArray(imei); - LOGGER.debug("IMEI: " + imeiLong); + if (BitUtil.to(type, 6) == MSG_REGULAR) { - List positions = null; + return decodeRegular(deviceSession, buf, type); - if (packetType == G1RUS_TYPE_HEARTBEAT) { - return null; - } else if ((packetType & G1RUS_TYPE_BCD_MASK) == G1RUS_TYPE_REGULAR) { - positions = new LinkedList<>(); - Position position = decodeRegular(channel, remoteAddress, buf, imeiLong, packetType); - if (position != null) { - positions.add(position); - } - } else if ((packetType & G1RUS_TYPE_BCD_MASK) == G1RUS_TYPE_SMS_FORWARD) { - return decodeSMSForward(buf); - } else if ((packetType & G1RUS_TYPE_BCD_MASK) == G1RUS_TYPE_SERIAL_PASS_THROUGH) { - return decodeSerialPassThrough(buf); - } else if ((packetType & G1RUS_TYPE_BCD_MASK) == G1RUS_TYPE_MIXED) { - positions = new LinkedList<>(); + } else if (BitUtil.to(type, 6) == MSG_MIXED) { + List positions = new LinkedList<>(); while (buf.readableBytes() > 5) { - int subPacketLength = readUnsignedShortUnescaped(buf); - short subPacketType = readUnsignedByteUnescaped(buf); - printPacketType(subPacketType); - - if ((subPacketType & G1RUS_TYPE_BCD_MASK) == G1RUS_TYPE_REGULAR) { - Position position = decodeRegular(channel, remoteAddress, buf, imeiLong, subPacketType); - if (position != null) { - positions.add(position); - } + int length = buf.readUnsignedShort(); + int subtype = buf.readUnsignedByte(); + if (BitUtil.to(subtype, 6) == MSG_REGULAR) { + positions.add(decodeRegular(deviceSession, buf, subtype)); } else { - skipBytesUnescaped(buf, subPacketLength - 1); + buf.skipBytes(length); } - /* else if ((subPacketType & G1RUS_TYPE_BCD_MASK) == G1RUS_TYPE_SMS_FORWARD) { - skipBytesUnescaped(buf, subPacketLength - 1); - *//*decodeSMSForward(buf);*//* - } else if ((subPacketType & G1RUS_TYPE_BCD_MASK) == G1RUS_TYPE_SERIAL_PASS_THROUGH) { - skipBytesUnescaped(buf, subPacketLength - 1); - *//*decodeSerialPassThrough(buf);*//* - }*/ } - } else { - LOGGER.error("Unknown packet type!"); - } + return positions.isEmpty() ? null : positions; - skipBytesUnescaped(buf, 2); /* CRC */ /* TODO: actually check it */ - short tail = buf.readUnsignedByte(); - if (tail == G1RUS_HEAD_TAIL) { - LOGGER.debug("Tail: OK"); - } else { - LOGGER.error("Tail: FAIL!"); } - return positions; + buf.skipBytes(2); + buf.readUnsignedByte(); // tail + + return null; + } + } -- cgit v1.2.3