Omni protocol implementation

rgm2 years ago

Hi,
I'm implementing a protocol called Omni, this protocol is very similar to Xrb28, when i send a command everithing works great and the command is received and executed by the device, when the device send a message like "i'm unlocked", it expects an answer, and this response is sendend by protocol decoder. The format of the response is correct, in fact if i send the hex of the string that is printed in the log file from a tool called hercules it works fine, but from the traccar backend with the same string it doesn't work, and the device continue to send the same message in loop.
This is the code of OmniProtocolDecoder:

package org.traccar.protocol;

import io.netty.channel.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.traccar.BaseProtocolDecoder;
import org.traccar.NetworkMessage;
import org.traccar.Protocol;
import org.traccar.helper.DateBuilder;
import org.traccar.helper.Parser;
import org.traccar.helper.PatternBuilder;
import org.traccar.model.Command;
import org.traccar.model.Position;
import org.traccar.model.PositionExt;
import org.traccar.session.DeviceSession;

import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.time.LocalDateTime;

import java.net.SocketAddress;
import java.util.regex.Pattern;

public class OmniProtocolDecoder extends BaseProtocolDecoder {

    private static final Logger LOGGER = LoggerFactory.getLogger(OmniProtocolDecoder.class);

    private String pendingCommand;

    public void setPendingCommand(String pendingCommand) {
        this.pendingCommand = pendingCommand;
    }

    public  OmniProtocolDecoder(Protocol protocol) {
        super(protocol);
    }

    private static final Pattern PATTERN = new PatternBuilder()
            .text("*")
            .expression("....,")
            .expression("..,")                   // vendor
            .number("d{15},")                    // imei
            .number("(dd)(dd)(dd)(dd)(dd)(dd),") // dateUnix (yyMMddhhmmss)
            .expression("..,")                   // type
            .number("[01],")                     // reserved
            .number("(dd)(dd)(dd).d+,")          // time (hhmmss)
            .expression("([AV]),")               // validity
            .number("(dd)(dd.d+),")              // latitude
            .expression("([NS]),")
            .number("(d{2,3})(dd.d+),")          // longitude
            .expression("([EW]),")
            .number("(d+),")                     // satellites
            .number("(d+.d+),")                  // hdop
            .number("(dd)(dd)(dd),")             // date (ddmmyy)
            .number("(-?d+.?d*),")               // altitude
            .expression(".,")                    // height unit
            .expression(".#")                    // mode
            .compile();

    private static final Pattern PATTERNWITHERROR = new PatternBuilder()
            .text("*")
            .expression("....,")
            .expression("..,")                   // vendor
            .number("d{15},")                    // imei
            .number("(dd)(dd)(dd)(dd)(dd)(dd),") // dateUnix (yyMMddhhmmss)
            .expression("..,")                   // type
            .number("[01],")                     // reserved
            .number("(dd)(dd)(dd).d+,")          // time (hhmmss)
            .expression("([AV]),")               // validity
            .number("(dd)(dd.d+),")              // latitude
            .expression("([NS]),")
            .number("(d{2,3})(dd.d+),")          // longitude
            .expression("([EW]),")
            .number("(d+),")                     // satellites
            .number("(d+.d+),")                  // hdop
            .expression(",")                     // dateError
            .number("(-?d+.?d*),")               // altitude
            .expression(".,")                    // height unit
            .expression(".#")                    // mode
            .compile();

    @Override
    protected Object decode(Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
        String sentence = (String) msg;

        DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, sentence.substring(9, 24));
        if (deviceSession == null) {
            return null;
        }

        String type = sentence.substring(38, 40);
        if (channel != null) {
            if (type.matches("L0|L1|W0")) {
                DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyMMddHHmmss");
                LocalDateTime now = LocalDateTime.now();
                if (type.equals("W0")) {
                    channel.write(new NetworkMessage(
                            "\u00ff\u00ff*CMDS" + sentence.substring(5, 25) + dtf.format(now) +  sentence.substring(37, 40) + "#\n",
                            remoteAddress));
                } else {
                    LOGGER.info("\u00ff\u00ff*CMDS" + sentence.substring(5, 25) + dtf.format(now) + ",Re" +  sentence.substring(37, 40) + "#\n");
                    channel.write(new NetworkMessage(
                            "\u00ff\u00ff*CMDS" + sentence.substring(5, 25) + dtf.format(now) + ",Re" +  sentence.substring(37, 40) + "#\n",
                            remoteAddress));
                }

            } else if (type.equals("R0") && pendingCommand != null) {
                String command = pendingCommand.equals(Command.TYPE_ALARM_ARM) ? "L1," : "L0,";
                channel.write(new NetworkMessage(
                        "\u00ff\u00ff*CMDS" + sentence.substring(5, 25) + command + sentence.substring(30) + "\n",
                        remoteAddress));
                pendingCommand = null;
            }
        }

        if (!type.equals("D0")) {

            Position position = new Position(getProtocolName());
            position.setDeviceId(deviceSession.getDeviceId());

            getLastLocation(position, null);

            String payload = sentence.substring(38, sentence.length() - 1);

            int index = 0;
            String[] values = payload.substring(3).split(",");

            switch (type) {
                case "Q0":
                    position.set(Position.KEY_BATTERY, Integer.parseInt(values[index++]) * 0.01);
                    position.set(Position.KEY_BATTERY_LEVEL, Integer.parseInt(values[index++]));
                    break;
                case "H0":
                    position.set(Position.KEY_BLOCKED, Integer.parseInt(values[index++]) > 0);
                    position.set(Position.KEY_BATTERY, Integer.parseInt(values[index++]) * 0.01);
                    position.set(Position.KEY_RSSI, Integer.parseInt(values[index++]));
                    position.set(Position.KEY_STATUS, Integer.parseInt(values[index++]));
                    position.set(Position.KEY_BATTERY_LEVEL, Integer.parseInt(values[index++]));
                    break;
                case "S5":
                    position.set(Position.KEY_BATTERY, Integer.parseInt(values[index++]) * 0.01);
                    position.set(Position.KEY_RSSI, Integer.parseInt(values[index++]));
                    position.set(Position.KEY_SATELLITES, Integer.parseInt(values[index++]));
                    position.set(Position.KEY_BLOCKED, Integer.parseInt(values[index++]) > 0);
                    switch (Integer.parseInt(values[index++])) {
                        case 1:
                            position.set(Position.KEY_ALARM, Position.ALARM_MOVEMENT);
                            break;
                        case 2:
                            position.set(Position.KEY_ALARM, Position.ALARM_FALL_DOWN);
                            break;
                        case 3:
                            position.set(Position.KEY_ALARM, Position.ALARM_LOW_BATTERY);
                            break;
                        default:
                            break;
                    }
                    position.set(Position.KEY_BATTERY_LEVEL, Integer.parseInt(values[index++]));
                    break;
                case "W0":
                    switch (Integer.parseInt(values[index++])) {
                        case 1:
                            position.set(Position.KEY_ALARM, PositionExt.ALARM_CONTROLLER_FAILURE);
                            break;
                        case 3:
                            position.set(Position.KEY_ALARM, PositionExt.ALARM_COMMUNICATION_FAILURE);
                            break;
                        case 4:
                            position.set(Position.KEY_ALARM, PositionExt.ALARM_MOTOR_HALL_FAILURE);
                            break;
                        case 6:
                            position.set(Position.KEY_ALARM, Position.ALARM_LOW_BATTERY);
                            break;
                        default:
                            break;
                    }
                    break;
                case "E0":
                    position.set(Position.KEY_ALARM, Position.ALARM_FAULT);
                    position.set("error", Integer.parseInt(values[index++]));
                    break;
                case "S1":
                    position.set(Position.KEY_EVENT, Integer.parseInt(values[index++]));
                    break;
                case "D1":
                case "R0":
                case "L0":
                case "L1":
                case "S4":
                case "S6":
                case "S7":
                case "V0":
                case "G0":
                case "K0":
                case "I0":
                case "M0":
                    position.set(Position.KEY_RESULT, payload);
                    break;
                default:
                    break;
            }

            return !position.getAttributes().isEmpty() ? position : null;

        } else {

            Parser parser = new Parser(PATTERN, sentence);
            if (!parser.matches()) {
                Parser parserAlt = new Parser(PATTERNWITHERROR, sentence);
                if (!parserAlt.matches()) {
                    LOGGER.info("not match");
                    return null;
                }
                Position position = new Position(getProtocolName());
                position.setDeviceId(deviceSession.getDeviceId());

                DateBuilder date = new DateBuilder().setDate(parserAlt.nextInt(), parserAlt.nextInt(), parserAlt.nextInt());
                DateBuilder time = new DateBuilder().setTime(parserAlt.nextInt(), parserAlt.nextInt(), parserAlt.nextInt());

                DateBuilder dateBuilder = new DateBuilder()
                        .setTime(parserAlt.nextInt(), parserAlt.nextInt(), parserAlt.nextInt());

                position.setValid(parserAlt.next().equals("A"));
                position.setLatitude(parserAlt.nextCoordinate());
                position.setLongitude(parserAlt.nextCoordinate());

                position.set(Position.KEY_SATELLITES, parserAlt.nextInt());
                position.set(Position.KEY_HDOP, parserAlt.nextDouble());

                Instant instant = Instant.now();
                LOGGER.info(instant.toString());
                String[] support = instant.toString().split("T");
                String[] timeSupport = support[1].split("\\.");
                LocalDateTime now = LocalDateTime.now();
                dateBuilder.setDateReverse(now.getDayOfMonth(), now.getMonthValue() + 1, now.getYear());
                position.setTime(dateBuilder.getDate());

                position.setAltitude(parserAlt.nextDouble());

                return position;
            }

            Position position = new Position(getProtocolName());
            position.setDeviceId(deviceSession.getDeviceId());

            DateBuilder date = new DateBuilder().setDate(parser.nextInt(), parser.nextInt(), parser.nextInt());
            DateBuilder time = new DateBuilder().setTime(parser.nextInt(), parser.nextInt(), parser.nextInt());

            DateBuilder dateBuilder = new DateBuilder()
                    .setTime(parser.nextInt(), parser.nextInt(), parser.nextInt());

            position.setValid(parser.next().equals("A"));
            position.setLatitude(parser.nextCoordinate());
            position.setLongitude(parser.nextCoordinate());

            position.set(Position.KEY_SATELLITES, parser.nextInt());
            position.set(Position.KEY_HDOP, parser.nextDouble());

            dateBuilder.setDateReverse(parser.nextInt(), parser.nextInt(), parser.nextInt());
            position.setTime(dateBuilder.getDate());

            position.setAltitude(parser.nextDouble());

            return position;
        }
    }
}

This is the code of OmniProtocolEncoder:

 package org.traccar.protocol;

import io.netty.channel.Channel;
import org.traccar.BaseProtocolEncoder;
import org.traccar.Protocol;
import org.traccar.model.Command;
import java.time.format.DateTimeFormatter;
import java.time.LocalDateTime;

public class OmniProtocolEncoder extends BaseProtocolEncoder {

    public OmniProtocolEncoder(Protocol protocol) {
        super(protocol);
    }

    private String formatCommand(Command command, String content) {
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyMMddHHmmss");
        LocalDateTime now = LocalDateTime.now();
        return String.format("\u00ff\u00ff*CMDS,LH,%s,%s,%s#\n", getUniqueId(command.getDeviceId()), dtf.format(now), content);
    }

    @Override
    protected Object encodeCommand(Channel channel, Command command) {

        switch (command.getType()) {
            case Command.TYPE_CUSTOM:
                return formatCommand(command, command.getString(Command.KEY_DATA));
            case Command.TYPE_POSITION_SINGLE:
                return formatCommand(command, "D0");
            case Command.TYPE_POSITION_PERIODIC:
                return formatCommand(command, "D1," + command.getInteger(Command.KEY_FREQUENCY));
            case Command.TYPE_ENGINE_STOP:
            case Command.TYPE_ALARM_DISARM:
                if (channel != null) {
                    OmniProtocolDecoder decoder = channel.pipeline().get(OmniProtocolDecoder.class);
                    if (decoder != null) {
                        decoder.setPendingCommand(command.getType());
                    }
                }
                return formatCommand(command, "L0,0,1234," + System.currentTimeMillis() / 1000);
            default:
                return null;
        }
    }
}

and this is a little bit of log:

2023-02-03 11:04:29  INFO: [T246f7d76: omni < 18.196.213.123] 2a434d44522c4c482c3836303533373036313433313339312c3030303030303030303030302c4c312c313233342c313637353431353432312c31230a
2023-02-03 11:04:29  INFO: ÿÿ*CMDS,LH,860537061431391,230203110429,Re,L1#

2023-02-03 11:04:29  INFO: [T246f7d76: omni > 18.196.213.123] ffff2a434d44532c4c482c3836303533373036313433313339312c3233303230333131303432392c52652c4c31230a
2023-02-03 11:04:29  INFO: Expo notification send
2023-02-03 11:04:29  INFO: Expo notification send
2023-02-03 11:04:29  INFO: [T246f7d76] id: 860537061431391, time: 2023-02-03 10:03:35, lat: 37.69136, lon: 15.14593, course: 0.0, result: L1,1234,1675415421,1
2023-02-03 11:04:35  INFO: [T9eaaaa13: teltonika < 194.88.242.120] ff
2023-02-03 11:05:00  INFO: [T246f7d76: omni < 18.196.213.123] 2a434d44522c4c482c3836303533373036313433313339312c3030303030303030303030302c4c312c313233342c313637353431353432312c31230a
2023-02-03 11:05:00  INFO: ÿÿ*CMDS,LH,860537061431391,230203110500,Re,L1#

2023-02-03 11:05:00  INFO: [T246f7d76: omni > 18.196.213.123] ffff2a434d44532c4c482c3836303533373036313433313339312c3233303230333131303530302c52652c4c31230a

I hope you can help me
Thanks in advance

Anton Tananaev2 years ago

If the protocol is similar, can the existing decoder be extended? Do you have the protocol documentation?

rgm2 years ago

i have the protocol documentation, do you want it?

Anton Tananaev2 years ago

Yes. What about the first question?

rgm2 years ago

I don't think it makes much sense, maybe if the protocol logic is made more generic it may make sense to extend the xrb28 protocol directly, how can i send you the documentation? it is a pdf file

Anton Tananaev2 years ago

You can send it to the support email address.

Anton Tananaev2 years ago

It looks very similar, so I think it totally makes sense to reuse the same decoder.

rgm2 years ago

but for the problem? do you have some advises?

Anton Tananaev2 years ago

If you want to contribute this, I'm happy to help. We can do it over an PR.

rgm2 years ago

I solved the problem sending a buffer instead of a String in channel.write

gabriel2 years ago

Hey @rgm
Did you were able to implement this Omni protocol successfully? Did you create a PR with it?

Thanks in advance!!

rgm2 years ago

Hi, @gabriel
I didn't create a PR, but I managed to implement the protocol.

gabriel2 years ago

@rgm Thank you for your prompt response. Would you be willing to share the implementation with me? I would be glad to assist in creating a pull request to include this feature in a future release. We are currently facing an issue with the Xrb28 protocol, as it is not accurately processing the positions from our Omni devices (kick scooters). Moreover, we also require the lock/unlock command which seems to be missing.

rgm2 years ago

Yes, but i've tested it on the Omni Lock for ebikes not in the kick scooters. Tell me how to share the code with you.

gabriel2 years ago

Thank you for the clarification. If it's not too much trouble, could you please share the code on a public or private repository on GitHub? It would be easier for us to collaborate and review the implementation. Alternatively, if you prefer, you can share the code with me through an email. Please let me know which option works best for you.