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.model.Position;
import org.traccar.session.ConnectionManager;
import org.traccar.session.DeviceSession;
import java.net.SocketAddress;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
public class G1rusProtocolDecoder extends BaseProtocolDecoder {
public G1rusProtocolDecoder(Protocol protocol) {
super(protocol);
}
private static final Logger LOGGER = LoggerFactory.getLogger(ConnectionManager.class);
/* 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 static final int G1RUS_ESCAPE_CHAR = 0x1B;
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;
}
}
}
private void skipBytesUnescaped(ByteBuf buf, int howMany) {
for (int i = 0; i < howMany; ++i) {
readUnsignedByteUnescaped(buf);
}
}
private void readBytesUnescaped(ByteBuf buf, byte[] to) {
for (int i = 0; i < to.length; ++i) {
to[i] = (byte) readUnsignedByteUnescaped(buf);
}
}
private void readBytesUnescaped(ByteBuf buf, byte[] to, int dstIndex, int length) {
for (int i = dstIndex; i < length; ++i) {
to[i] = (byte) readUnsignedByteUnescaped(buf);
}
}
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 ((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 */
}
DeviceSession deviceSession = null;
Position position = null;
int dataUploadingMask = readUnsignedShortUnescaped(buf);
if ((dataUploadingMask & G1RUS_DATA_SYS_MASK) == G1RUS_DATA_SYS_MASK) {
decodeSYSSub(buf);
}
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));
}
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) {
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);
List positions = null;
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<>();
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);
}
} else {
skipBytesUnescaped(buf, subPacketLength - 1);
}
/* 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!");
}
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;
}
}