diff --git a/build.gradle b/build.gradle
index a51ebaff011..3677f8f940a 100644
--- a/build.gradle
+++ b/build.gradle
@@ -14,7 +14,7 @@ compileJava.options.encoding = "UTF-8"
jar.destinationDirectory = file("$projectDir/target")
checkstyle {
- toolVersion = "10.12.0"
+ toolVersion = "10.12.5"
configFile = "gradle/checkstyle.xml" as File
checkstyleTest.enabled = false
}
@@ -27,11 +27,11 @@ enforce {
ext {
guiceVersion = "7.0.0"
- jettyVersion = "11.0.18"
- jerseyVersion = "3.1.3"
- jacksonVersion = "2.15.2" // same version as jersey-media-json-jackson dependency
- protobufVersion = "3.24.0"
- jxlsVersion = "2.13.0"
+ jettyVersion = "11.0.19"
+ jerseyVersion = "3.1.5"
+ jacksonVersion = "2.15.3" // same version as jersey-media-json-jackson dependency
+ protobufVersion = "3.25.2"
+ jxlsVersion = "2.14.0"
junitVersion = "5.10.1"
}
@@ -45,12 +45,12 @@ dependencies {
implementation "commons-codec:commons-codec:1.16.0"
implementation "com.h2database:h2:2.2.224"
implementation "com.mysql:mysql-connector-j:8.2.0"
- implementation "org.mariadb.jdbc:mariadb-java-client:3.3.0"
- implementation "org.postgresql:postgresql:42.6.0"
+ implementation "org.mariadb.jdbc:mariadb-java-client:3.3.2"
+ implementation "org.postgresql:postgresql:42.7.1"
implementation "com.microsoft.sqlserver:mssql-jdbc:12.4.2.jre11"
implementation "com.zaxxer:HikariCP:5.1.0"
- implementation "io.netty:netty-all:4.1.101.Final"
- implementation "org.slf4j:slf4j-jdk14:2.0.9"
+ implementation "io.netty:netty-all:4.1.104.Final"
+ implementation "org.slf4j:slf4j-jdk14:2.0.11"
implementation "com.google.inject:guice:$guiceVersion"
implementation "com.google.inject.extensions:guice-servlet:$guiceVersion"
implementation "org.owasp.encoder:encoder:1.2.3"
@@ -66,7 +66,7 @@ dependencies {
implementation "org.glassfish.jersey.containers:jersey-container-servlet:$jerseyVersion"
implementation "org.glassfish.jersey.media:jersey-media-json-jackson:$jerseyVersion"
implementation "org.glassfish.jersey.inject:jersey-hk2:$jerseyVersion"
- implementation "org.glassfish.hk2:guice-bridge:3.0.4" // same version as jersey-hk2
+ implementation "org.glassfish.hk2:guice-bridge:3.0.5" // same version as jersey-hk2
implementation "com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:$jacksonVersion"
implementation "com.fasterxml.jackson.datatype:jackson-datatype-jakarta-jsonp:$jacksonVersion"
implementation "org.liquibase:liquibase-core:4.23.2" // upgrade has issues
@@ -79,20 +79,20 @@ dependencies {
implementation "org.mnode.ical4j:ical4j:3.2.14"
implementation "org.locationtech.spatial4j:spatial4j:0.8"
implementation "org.locationtech.jts:jts-core:1.19.0"
- implementation "net.java.dev.jna:jna-platform:5.13.0"
+ implementation "net.java.dev.jna:jna-platform:5.14.0"
implementation "com.github.jnr:jnr-posix:3.1.18"
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
- implementation "com.amazonaws:aws-java-sdk-sns:1.12.592"
- implementation "org.apache.kafka:kafka-clients:3.6.0"
+ implementation "com.amazonaws:aws-java-sdk-sns:1.12.636"
+ implementation "org.apache.kafka:kafka-clients:3.6.1"
implementation "com.hivemq:hivemq-mqtt-client:1.3.3"
- implementation "redis.clients:jedis:5.0.2"
+ implementation "redis.clients:jedis:5.1.0"
implementation "com.google.firebase:firebase-admin:9.2.0"
- implementation "com.nimbusds:oauth2-oidc-sdk:11.6"
+ implementation "com.nimbusds:oauth2-oidc-sdk:11.9.1"
implementation "com.rabbitmq:amqp-client:5.20.0"
implementation "com.warrenstrange:googleauth:1.5.0"
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
testImplementation "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
- testImplementation "org.mockito:mockito-core:5.7.0"
+ testImplementation "org.mockito:mockito-core:5.8.0"
}
test {
@@ -109,7 +109,7 @@ jar {
manifest {
attributes(
"Main-Class": "org.traccar.Main",
- "Implementation-Version": "5.10",
+ "Implementation-Version": "5.12",
"Class-Path": configurations.runtimeClasspath.files.collect { "lib/$it.name" }.join(" "))
}
}
diff --git a/setup/default.xml b/setup/default.xml
index 48fd8c99312..b89e3ebef0e 100644
--- a/setup/default.xml
+++ b/setup/default.xml
@@ -14,6 +14,7 @@
./modern
false
false
+ true
true
locationiq
@@ -295,5 +296,6 @@
5251
5252
5253
+ 5254
diff --git a/setup/traccar.iss b/setup/traccar.iss
index 23daf6e1389..2ccee1c3e8a 100644
--- a/setup/traccar.iss
+++ b/setup/traccar.iss
@@ -1,6 +1,6 @@
[Setup]
AppName=Traccar
-AppVersion=5.10
+AppVersion=5.12
DefaultDirName={pf}\Traccar
OutputBaseFilename=traccar-setup
ArchitecturesInstallIn64BitMode=x64
diff --git a/src/main/java/org/traccar/BaseProtocolDecoder.java b/src/main/java/org/traccar/BaseProtocolDecoder.java
index 4d4086c3c24..495a866c051 100644
--- a/src/main/java/org/traccar/BaseProtocolDecoder.java
+++ b/src/main/java/org/traccar/BaseProtocolDecoder.java
@@ -51,6 +51,8 @@ public abstract class BaseProtocolDecoder extends ExtendedObjectDecoder {
private MediaManager mediaManager;
private CommandsManager commandsManager;
+ private String modelOverride;
+
public BaseProtocolDecoder(Protocol protocol) {
this.protocol = protocol;
}
@@ -141,6 +143,14 @@ public DeviceSession getDeviceSession(Channel channel, SocketAddress remoteAddre
}
}
+ public void setModelOverride(String modelOverride) {
+ this.modelOverride = modelOverride;
+ }
+
+ public String getDeviceModel(DeviceSession deviceSession) {
+ return modelOverride != null ? modelOverride : deviceSession.getModel();
+ }
+
public void getLastLocation(Position position, Date deviceTime) {
if (position.getDeviceId() != 0) {
position.setOutdated(true);
diff --git a/src/main/java/org/traccar/BaseProtocolEncoder.java b/src/main/java/org/traccar/BaseProtocolEncoder.java
index b9ca168385a..e357c27dccf 100644
--- a/src/main/java/org/traccar/BaseProtocolEncoder.java
+++ b/src/main/java/org/traccar/BaseProtocolEncoder.java
@@ -39,6 +39,8 @@ public abstract class BaseProtocolEncoder extends ChannelOutboundHandlerAdapter
private CacheManager cacheManager;
+ private String modelOverride;
+
public BaseProtocolEncoder(Protocol protocol) {
this.protocol = protocol;
}
@@ -68,6 +70,15 @@ protected void initDevicePassword(Command command, String defaultPassword) {
}
}
+ public void setModelOverride(String modelOverride) {
+ this.modelOverride = modelOverride;
+ }
+
+ public String getDeviceModel(long deviceId) {
+ String model = getCacheManager().getObject(Device.class, deviceId).getModel();
+ return modelOverride != null ? modelOverride : model;
+ }
+
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
diff --git a/src/main/java/org/traccar/Main.java b/src/main/java/org/traccar/Main.java
index e34fbb72a14..33fcf654f83 100644
--- a/src/main/java/org/traccar/Main.java
+++ b/src/main/java/org/traccar/Main.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2012 - 2022 Anton Tananaev (anton@traccar.org)
+ * Copyright 2012 - 2024 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.
@@ -20,10 +20,8 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.traccar.broadcast.BroadcastService;
-import org.traccar.helper.model.DeviceUtil;
import org.traccar.schedule.ScheduleManager;
import org.traccar.storage.DatabaseModule;
-import org.traccar.storage.Storage;
import org.traccar.web.WebModule;
import org.traccar.web.WebServer;
@@ -33,10 +31,9 @@
import java.lang.management.OperatingSystemMXBean;
import java.lang.management.RuntimeMXBean;
import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Locale;
-import java.util.Objects;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
public final class Main {
@@ -70,8 +67,7 @@ public static void logSystemInfo() {
+ " heap: " + memoryBean.getHeapMemoryUsage().getMax() / (1024 * 1024) + "mb"
+ " non-heap: " + memoryBean.getNonHeapMemoryUsage().getMax() / (1024 * 1024) + "mb");
- LOGGER.info("Character encoding: "
- + System.getProperty("file.encoding") + " charset: " + Charset.defaultCharset());
+ LOGGER.info("Character encoding: " + Charset.defaultCharset().displayName());
} catch (Exception error) {
LOGGER.warn("Failed to get system info");
@@ -122,18 +118,14 @@ public static void run(String configFile) {
LOGGER.info("Version: " + Main.class.getPackage().getImplementationVersion());
LOGGER.info("Starting server...");
- if (injector.getInstance(BroadcastService.class).singleInstance()) {
- DeviceUtil.resetStatus(injector.getInstance(Storage.class));
- }
-
- var services = Stream.of(
- ServerManager.class, WebServer.class, ScheduleManager.class, BroadcastService.class)
- .map(injector::getInstance)
- .filter(Objects::nonNull)
- .collect(Collectors.toList());
-
- for (var service : services) {
- service.start();
+ var services = new ArrayList();
+ for (var clazz : List.of(
+ ScheduleManager.class, ServerManager.class, WebServer.class, BroadcastService.class)) {
+ var service = injector.getInstance(clazz);
+ if (service != null) {
+ service.start();
+ services.add(service);
+ }
}
Thread.setDefaultUncaughtExceptionHandler((t, e) -> LOGGER.error("Thread exception", e));
diff --git a/src/main/java/org/traccar/WindowsService.java b/src/main/java/org/traccar/WindowsService.java
index f233337a755..08eba25a650 100644
--- a/src/main/java/org/traccar/WindowsService.java
+++ b/src/main/java/org/traccar/WindowsService.java
@@ -170,7 +170,7 @@ private void reportStatus(int status, int win32ExitCode, int waitHint) {
public abstract void run();
- private class ServiceMain implements SERVICE_MAIN_FUNCTION {
+ private final class ServiceMain implements SERVICE_MAIN_FUNCTION {
public void callback(int dwArgc, Pointer lpszArgv) {
ServiceControl serviceControl = new ServiceControl();
@@ -203,7 +203,7 @@ public void callback(int dwArgc, Pointer lpszArgv) {
}
- private class ServiceControl implements HandlerEx {
+ private final class ServiceControl implements HandlerEx {
public int callback(int dwControl, int dwEventType, Pointer lpEventData, Pointer lpContext) {
switch (dwControl) {
diff --git a/src/main/java/org/traccar/api/resource/DeviceResource.java b/src/main/java/org/traccar/api/resource/DeviceResource.java
index 217ccda6530..89bba7237bf 100644
--- a/src/main/java/org/traccar/api/resource/DeviceResource.java
+++ b/src/main/java/org/traccar/api/resource/DeviceResource.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2015 - 2022 Anton Tananaev (anton@traccar.org)
+ * Copyright 2015 - 2024 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.
@@ -19,6 +19,8 @@
import org.traccar.api.BaseObjectResource;
import org.traccar.api.signature.TokenManager;
import org.traccar.broadcast.BroadcastService;
+import org.traccar.config.Config;
+import org.traccar.config.Keys;
import org.traccar.database.MediaManager;
import org.traccar.helper.LogAction;
import org.traccar.model.Device;
@@ -60,6 +62,9 @@
@Consumes(MediaType.APPLICATION_JSON)
public class DeviceResource extends BaseObjectResource {
+ @Inject
+ private Config config;
+
@Inject
private CacheManager cacheManager;
@@ -199,6 +204,15 @@ public String shareDevice(
@FormParam("expiration") Date expiration) throws StorageException, GeneralSecurityException, IOException {
User user = permissionsService.getUser(getUserId());
+ if (permissionsService.getServer().getBoolean(Keys.DEVICE_SHARE_DISABLE.getKey())) {
+ throw new SecurityException("Sharing is disabled");
+ }
+ if (user.getTemporary()) {
+ throw new SecurityException("Temporary user");
+ }
+ if (user.getExpirationTime() != null && user.getExpirationTime().before(expiration)) {
+ expiration = user.getExpirationTime();
+ }
Device device = storage.getObject(Device.class, new Request(
new Columns.All(),
@@ -206,16 +220,24 @@ public String shareDevice(
new Condition.Equals("id", deviceId),
new Condition.Permission(User.class, user.getId(), Device.class))));
- User share = new User();
- share.setName(device.getName());
- share.setEmail(user.getEmail() + ":" + device.getUniqueId());
- share.setExpirationTime(expiration);
- share.setTemporary(true);
- share.setReadonly(true);
+ String shareEmail = user.getEmail() + ":" + device.getUniqueId();
+ User share = storage.getObject(User.class, new Request(
+ new Columns.All(), new Condition.Equals("email", shareEmail)));
+
+ if (share == null) {
+ share = new User();
+ share.setName(device.getName());
+ share.setEmail(shareEmail);
+ share.setExpirationTime(expiration);
+ share.setTemporary(true);
+ share.setReadonly(true);
+ share.setLimitCommands(user.getLimitCommands() || !config.getBoolean(Keys.WEB_SHARE_DEVICE_COMMANDS));
+ share.setDisableReports(user.getDisableReports() || !config.getBoolean(Keys.WEB_SHARE_DEVICE_REPORTS));
- share.setId(storage.addObject(share, new Request(new Columns.Exclude("id"))));
+ share.setId(storage.addObject(share, new Request(new Columns.Exclude("id"))));
- storage.addPermission(new Permission(User.class, share.getId(), Device.class, deviceId));
+ storage.addPermission(new Permission(User.class, share.getId(), Device.class, deviceId));
+ }
return tokenManager.generateToken(share.getId(), expiration);
}
diff --git a/src/main/java/org/traccar/api/resource/ServerResource.java b/src/main/java/org/traccar/api/resource/ServerResource.java
index 66ecc74e18c..2a72d27736c 100644
--- a/src/main/java/org/traccar/api/resource/ServerResource.java
+++ b/src/main/java/org/traccar/api/resource/ServerResource.java
@@ -100,9 +100,6 @@ public Server get() throws StorageException {
} else {
server.setNewServer(UserUtil.isEmpty(storage));
}
- if (user != null && user.getAdministrator()) {
- server.setStorageSpace(Log.getStorageSpace());
- }
return server;
}
diff --git a/src/main/java/org/traccar/config/Keys.java b/src/main/java/org/traccar/config/Keys.java
index 3059c4f4b56..02e6848759a 100644
--- a/src/main/java/org/traccar/config/Keys.java
+++ b/src/main/java/org/traccar/config/Keys.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 - 2023 Anton Tananaev (anton@traccar.org)
+ * Copyright 2019 - 2024 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.
@@ -340,6 +340,13 @@ private Keys() {
List.of(KeyType.SERVER, KeyType.DEVICE),
0.0);
+ /**
+ * Disable device sharing on the server.
+ */
+ public static final ConfigKey DEVICE_SHARE_DISABLE = new BooleanConfigKey(
+ "disableShare",
+ List.of(KeyType.SERVER));
+
/**
* Speed limit threshold multiplier. For example, if the speed limit is 100, but we only want to generate an event
* if the speed is higher than 105, this parameter can be set to 1.05. Default multiplier is 1.0.
@@ -489,14 +496,6 @@ private Keys() {
"database.throttleUnknown",
List.of(KeyType.CONFIG));
- /**
- * By default, server syncs with the database if it encounters and unknown device. This flag allows to disable that
- * behavior to improve performance in some cases.
- */
- public static final ConfigKey DATABASE_IGNORE_UNKNOWN = new BooleanConfigKey(
- "database.ignoreUnknown",
- List.of(KeyType.CONFIG));
-
/**
* Automatically register unknown devices in the database.
*/
@@ -664,7 +663,7 @@ private Keys() {
/**
* OpenID Connect Authorization URL.
* This can usually be found in the documentation of your identity provider or by using the well-known
- * configuration endpoint, eg. https://auth.example.com//.well-known/openid-configuration
+ * configuration endpoint, e.g. https://auth.example.com//.well-known/openid-configuration
* Required to enable SSO if openid.issuerUrl is not set.
*/
public static final ConfigKey OPENID_AUTH_URL = new StringConfigKey(
@@ -1225,9 +1224,37 @@ private Keys() {
"notificator.telegram.sendLocation",
List.of(KeyType.CONFIG));
+ /**
+ * Enable user expiration email notification.
+ */
+ public static final ConfigKey NOTIFICATION_EXPIRATION_USER = new BooleanConfigKey(
+ "notification.expiration.user",
+ List.of(KeyType.CONFIG));
+
+ /**
+ * User expiration reminder. Value in milliseconds.
+ */
+ public static final ConfigKey NOTIFICATION_EXPIRATION_USER_REMINDER = new LongConfigKey(
+ "notification.expiration.user.reminder",
+ List.of(KeyType.CONFIG));
+
+ /**
+ * Enable device expiration email notification.
+ */
+ public static final ConfigKey NOTIFICATION_EXPIRATION_DEVICE = new BooleanConfigKey(
+ "notification.expiration.device",
+ List.of(KeyType.CONFIG));
+
+ /**
+ * Device expiration reminder. Value in milliseconds.
+ */
+ public static final ConfigKey NOTIFICATION_EXPIRATION_DEVICE_REMINDER = new LongConfigKey(
+ "notification.expiration.device.reminder",
+ List.of(KeyType.CONFIG));
+
/**
* Maximum time period for reports in seconds. Can be useful to prevent users to request unreasonably long reports.
- * By default there is no limit.
+ * By default, there is no limit.
*/
public static final ConfigKey REPORT_PERIOD_LIMIT = new LongConfigKey(
"report.periodLimit",
@@ -1778,6 +1805,27 @@ private Keys() {
"web.url",
List.of(KeyType.CONFIG));
+ /**
+ * Show logs from unknown devices.
+ */
+ public static final ConfigKey WEB_SHOW_UNKNOWN_DEVICES = new BooleanConfigKey(
+ "web.showUnknownDevices",
+ List.of(KeyType.CONFIG));
+
+ /**
+ * Enable commands for a shared device.
+ */
+ public static final ConfigKey WEB_SHARE_DEVICE_COMMANDS = new BooleanConfigKey(
+ "web.shareDevice.commands",
+ List.of(KeyType.CONFIG));
+
+ /**
+ * Enable reports for a shared device.
+ */
+ public static final ConfigKey WEB_SHARE_DEVICE_REPORTS = new BooleanConfigKey(
+ "web.shareDevice.reports",
+ List.of(KeyType.CONFIG));
+
/**
* Output logging to the standard terminal output instead of a log file.
*/
diff --git a/src/main/java/org/traccar/database/DeviceLookupService.java b/src/main/java/org/traccar/database/DeviceLookupService.java
index 6ec6841a1f3..90d23531e33 100644
--- a/src/main/java/org/traccar/database/DeviceLookupService.java
+++ b/src/main/java/org/traccar/database/DeviceLookupService.java
@@ -49,7 +49,7 @@ public class DeviceLookupService {
private final boolean throttlingEnabled;
- private static class IdentifierInfo {
+ private static final class IdentifierInfo {
private long lastQuery;
private long delay;
private Timeout timeout;
diff --git a/src/main/java/org/traccar/database/NotificationManager.java b/src/main/java/org/traccar/database/NotificationManager.java
index 45263ff3c16..65437f0a1f7 100644
--- a/src/main/java/org/traccar/database/NotificationManager.java
+++ b/src/main/java/org/traccar/database/NotificationManager.java
@@ -87,7 +87,7 @@ private void updateEvent(Event event, Position position) {
return;
}
- var notifications = cacheManager.getDeviceNotifications(event.getDeviceId())
+ var notifications = cacheManager.getDeviceNotifications(event.getDeviceId()).stream()
.filter(notification -> notification.getType().equals(event.getType()))
.filter(notification -> {
if (event.getType().equals(Event.TYPE_ALARM)) {
diff --git a/src/main/java/org/traccar/handler/events/MaintenanceEventHandler.java b/src/main/java/org/traccar/handler/events/MaintenanceEventHandler.java
index 6c4271ce231..2fa2e8869a6 100644
--- a/src/main/java/org/traccar/handler/events/MaintenanceEventHandler.java
+++ b/src/main/java/org/traccar/handler/events/MaintenanceEventHandler.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2016 - 2022 Anton Tananaev (anton@traccar.org)
+ * Copyright 2016 - 2024 Anton Tananaev (anton@traccar.org)
* Copyright 2016 - 2018 Andrey Kunitsyn (andrey@traccar.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -49,8 +49,8 @@ protected Map analyzePosition(Position position) {
Map events = new HashMap<>();
for (Maintenance maintenance : cacheManager.getDeviceObjects(position.getDeviceId(), Maintenance.class)) {
if (maintenance.getPeriod() != 0) {
- double oldValue = lastPosition.getDouble(maintenance.getType());
- double newValue = position.getDouble(maintenance.getType());
+ double oldValue = getValue(lastPosition, maintenance.getType());
+ double newValue = getValue(position, maintenance.getType());
if (oldValue != 0.0 && newValue != 0.0 && newValue >= maintenance.getStart()) {
if (oldValue < maintenance.getStart()
|| (long) ((oldValue - maintenance.getStart()) / maintenance.getPeriod())
@@ -67,4 +67,17 @@ protected Map analyzePosition(Position position) {
return events;
}
+ private double getValue(Position position, String type) {
+ switch (type) {
+ case "serverTime":
+ return position.getServerTime().getTime();
+ case "deviceTime":
+ return position.getDeviceTime().getTime();
+ case "fixTime":
+ return position.getFixTime().getTime();
+ default:
+ return position.getDouble(type);
+ }
+ }
+
}
diff --git a/src/main/java/org/traccar/helper/BufferUtil.java b/src/main/java/org/traccar/helper/BufferUtil.java
index 12c31ba9df5..b453c437f8f 100644
--- a/src/main/java/org/traccar/helper/BufferUtil.java
+++ b/src/main/java/org/traccar/helper/BufferUtil.java
@@ -83,4 +83,8 @@ public static boolean isPrintable(ByteBuf buf, int length) {
return printable;
}
+ public static String readString(ByteBuf buf, int length) {
+ return buf.readCharSequence(length, StandardCharsets.US_ASCII).toString();
+ }
+
}
diff --git a/src/main/java/org/traccar/model/Device.java b/src/main/java/org/traccar/model/Device.java
index e0781597649..a3088a613d3 100644
--- a/src/main/java/org/traccar/model/Device.java
+++ b/src/main/java/org/traccar/model/Device.java
@@ -53,6 +53,9 @@ public String getUniqueId() {
}
public void setUniqueId(String uniqueId) {
+ if (uniqueId.contains("../") || uniqueId.contains("..\\")) {
+ throw new IllegalArgumentException("Invalid unique id");
+ }
this.uniqueId = uniqueId.trim();
}
diff --git a/src/main/java/org/traccar/notification/NotificationFormatter.java b/src/main/java/org/traccar/notification/NotificationFormatter.java
index e994729c026..7685eac0dda 100644
--- a/src/main/java/org/traccar/notification/NotificationFormatter.java
+++ b/src/main/java/org/traccar/notification/NotificationFormatter.java
@@ -23,6 +23,7 @@
import org.traccar.model.Event;
import org.traccar.model.Geofence;
import org.traccar.model.Maintenance;
+import org.traccar.model.Notification;
import org.traccar.model.Position;
import org.traccar.model.Server;
import org.traccar.model.User;
@@ -44,13 +45,15 @@ public NotificationFormatter(
this.textTemplateFormatter = textTemplateFormatter;
}
- public NotificationMessage formatMessage(User user, Event event, Position position, String templatePath) {
+ public NotificationMessage formatMessage(
+ Notification notification, User user, Event event, Position position, String templatePath) {
Server server = cacheManager.getServer();
Device device = cacheManager.getObject(Device.class, event.getDeviceId());
VelocityContext velocityContext = textTemplateFormatter.prepareContext(server, user);
+ velocityContext.put("notification", notification);
velocityContext.put("device", device);
velocityContext.put("event", event);
if (position != null) {
diff --git a/src/main/java/org/traccar/notificators/NotificatorFirebase.java b/src/main/java/org/traccar/notificators/NotificatorFirebase.java
index d75eb21a93e..89031ba2682 100644
--- a/src/main/java/org/traccar/notificators/NotificatorFirebase.java
+++ b/src/main/java/org/traccar/notificators/NotificatorFirebase.java
@@ -85,7 +85,7 @@ public NotificatorFirebase(
public void send(Notification notification, User user, Event event, Position position) throws MessageException {
if (user.hasAttribute("notificationTokens")) {
- var shortMessage = notificationFormatter.formatMessage(user, event, position, "short");
+ var shortMessage = notificationFormatter.formatMessage(notification, user, event, position, "short");
List registrationTokens = new ArrayList<>(
Arrays.asList(user.getString("notificationTokens").split("[, ]")));
diff --git a/src/main/java/org/traccar/notificators/NotificatorMail.java b/src/main/java/org/traccar/notificators/NotificatorMail.java
index 3ab050686b8..11d4c5baea1 100644
--- a/src/main/java/org/traccar/notificators/NotificatorMail.java
+++ b/src/main/java/org/traccar/notificators/NotificatorMail.java
@@ -43,7 +43,7 @@ public NotificatorMail(MailManager mailManager, NotificationFormatter notificati
@Override
public void send(Notification notification, User user, Event event, Position position) throws MessageException {
try {
- var fullMessage = notificationFormatter.formatMessage(user, event, position, "full");
+ var fullMessage = notificationFormatter.formatMessage(notification, user, event, position, "full");
mailManager.sendMessage(user, false, fullMessage.getSubject(), fullMessage.getBody());
} catch (MessagingException e) {
throw new MessageException(e);
diff --git a/src/main/java/org/traccar/notificators/NotificatorPushover.java b/src/main/java/org/traccar/notificators/NotificatorPushover.java
index 9f2a8c94de2..cf4c4026b0e 100644
--- a/src/main/java/org/traccar/notificators/NotificatorPushover.java
+++ b/src/main/java/org/traccar/notificators/NotificatorPushover.java
@@ -63,7 +63,7 @@ public NotificatorPushover(Config config, NotificationFormatter notificationForm
@Override
public void send(Notification notification, User user, Event event, Position position) {
- var shortMessage = notificationFormatter.formatMessage(user, event, position, "short");
+ var shortMessage = notificationFormatter.formatMessage(notification, user, event, position, "short");
Message message = new Message();
message.token = token;
diff --git a/src/main/java/org/traccar/notificators/NotificatorSms.java b/src/main/java/org/traccar/notificators/NotificatorSms.java
index 2b6b20b1b6c..ce362290efe 100644
--- a/src/main/java/org/traccar/notificators/NotificatorSms.java
+++ b/src/main/java/org/traccar/notificators/NotificatorSms.java
@@ -46,7 +46,7 @@ public NotificatorSms(
@Override
public void send(Notification notification, User user, Event event, Position position) throws MessageException {
if (user.getPhone() != null) {
- var shortMessage = notificationFormatter.formatMessage(user, event, position, "short");
+ var shortMessage = notificationFormatter.formatMessage(notification, user, event, position, "short");
statisticsManager.registerSms();
smsManager.sendMessage(user.getPhone(), shortMessage.getBody(), false);
}
diff --git a/src/main/java/org/traccar/notificators/NotificatorTelegram.java b/src/main/java/org/traccar/notificators/NotificatorTelegram.java
index c91aaa4ff8d..eaee3281083 100644
--- a/src/main/java/org/traccar/notificators/NotificatorTelegram.java
+++ b/src/main/java/org/traccar/notificators/NotificatorTelegram.java
@@ -87,7 +87,7 @@ private LocationMessage createLocationMessage(String messageChatId, Position pos
@Override
public void send(Notification notification, User user, Event event, Position position) {
- var shortMessage = notificationFormatter.formatMessage(user, event, position, "short");
+ var shortMessage = notificationFormatter.formatMessage(notification, user, event, position, "short");
TextMessage message = new TextMessage();
message.chatId = user.getString("telegramChatId");
diff --git a/src/main/java/org/traccar/notificators/NotificatorTraccar.java b/src/main/java/org/traccar/notificators/NotificatorTraccar.java
index 717742a1ee9..c00e3e02950 100644
--- a/src/main/java/org/traccar/notificators/NotificatorTraccar.java
+++ b/src/main/java/org/traccar/notificators/NotificatorTraccar.java
@@ -87,7 +87,7 @@ public NotificatorTraccar(
public void send(org.traccar.model.Notification notification, User user, Event event, Position position) {
if (user.hasAttribute("notificationTokens")) {
- var shortMessage = notificationFormatter.formatMessage(user, event, position, "short");
+ var shortMessage = notificationFormatter.formatMessage(notification, user, event, position, "short");
NotificationObject item = new NotificationObject();
item.title = shortMessage.getSubject();
diff --git a/src/main/java/org/traccar/notificators/NotificatorWeb.java b/src/main/java/org/traccar/notificators/NotificatorWeb.java
index 3a125db3c2c..2b9030226e6 100644
--- a/src/main/java/org/traccar/notificators/NotificatorWeb.java
+++ b/src/main/java/org/traccar/notificators/NotificatorWeb.java
@@ -51,7 +51,7 @@ public void send(Notification notification, User user, Event event, Position pos
copy.setMaintenanceId(event.getMaintenanceId());
copy.getAttributes().putAll(event.getAttributes());
- var message = notificationFormatter.formatMessage(user, event, position, "short");
+ var message = notificationFormatter.formatMessage(notification, user, event, position, "short");
copy.set("message", message.getBody());
connectionManager.updateEvent(true, user.getId(), copy);
diff --git a/src/main/java/org/traccar/protocol/DualcamProtocolDecoder.java b/src/main/java/org/traccar/protocol/DualcamProtocolDecoder.java
index 75cd52384da..411e2b9d718 100644
--- a/src/main/java/org/traccar/protocol/DualcamProtocolDecoder.java
+++ b/src/main/java/org/traccar/protocol/DualcamProtocolDecoder.java
@@ -19,7 +19,6 @@
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import org.traccar.BaseProtocolDecoder;
-import org.traccar.model.Device;
import org.traccar.session.DeviceSession;
import org.traccar.NetworkMessage;
import org.traccar.Protocol;
@@ -77,7 +76,7 @@ protected Object decode(
deviceSession = getDeviceSession(channel, remoteAddress, uniqueId);
long settings = buf.readUnsignedInt();
if (channel != null && deviceSession != null) {
- model = getCacheManager().getObject(Device.class, deviceSession.getDeviceId()).getModel();
+ model = getDeviceModel(deviceSession);
ByteBuf response = Unpooled.buffer();
if (BitUtil.check(settings, 25)) {
response.writeShort(MSG_PATH_REQUEST);
diff --git a/src/main/java/org/traccar/protocol/FleetGuideProtocol.java b/src/main/java/org/traccar/protocol/FleetGuideProtocol.java
new file mode 100644
index 00000000000..46611c25c3f
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/FleetGuideProtocol.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2024 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 jakarta.inject.Inject;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.config.Config;
+
+public class FleetGuideProtocol extends BaseProtocol {
+
+ @Inject
+ public FleetGuideProtocol(Config config) {
+ addServer(new TrackerServer(config, getName(), true) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline, Config config) {
+ pipeline.addLast(new FleetGuideProtocolDecoder(FleetGuideProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/FleetGuideProtocolDecoder.java b/src/main/java/org/traccar/protocol/FleetGuideProtocolDecoder.java
new file mode 100644
index 00000000000..8f679525be1
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/FleetGuideProtocolDecoder.java
@@ -0,0 +1,328 @@
+/*
+ * Copyright 2024 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 org.traccar.BaseProtocolDecoder;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.Checksum;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+import org.traccar.session.DeviceSession;
+
+import java.net.SocketAddress;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+
+public class FleetGuideProtocolDecoder extends BaseProtocolDecoder {
+
+ public FleetGuideProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ public static final int MSG_EMPTY = 0;
+ public static final int MSG_SYNC_REQ = 1;
+ public static final int MSG_SYNC_ACK = 2;
+ public static final int MSG_DATA_R_ACK = 3;
+ public static final int MSG_DATA_N_ACK = 4;
+ public static final int MSG_REP_R_ACK = 5;
+ public static final int MSG_REP_N_ACK = 6;
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ buf.readUnsignedByte(); // signature
+ int options = buf.readUnsignedShortLE();
+ int length = BitUtil.to(options, 11);
+
+ DeviceSession deviceSession;
+ Long deviceId;
+ if (BitUtil.check(options, 11)) {
+ deviceId = buf.readUnsignedIntLE();
+ deviceSession = getDeviceSession(channel, remoteAddress, String.valueOf(deviceId));
+ } else {
+ deviceId = null;
+ deviceSession = getDeviceSession(channel, remoteAddress);
+ }
+ if (deviceSession == null) {
+ return null;
+ }
+
+ int type;
+ Integer index;
+ if (BitUtil.check(options, 12)) {
+ int value = buf.readUnsignedByte();
+ type = BitUtil.to(value, 4);
+ index = BitUtil.from(value, 4);
+ } else {
+ type = 0;
+ index = null;
+ }
+
+ if (type != MSG_DATA_N_ACK && type != MSG_REP_N_ACK) {
+ Integer responseType;
+ if (type == MSG_SYNC_REQ) {
+ responseType = MSG_SYNC_ACK;
+ } else {
+ responseType = null;
+ }
+ sendResponse(channel, remoteAddress, deviceId, responseType, index);
+ }
+
+ if (BitUtil.check(options, 13)) {
+ buf.readUnsignedShortLE(); // acknowledgement
+ }
+
+ ByteBuf data;
+ if (BitUtil.check(options, 14)) {
+ data = decompress(buf.readSlice(length));
+ } else {
+ data = buf.readRetainedSlice(length);
+ }
+
+ List positions = new LinkedList<>();
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ while (data.isReadable()) {
+
+ int recordHeader = data.readUnsignedShortLE();
+ int recordLength = BitUtil.to(recordHeader, 10);
+ int recordType = BitUtil.from(recordHeader, 10);
+ int recordEndIndex = data.readerIndex() + recordLength;
+
+ if (recordType == 0 && position.getDeviceTime() != null) {
+ processPosition(positions, position);
+ position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+ }
+
+ switch (recordType) {
+ case 0:
+ position.setTime(new Date((data.readUnsignedIntLE() + 1262304000) * 1000)); // since 2010-01-01
+ break;
+ case 1:
+ position.setLatitude(data.readUnsignedIntLE() * 90.0 / 0xFFFFFFFFL);
+ position.setLongitude(data.readUnsignedIntLE() * 180.0 / 0xFFFFFFFFL);
+ int speed = data.readUnsignedShortLE();
+ position.setSpeed(UnitsConverter.knotsFromKph(BitUtil.to(speed, 14) * 0.1));
+ if (BitUtil.check(speed, 14)) {
+ position.setLatitude(-position.getLatitude());
+ }
+ if (BitUtil.check(speed, 15)) {
+ position.setLongitude(-position.getLongitude());
+ }
+ int course = data.readUnsignedShortLE();
+ position.setSpeed(BitUtil.to(course, 9));
+ int motion = BitUtil.between(course, 9, 11);
+ if (motion > 0) {
+ position.set(Position.KEY_MOTION, motion == 1);
+ }
+ position.set(Position.KEY_SATELLITES, BitUtil.from(course, 11));
+ int altitude = data.readUnsignedShortLE();
+ position.setAltitude(BitUtil.to(altitude, 14));
+ if (BitUtil.check(altitude, 14)) {
+ position.setAltitude(-position.getAltitude());
+ }
+ break;
+ case 3:
+ int powerLow = data.readUnsignedByte();
+ int powerFlags = data.readUnsignedByte();
+ int batteryHigh = data.readUnsignedByte();
+ position.set(Position.KEY_POWER, (powerLow + (BitUtil.to(powerFlags, 5) << 8)) * 0.01);
+ position.set(Position.KEY_IGNITION, BitUtil.check(powerFlags, 5));
+ position.set(Position.KEY_BATTERY, (BitUtil.from(powerFlags, 6) + (batteryHigh << 2)) * 0.01);
+ if (recordLength >= 4) {
+ int extraFlags = data.readUnsignedByte();
+ if (BitUtil.check(extraFlags, 0)) {
+ position.set(Position.KEY_ALARM, Position.ALARM_LOW_POWER);
+ }
+ if (BitUtil.check(extraFlags, 1)) {
+ position.set(Position.KEY_ALARM, Position.ALARM_LOW_BATTERY);
+ }
+ }
+ break;
+ case 6:
+ position.set(Position.KEY_INPUT, data.readUnsignedByte());
+ break;
+ case 7:
+ position.set(Position.KEY_OUTPUT, data.readUnsignedByte());
+ break;
+ case 8:
+ int adcMask = data.readUnsignedByte();
+ for (int i = 0; i < 8; i++) {
+ if (BitUtil.check(adcMask, i)) {
+ position.set(Position.PREFIX_ADC + (i + 1), data.readUnsignedShortLE());
+ }
+ }
+ break;
+ case 11:
+ int fuelMask = data.readUnsignedByte();
+ for (int i = 1; i < 8; i++) {
+ if (BitUtil.check(fuelMask, i)) {
+ position.set("fuel" + i, data.readUnsignedShortLE());
+ }
+ }
+ break;
+ case 12:
+ int fuelTempMask = data.readUnsignedByte();
+ for (int i = 1; i < 8; i++) {
+ if (BitUtil.check(fuelTempMask, i)) {
+ position.set("fuelTemp" + i, (int) data.readByte());
+ }
+ }
+ break;
+ case 13:
+ int tempMask = data.readUnsignedByte();
+ for (int i = 0; i < 8; i++) {
+ if (BitUtil.check(tempMask, i)) {
+ position.set(Position.PREFIX_TEMP + (i + 1), data.readShortLE() * 0.01);
+ }
+ }
+ break;
+ case 18:
+ int sensorIndex = data.readUnsignedByte();
+ switch (recordLength - 1) {
+ case 1:
+ position.set("sensor" + sensorIndex, data.readUnsignedByte());
+ break;
+ case 2:
+ position.set("sensor" + sensorIndex, data.readUnsignedShortLE());
+ break;
+ case 4:
+ position.set("sensor" + sensorIndex, data.readUnsignedIntLE());
+ break;
+ default:
+ break;
+ }
+ break;
+ default:
+ break;
+ }
+
+ data.readerIndex(recordEndIndex);
+
+ }
+
+ processPosition(positions, position);
+
+ data.release();
+
+ return positions.isEmpty() ? null : positions;
+ }
+
+ private void processPosition(List positions, Position position) {
+ if (!position.getAttributes().isEmpty()) {
+ if (position.getFixTime() == null) {
+ position.setTime(new Date());
+ }
+ if (!position.getAttributes().containsKey(Position.KEY_SATELLITES)) {
+ getLastLocation(position, null);
+ }
+ positions.add(position);
+ }
+ }
+
+
+ private void sendResponse(
+ Channel channel, SocketAddress remoteAddress, Long deviceId, Integer type, Integer index) {
+ if (channel != null) {
+
+ ByteBuf response = Unpooled.buffer();
+ response.writeByte(0x53); // signature
+
+ int options = 0;
+ if (deviceId != null) {
+ options |= 1 << 11;
+ }
+ if (type != null) {
+ options |= 1 << 12;
+ }
+ if (index != null) {
+ options |= 1 << 13;
+ }
+ response.writeShortLE(options);
+
+ if (deviceId != null) {
+ response.writeIntLE(deviceId.intValue());
+ }
+ if (type != null) {
+ response.writeByte(type);
+ }
+ if (index != null) {
+ int mask = (1 << (index + 1)) - 1;
+ response.writeShortLE(mask);
+ }
+ response.writeShortLE(Checksum.crc16(
+ Checksum.CRC16_CCITT_FALSE, response.nioBuffer(1, response.writerIndex() - 1)));
+
+ channel.writeAndFlush(new NetworkMessage(response, remoteAddress));
+ }
+ }
+
+ private int readVarSize(ByteBuf buf) {
+ int b;
+ int y = 0;
+ do {
+ b = buf.readUnsignedByte();
+ y = (y << 7) | (b & 0x0000007f);
+ } while ((b & 0x00000080) > 0);
+
+ return y;
+ }
+
+ private ByteBuf decompress(ByteBuf in) {
+
+ ByteBuf out = Unpooled.buffer();
+
+ if (in.readableBytes() < 1) {
+ return out;
+ }
+
+ int marker = in.readUnsignedByte();
+
+ do {
+ int symbol = in.readUnsignedByte();
+ if (symbol == marker) {
+ if (in.getUnsignedByte(in.readerIndex()) == 0) {
+ out.writeByte(marker);
+ in.skipBytes(1);
+ } else {
+ int length = readVarSize(in);
+ int offset = readVarSize(in);
+
+ for (int i = 0; i < length; i++) {
+ out.writeByte(out.getUnsignedByte(out.writerIndex() - offset));
+ }
+ }
+ } else {
+ out.writeByte(symbol);
+ }
+ } while (in.isReadable());
+
+ return out;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Gl200TextProtocolDecoder.java b/src/main/java/org/traccar/protocol/Gl200TextProtocolDecoder.java
index ffeeab7deeb..8fb30e2ad01 100644
--- a/src/main/java/org/traccar/protocol/Gl200TextProtocolDecoder.java
+++ b/src/main/java/org/traccar/protocol/Gl200TextProtocolDecoder.java
@@ -16,6 +16,7 @@
package org.traccar.protocol;
import io.netty.buffer.Unpooled;
+import org.apache.commons.lang3.StringUtils;
import org.traccar.BaseProtocolDecoder;
import org.traccar.helper.DataConverter;
import org.traccar.session.DeviceSession;
@@ -61,7 +62,7 @@ protected void init() {
private static final Pattern PATTERN_ACK = new PatternBuilder()
.text("+ACK:GT")
.expression("...,") // type
- .number("([0-9A-Z]{2}xxxx),") // protocol version
+ .expression("(.{6}|.{10}),") // protocol version
.number("(d{15}|x{14}),") // imei
.any().text(",")
.number("(dddd)(dd)(dd)") // date (yyyymmdd)
@@ -130,7 +131,7 @@ private Long parseHours(String hoursString) {
private static final Pattern PATTERN_INF = new PatternBuilder()
.text("+").expression("(?:RESP|BUFF):GTINF,")
- .number("[0-9A-Z]{2}xxxx,") // protocol version
+ .expression("(?:.{6}|.{10})?,") // protocol version
.number("(d{15}|x{14}),") // imei
.expression("(?:[0-9A-Z]{17},)?") // vin
.expression("(?:[^,]+)?,") // device name
@@ -231,7 +232,7 @@ private Object decodeInf(Channel channel, SocketAddress remoteAddress, String se
private static final Pattern PATTERN_VER = new PatternBuilder()
.text("+").expression("(?:RESP|BUFF):GTVER,")
- .number("[0-9A-Z]{2}xxxx,") // protocol version
+ .expression("(?:.{6}|.{10})?,") // protocol version
.number("(d{15}|x{14}),") // imei
.expression("[^,]*,") // device name
.expression("([^,]*),") // device type
@@ -340,7 +341,7 @@ private void decodeLocation(Position position, Parser parser) {
private static final Pattern PATTERN_OBD = new PatternBuilder()
.text("+RESP:GTOBD,")
- .number("[0-9A-Z]{2}xxxx,") // protocol version
+ .expression("(?:.{6}|.{10})?,") // protocol version
.number("(d{15}|x{14}),") // imei
.expression("(?:[0-9A-Z]{17})?,") // vin
.expression("[^,]{0,20},") // device name
@@ -419,17 +420,17 @@ private Object decodeCan(Channel channel, SocketAddress remoteAddress, String se
DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, values[index++]);
position.setDeviceId(deviceSession.getDeviceId());
- String deviceName = values[index++];
+ String model = StringUtils.firstNonEmpty(values[index++], getDeviceModel(deviceSession));
index += 1; // report type
- index += 1; // canbus state
+ index += 1; // can bus state
long reportMask = Long.parseLong(values[index++], 16);
long reportMaskExt = 0;
if (BitUtil.check(reportMask, 0)) {
position.set(Position.KEY_VIN, values[index++]);
}
- if (BitUtil.check(reportMask, 1)) {
- position.set(Position.KEY_IGNITION, Integer.parseInt(values[index++]) > 0);
+ if (BitUtil.check(reportMask, 1) && !values[index++].isEmpty()) {
+ position.set(Position.KEY_IGNITION, Integer.parseInt(values[index - 1]) > 0);
}
if (BitUtil.check(reportMask, 2)) {
position.set(Position.KEY_OBD_ODOMETER, values[index++]);
@@ -491,7 +492,7 @@ private Object decodeCan(Channel channel, SocketAddress remoteAddress, String se
if (BitUtil.check(reportMask, 21) && !values[index++].isEmpty()) {
position.set("engineOverspeed", Double.parseDouble(values[index - 1]));
}
- if ("GV350M".equals(deviceName)) {
+ if ("GV350M".equals(model)) {
if (BitUtil.check(reportMask, 22)) {
index += 1; // impulse distance
}
@@ -501,6 +502,28 @@ private Object decodeCan(Channel channel, SocketAddress remoteAddress, String se
if (BitUtil.check(reportMask, 24)) {
index += 1; // catalyst liquid level
}
+ } else if ("GV355CEU".equals(model)) {
+ if (BitUtil.check(reportMask, 22)) {
+ index += 1; // impulse distance
+ }
+ if (BitUtil.check(reportMask, 23)) {
+ index += 1; // engine cold starts
+ }
+ if (BitUtil.check(reportMask, 24)) {
+ index += 1; // engine all starts
+ }
+ if (BitUtil.check(reportMask, 25)) {
+ index += 1; // engine starts by ignition
+ }
+ if (BitUtil.check(reportMask, 26)) {
+ index += 1; // total engine cold running time
+ }
+ if (BitUtil.check(reportMask, 27)) {
+ index += 1; // handbrake applies during ride
+ }
+ if (BitUtil.check(reportMask, 28)) {
+ index += 1; // electric report mask
+ }
}
if (BitUtil.check(reportMask, 29) && !values[index++].isEmpty()) {
reportMaskExt = Long.parseLong(values[index - 1], 16);
@@ -581,7 +604,7 @@ private Object decodeCan(Channel channel, SocketAddress remoteAddress, String se
DateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
- if (BitUtil.check(reportMask, 30)) {
+ if (!"GV355CEU".equals(model) && BitUtil.check(reportMask, 30)) {
while (values[index].isEmpty()) {
index += 1;
}
@@ -606,6 +629,7 @@ private Object decodeCan(Channel channel, SocketAddress remoteAddress, String se
index += 1; // reserved
}
+ index = values.length - 2;
if (ignoreFixTime) {
position.setTime(dateFormat.parse(values[index]));
} else {
@@ -636,7 +660,7 @@ private void decodeStatus(Position position, Parser parser) {
private static final Pattern PATTERN_FRI = new PatternBuilder()
.text("+").expression("(?:RESP|BUFF):GT...,")
- .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .expression("(?:.{6}|.{10})?,") // protocol version
.number("(d{15}|x{14}),") // imei
.expression("(?:([0-9A-Z]{17}),)?") // vin
.expression("[^,]*,") // device name
@@ -764,7 +788,7 @@ private Object decodeFri(Channel channel, SocketAddress remoteAddress, String se
private static final Pattern PATTERN_ERI = new PatternBuilder()
.text("+").expression("(?:RESP|BUFF):GTERI,")
- .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .expression("(?:.{6}|.{10})?,") // protocol version
.number("(d{15}|x{14}),") // imei
.expression("[^,]*,") // device name
.number("(x{8}),") // mask
@@ -905,7 +929,7 @@ private Object decodeEri(Channel channel, SocketAddress remoteAddress, String se
private static final Pattern PATTERN_IGN = new PatternBuilder()
.text("+").expression("(?:RESP|BUFF):GTIG[NF],")
- .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .expression("(?:.{6}|.{10})?,") // protocol version
.number("(d{15}|x{14}),") // imei
.expression("[^,]*,") // device name
.number("d+,") // ignition off duration
@@ -939,7 +963,7 @@ private Object decodeIgn(Channel channel, SocketAddress remoteAddress, String se
private static final Pattern PATTERN_LSW = new PatternBuilder()
.text("+RESP:").expression("GT[LT]SW,")
- .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .expression("(?:.{6}|.{10})?,") // protocol version
.number("(d{15}|x{14}),") // imei
.expression("[^,]*,") // device name
.number("[01],") // type
@@ -970,7 +994,7 @@ private Object decodeLsw(Channel channel, SocketAddress remoteAddress, String se
private static final Pattern PATTERN_IDA = new PatternBuilder()
.text("+RESP:GTIDA,")
- .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .expression("(?:.{6}|.{10})?,") // protocol version
.number("(d{15}|x{14}),") // imei
.expression("[^,]*,,") // device name
.number("([^,]+),") // rfid
@@ -1006,7 +1030,7 @@ private Object decodeIda(Channel channel, SocketAddress remoteAddress, String se
private static final Pattern PATTERN_WIF = new PatternBuilder()
.text("+RESP:GTWIF,")
- .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .expression("(?:.{6}|.{10})?,") // protocol version
.number("(d{15}|x{14}),") // imei
.expression("[^,]*,") // device name
.number("(d+),") // count
@@ -1047,7 +1071,7 @@ private Object decodeWif(Channel channel, SocketAddress remoteAddress, String se
private static final Pattern PATTERN_GSM = new PatternBuilder()
.text("+RESP:GTGSM,")
- .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .expression("(?:.{6}|.{10})?,") // protocol version
.number("(d{15}|x{14}),") // imei
.expression("(?:STR|CTN|NMR|RTL),") // fix type
.expression("(.*)") // cells
@@ -1086,7 +1110,7 @@ private Object decodeGsm(Channel channel, SocketAddress remoteAddress, String se
private static final Pattern PATTERN_PNA = new PatternBuilder()
.text("+RESP:GT").expression("P[NF]A,")
- .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .expression("(?:.{6}|.{10})?,") // protocol version
.number("(d{15}|x{14}),") // imei
.expression("[^,]*,") // device name
.number("(dddd)(dd)(dd)") // date (yyyymmdd)
@@ -1112,7 +1136,7 @@ private Object decodePna(Channel channel, SocketAddress remoteAddress, String se
private static final Pattern PATTERN_DAR = new PatternBuilder()
.text("+RESP:GTDAR,")
- .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .expression("(?:.{6}|.{10})?,") // protocol version
.number("(d{15}|x{14}),") // imei
.expression("[^,]*,") // device name
.number("(d),") // warning type
@@ -1151,7 +1175,7 @@ private Object decodeDar(Channel channel, SocketAddress remoteAddress, String se
private static final Pattern PATTERN_DTT = new PatternBuilder()
.text("+RESP:GTDTT,")
- .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .expression("(?:.{6}|.{10})?,") // protocol version
.number("(d{15}|x{14}),") // imei
.expression("[^,]*,,,") // device name
.number("d,") // data type
@@ -1189,7 +1213,7 @@ private Object decodeDtt(Channel channel, SocketAddress remoteAddress, String se
private static final Pattern PATTERN_BAA = new PatternBuilder()
.text("+RESP:GTBAA,")
- .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .expression("(?:.{6}|.{10})?,") // protocol version
.number("(d{15}|x{14}),") // imei
.expression("[^,]*,") // device name
.number("x+,") // index
@@ -1245,7 +1269,7 @@ private Object decodeBaa(Channel channel, SocketAddress remoteAddress, String se
private static final Pattern PATTERN_BID = new PatternBuilder()
.text("+RESP:GTBID,")
- .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .expression("(?:.{6}|.{10})?,") // protocol version
.number("(d{15}|x{14}),") // imei
.expression("[^,]*,") // device name
.number("d,") // count
@@ -1287,7 +1311,7 @@ private Object decodeBid(Channel channel, SocketAddress remoteAddress, String se
private static final Pattern PATTERN_LSA = new PatternBuilder()
.text("+RESP:GTLSA,")
- .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .expression("(?:.{6}|.{10})?,") // protocol version
.number("(d{15}|x{14}),") // imei
.expression("[^,]*,") // device name
.number("d,") // event state 1
@@ -1327,7 +1351,7 @@ private Object decodeLsa(Channel channel, SocketAddress remoteAddress, String se
private static final Pattern PATTERN = new PatternBuilder()
.text("+").expression("(?:RESP|BUFF):GT...,")
- .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .expression("(?:.{6}|.{10})?,") // protocol version
.number("(d{15}|x{14}),") // imei
.expression("[^,]*,") // device name
.number("d*,")
diff --git a/src/main/java/org/traccar/protocol/Gt06ProtocolEncoder.java b/src/main/java/org/traccar/protocol/Gt06ProtocolEncoder.java
index dc5dd446f40..fd6bb845178 100644
--- a/src/main/java/org/traccar/protocol/Gt06ProtocolEncoder.java
+++ b/src/main/java/org/traccar/protocol/Gt06ProtocolEncoder.java
@@ -23,7 +23,6 @@
import org.traccar.helper.Checksum;
import org.traccar.helper.model.AttributeUtil;
import org.traccar.model.Command;
-import org.traccar.model.Device;
import java.nio.charset.StandardCharsets;
@@ -74,11 +73,11 @@ protected Object encodeCommand(Command command) {
String password = AttributeUtil.getDevicePassword(
getCacheManager(), command.getDeviceId(), getProtocolName(), "123456");
- Device device = getCacheManager().getObject(Device.class, command.getDeviceId());
+ String model = getDeviceModel(command.getDeviceId());
switch (command.getType()) {
case Command.TYPE_ENGINE_STOP:
- if ("G109".equals(device.getModel())) {
+ if ("G109".equals(model)) {
return encodeContent(command.getDeviceId(), "DYD#");
} else if (alternative) {
return encodeContent(command.getDeviceId(), "DYD," + password + "#");
@@ -86,7 +85,7 @@ protected Object encodeCommand(Command command) {
return encodeContent(command.getDeviceId(), "Relay,1#");
}
case Command.TYPE_ENGINE_RESUME:
- if ("G109".equals(device.getModel())) {
+ if ("G109".equals(model)) {
return encodeContent(command.getDeviceId(), "HFYD#");
} else if (alternative) {
return encodeContent(command.getDeviceId(), "HFYD," + password + "#");
diff --git a/src/main/java/org/traccar/protocol/HuabaoProtocolDecoder.java b/src/main/java/org/traccar/protocol/HuabaoProtocolDecoder.java
index a102e9e442b..2243bd982a1 100644
--- a/src/main/java/org/traccar/protocol/HuabaoProtocolDecoder.java
+++ b/src/main/java/org/traccar/protocol/HuabaoProtocolDecoder.java
@@ -471,8 +471,12 @@ private Position decodeLocation(DeviceSession deviceSession, ByteBuf buf) {
case 0x02:
position.set(Position.KEY_FUEL_LEVEL, buf.readUnsignedShort() * 0.1);
break;
+ case 0x25:
+ position.set(Position.KEY_INPUT, buf.readUnsignedInt());
+ break;
case 0x2b:
- position.set(Position.KEY_FUEL_CONSUMPTION, buf.readUnsignedInt());
+ position.set(Position.PREFIX_ADC + 1, buf.readUnsignedShort());
+ position.set(Position.PREFIX_ADC + 2, buf.readUnsignedShort());
break;
case 0x30:
position.set(Position.KEY_RSSI, buf.readUnsignedByte());
@@ -517,6 +521,36 @@ private Position decodeLocation(DeviceSession deviceSession, ByteBuf buf) {
buf.readUnsignedByte(); // rssi
}
break;
+ case 0x64:
+ buf.readUnsignedInt(); // alarm serial number
+ buf.readUnsignedByte(); // alarm status
+ position.set("adasAlarm", buf.readUnsignedByte());
+ break;
+ case 0x65:
+ buf.readUnsignedInt(); // alarm serial number
+ buf.readUnsignedByte(); // alarm status
+ position.set("dmsAlarm", buf.readUnsignedByte());
+ break;
+ case 0x70:
+ buf.readUnsignedInt(); // alarm serial number
+ buf.readUnsignedByte(); // alarm status
+ switch (buf.readUnsignedByte()) {
+ case 0x01:
+ position.set(Position.KEY_ALARM, Position.ALARM_ACCELERATION);
+ break;
+ case 0x02:
+ position.set(Position.KEY_ALARM, Position.ALARM_BRAKING);
+ break;
+ case 0x03:
+ position.set(Position.KEY_ALARM, Position.ALARM_CORNERING);
+ break;
+ case 0x16:
+ position.set(Position.KEY_ALARM, Position.ALARM_ACCIDENT);
+ break;
+ default:
+ break;
+ }
+ break;
case 0x69:
position.set(Position.KEY_BATTERY, buf.readUnsignedShort() * 0.01);
break;
@@ -571,7 +605,11 @@ private Position decodeLocation(DeviceSession deviceSession, ByteBuf buf) {
break;
case 0xD4:
case 0xE1:
- position.set(Position.KEY_BATTERY_LEVEL, buf.readUnsignedByte());
+ if (length == 1) {
+ position.set(Position.KEY_BATTERY_LEVEL, buf.readUnsignedByte());
+ } else {
+ position.set(Position.KEY_DRIVER_UNIQUE_ID, String.valueOf(buf.readUnsignedInt()));
+ }
break;
case 0xD5:
if (length == 2) {
@@ -594,6 +632,9 @@ private Position decodeLocation(DeviceSession deviceSession, ByteBuf buf) {
position.set(Position.KEY_MOTION, BitUtil.check(deviceStatus, 2));
position.set("cover", BitUtil.check(deviceStatus, 3));
break;
+ case 0xE2:
+ position.set(Position.KEY_FUEL_LEVEL, buf.readUnsignedInt() * 0.1);
+ break;
case 0xE6:
while (buf.readerIndex() < endIndex) {
int sensorIndex = buf.readUnsignedByte();
diff --git a/src/main/java/org/traccar/protocol/LaipacProtocolDecoder.java b/src/main/java/org/traccar/protocol/LaipacProtocolDecoder.java
index 5745909c710..343d42e09df 100644
--- a/src/main/java/org/traccar/protocol/LaipacProtocolDecoder.java
+++ b/src/main/java/org/traccar/protocol/LaipacProtocolDecoder.java
@@ -28,7 +28,6 @@
import org.traccar.model.CellTower;
import org.traccar.model.Network;
import org.traccar.model.Position;
-import org.traccar.model.Device;
import org.traccar.helper.BitUtil;
import java.net.SocketAddress;
@@ -226,7 +225,7 @@ protected Object decodeAvrmc(
return null;
}
- String model = getCacheManager().getObject(Device.class, deviceSession.getDeviceId()).getModel();
+ String model = getDeviceModel(deviceSession);
Position position = new Position(getProtocolName());
diff --git a/src/main/java/org/traccar/protocol/MeiligaoProtocolEncoder.java b/src/main/java/org/traccar/protocol/MeiligaoProtocolEncoder.java
index 5859d91ce22..6d3b4f7e93a 100644
--- a/src/main/java/org/traccar/protocol/MeiligaoProtocolEncoder.java
+++ b/src/main/java/org/traccar/protocol/MeiligaoProtocolEncoder.java
@@ -24,7 +24,6 @@
import org.traccar.helper.DataConverter;
import org.traccar.helper.model.AttributeUtil;
import org.traccar.model.Command;
-import org.traccar.model.Device;
import java.nio.charset.StandardCharsets;
import java.util.Set;
@@ -64,7 +63,7 @@ private ByteBuf encodeOutputCommand(long deviceId, int index, int value) {
int outputCount;
int outputType;
- String model = getCacheManager().getObject(Device.class, deviceId).getModel();
+ String model = getDeviceModel(deviceId);
if (model != null && Set.of("TK510", "GT08", "TK208", "TK228", "MT05").contains(model)) {
outputCount = 5;
diff --git a/src/main/java/org/traccar/protocol/MeitrackProtocolDecoder.java b/src/main/java/org/traccar/protocol/MeitrackProtocolDecoder.java
index c37d1fe4750..88b6380a506 100644
--- a/src/main/java/org/traccar/protocol/MeitrackProtocolDecoder.java
+++ b/src/main/java/org/traccar/protocol/MeitrackProtocolDecoder.java
@@ -20,7 +20,6 @@
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import org.traccar.BaseProtocolDecoder;
-import org.traccar.model.Device;
import org.traccar.session.DeviceSession;
import org.traccar.NetworkMessage;
import org.traccar.Protocol;
@@ -38,6 +37,7 @@
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
+import java.util.Objects;
import java.util.regex.Pattern;
public class MeitrackProtocolDecoder extends BaseProtocolDecoder {
@@ -206,11 +206,7 @@ private Position decodeRegular(Channel channel, SocketAddress remoteAddress, Byt
position.set(Position.PREFIX_ADC + i, parser.nextHexInt());
}
- String model = getCacheManager().getObject(Device.class, deviceSession.getDeviceId()).getModel();
- if (model == null) {
- model = "";
- }
- switch (model.toUpperCase()) {
+ switch (Objects.requireNonNullElse(getDeviceModel(deviceSession), "").toUpperCase()) {
case "MVT340":
case "MVT380":
position.set(Position.KEY_BATTERY, parser.nextHexInt() * 3.0 * 2.0 / 1024.0);
@@ -465,6 +461,9 @@ private List decodeBinaryE(Channel channel, SocketAddress remoteAddres
case 0x16:
position.set(Position.PREFIX_ADC + 1, buf.readUnsignedShortLE() * 0.01);
break;
+ case 0x17:
+ position.set(Position.PREFIX_ADC + 2, buf.readUnsignedShortLE() * 0.01);
+ break;
case 0x19:
position.set(Position.KEY_BATTERY, buf.readUnsignedShortLE() * 0.01);
break;
diff --git a/src/main/java/org/traccar/protocol/Minifinder2Protocol.java b/src/main/java/org/traccar/protocol/Minifinder2Protocol.java
index c12933b8166..082b9146d34 100644
--- a/src/main/java/org/traccar/protocol/Minifinder2Protocol.java
+++ b/src/main/java/org/traccar/protocol/Minifinder2Protocol.java
@@ -31,7 +31,8 @@ public class Minifinder2Protocol extends BaseProtocol {
@Inject
public Minifinder2Protocol(Config config) {
setSupportedDataCommands(
- Command.TYPE_FIRMWARE_UPDATE);
+ Command.TYPE_FIRMWARE_UPDATE,
+ Command.TYPE_CONFIGURATION);
addServer(new TrackerServer(config, getName(), false) {
@Override
protected void addProtocolHandlers(PipelineBuilder pipeline, Config config) {
diff --git a/src/main/java/org/traccar/protocol/Minifinder2ProtocolDecoder.java b/src/main/java/org/traccar/protocol/Minifinder2ProtocolDecoder.java
index 57ceab4c7e8..64373e3446d 100644
--- a/src/main/java/org/traccar/protocol/Minifinder2ProtocolDecoder.java
+++ b/src/main/java/org/traccar/protocol/Minifinder2ProtocolDecoder.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 - 2023 Anton Tananaev (anton@traccar.org)
+ * Copyright 2019 - 2024 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.
@@ -20,6 +20,7 @@
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import org.traccar.BaseProtocolDecoder;
+import org.traccar.helper.BufferUtil;
import org.traccar.session.DeviceSession;
import org.traccar.NetworkMessage;
import org.traccar.Protocol;
@@ -315,6 +316,10 @@ protected Object decode(
return positions;
+ } else if (type == MSG_CONFIGURATION) {
+
+ return decodeConfiguration(channel, remoteAddress, buf);
+
} else if (type == MSG_RESPONSE) {
DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
@@ -337,4 +342,169 @@ protected Object decode(
return null;
}
+ private Position decodeConfiguration(Channel channel, SocketAddress remoteAddress, ByteBuf buf) {
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ getLastLocation(position, null);
+
+ while (buf.isReadable()) {
+ int length = buf.readUnsignedByte() - 1;
+ int endIndex = buf.readerIndex() + length + 1;
+ int key = buf.readUnsignedByte();
+
+ switch (key) {
+ case 0x01:
+ position.set("moduleNumber", buf.readUnsignedInt());
+ break;
+ case 0x02:
+ position.set(Position.KEY_VERSION_FW, String.valueOf(buf.readUnsignedInt()));
+ break;
+ case 0x03:
+ position.set("imei", buf.readCharSequence(length, StandardCharsets.US_ASCII).toString());
+ break;
+ case 0x04:
+ position.set(Position.KEY_ICCID, BufferUtil.readString(buf, length));
+ break;
+ case 0x05:
+ position.set("bleMac", ByteBufUtil.hexDump(buf.readSlice(length)));
+ break;
+ case 0x06:
+ position.set("settingTime", buf.readUnsignedInt());
+ break;
+ case 0x07:
+ position.set("runTimes", buf.readUnsignedInt());
+ break;
+ case 0x0A:
+ position.set("interval", buf.readUnsignedMedium());
+ position.set("petMode", buf.readUnsignedByte());
+ break;
+ case 0x0D:
+ position.set("passwordProtect", buf.readUnsignedInt());
+ break;
+ case 0x0E:
+ position.set("timeZone", (int) buf.readByte());
+ break;
+ case 0x0F:
+ position.set("enableControl", buf.readUnsignedInt());
+ break;
+ case 0x13:
+ position.set("deviceName", BufferUtil.readString(buf, length));
+ break;
+ case 0x14:
+ position.set(Position.KEY_BATTERY_LEVEL, buf.readUnsignedByte());
+ position.set(Position.KEY_BATTERY, buf.readUnsignedShort() * 0.001);
+ break;
+ case 0x15:
+ position.set("bleLatitude", buf.readIntLE() * 0.0000001);
+ position.set("bleLongitude", buf.readIntLE() * 0.0000001);
+ position.set("bleLocation", BufferUtil.readString(buf, length - 8));
+ break;
+ case 0x17:
+ position.set("gpsUrl", BufferUtil.readString(buf, length));
+ break;
+ case 0x18:
+ position.set("lbsUrl", BufferUtil.readString(buf, length));
+ break;
+ case 0x1A:
+ position.set("firmware", BufferUtil.readString(buf, length));
+ break;
+ case 0x1B:
+ position.set("gsmModule", BufferUtil.readString(buf, length));
+ break;
+ case 0x1D:
+ position.set("agpsUpdate", buf.readUnsignedByte());
+ position.set("agpsLatitude", buf.readIntLE() * 0.0000001);
+ position.set("agpsLongitude", buf.readIntLE() * 0.0000001);
+ break;
+ case 0x30:
+ position.set("numberFlag", buf.readUnsignedByte());
+ position.set("number", BufferUtil.readString(buf, length - 1));
+ break;
+ case 0x31:
+ position.set("prefixFlag", buf.readUnsignedByte());
+ position.set("prefix", BufferUtil.readString(buf, length - 1));
+ break;
+ case 0x33:
+ position.set("phoneSwitches", buf.readUnsignedByte());
+ break;
+ case 0x40:
+ position.set("apn", BufferUtil.readString(buf, length));
+ break;
+ case 0x41:
+ position.set("apnUser", BufferUtil.readString(buf, length));
+ break;
+ case 0x42:
+ position.set("apnPassword", BufferUtil.readString(buf, length));
+ break;
+ case 0x43:
+ buf.readUnsignedByte(); // flag
+ position.set("port", buf.readUnsignedShort());
+ position.set("server", BufferUtil.readString(buf, length - 3));
+ break;
+ case 0x44:
+ position.set("heartbeatInterval", buf.readUnsignedInt());
+ position.set("uploadInterval", buf.readUnsignedInt());
+ position.set("uploadLazyInterval", buf.readUnsignedInt());
+ break;
+ case 0x47:
+ position.set("deviceId", BufferUtil.readString(buf, length));
+ break;
+ case 0x4E:
+ position.set("gsmBand", buf.readUnsignedByte());
+ break;
+ case 0x50:
+ position.set("powerAlert", buf.readUnsignedInt());
+ break;
+ case 0x51:
+ position.set("geoAlert", buf.readUnsignedInt());
+ break;
+ case 0x53:
+ position.set("motionAlert", buf.readUnsignedInt());
+ break;
+ case 0x5C:
+ position.set("barkLevel", buf.readUnsignedByte());
+ position.set("barkInterval", buf.readUnsignedInt());
+ break;
+ case 0x61:
+ position.set("msisdn", BufferUtil.readString(buf, length));
+ break;
+ case 0x62:
+ position.set("wifiWhitelist", buf.readUnsignedByte());
+ position.set("wifiWhitelistMac", ByteBufUtil.hexDump(buf.readSlice(6)));
+ break;
+ case 0x64:
+ position.set(Position.KEY_RSSI, buf.readUnsignedByte());
+ position.set("networkBand", buf.readUnsignedInt());
+ position.set(Position.KEY_OPERATOR, BufferUtil.readString(buf, length - 5));
+ break;
+ case 0x65:
+ position.set(Position.KEY_RSSI, buf.readUnsignedByte());
+ position.set("networkStatus", buf.readUnsignedByte());
+ position.set("serverStatus", buf.readUnsignedByte());
+ position.set("networkPlmn", ByteBufUtil.hexDump(buf.readSlice(6)));
+ position.set("homePlmn", ByteBufUtil.hexDump(buf.readSlice(6)));
+ break;
+ case 0x66:
+ position.set("imsi", BufferUtil.readString(buf, length));
+ break;
+ case 0x75:
+ position.set("extraEnableControl", buf.readUnsignedInt());
+ break;
+ default:
+ break;
+ }
+
+ buf.readerIndex(endIndex);
+ }
+
+ return position;
+ }
+
}
diff --git a/src/main/java/org/traccar/protocol/Minifinder2ProtocolEncoder.java b/src/main/java/org/traccar/protocol/Minifinder2ProtocolEncoder.java
index fab3c3a6d4a..6e330a4ddda 100644
--- a/src/main/java/org/traccar/protocol/Minifinder2ProtocolEncoder.java
+++ b/src/main/java/org/traccar/protocol/Minifinder2ProtocolEncoder.java
@@ -21,7 +21,6 @@
import org.traccar.Protocol;
import org.traccar.helper.Checksum;
import org.traccar.model.Command;
-import org.traccar.model.Device;
import java.nio.charset.StandardCharsets;
@@ -48,8 +47,14 @@ private ByteBuf encodeContent(ByteBuf content) {
@Override
protected Object encodeCommand(Command command) {
- Device device = getCacheManager().getObject(Device.class, command.getDeviceId());
- if ("Nano".equalsIgnoreCase(device.getModel())) {
+ if (command.getType().equals(Command.TYPE_CONFIGURATION)) {
+ ByteBuf content = Unpooled.buffer();
+ content.writeByte(Minifinder2ProtocolDecoder.MSG_CONFIGURATION);
+ content.writeByte(1); // length
+ content.writeByte(0xF0); // type
+ }
+
+ if ("Nano".equalsIgnoreCase(getDeviceModel(command.getDeviceId()))) {
ByteBuf content = Unpooled.buffer();
if (command.getType().equals(Command.TYPE_FIRMWARE_UPDATE)) {
String url = command.getString(Command.KEY_DATA);
diff --git a/src/main/java/org/traccar/protocol/Mta6ProtocolDecoder.java b/src/main/java/org/traccar/protocol/Mta6ProtocolDecoder.java
index 896c7a2d2c8..9704cf09959 100644
--- a/src/main/java/org/traccar/protocol/Mta6ProtocolDecoder.java
+++ b/src/main/java/org/traccar/protocol/Mta6ProtocolDecoder.java
@@ -96,7 +96,7 @@ public float readFloat(ByteBuf buf) {
}
- private static class TimeReader extends FloatReader {
+ private static final class TimeReader extends FloatReader {
private long weekNumber;
diff --git a/src/main/java/org/traccar/protocol/RstProtocolDecoder.java b/src/main/java/org/traccar/protocol/RstProtocolDecoder.java
index 2493f0d9f88..eafa4d3d77c 100644
--- a/src/main/java/org/traccar/protocol/RstProtocolDecoder.java
+++ b/src/main/java/org/traccar/protocol/RstProtocolDecoder.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 - 2023 Anton Tananaev (anton@traccar.org)
+ * Copyright 2019 - 2024 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.
@@ -143,6 +143,17 @@ protected Object decode(
return position;
+ } else if (type == 134) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ getLastLocation(position, null);
+
+ position.set(Position.KEY_RESULT, String.valueOf(type));
+
+ return position;
+
} else {
return null;
diff --git a/src/main/java/org/traccar/protocol/RuptelaProtocolDecoder.java b/src/main/java/org/traccar/protocol/RuptelaProtocolDecoder.java
index cde626c5f1a..e1efb5757ba 100644
--- a/src/main/java/org/traccar/protocol/RuptelaProtocolDecoder.java
+++ b/src/main/java/org/traccar/protocol/RuptelaProtocolDecoder.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2013 - 2022 Anton Tananaev (anton@traccar.org)
+ * Copyright 2013 - 2024 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.
@@ -112,6 +112,10 @@ private void decodeParameter(Position position, int id, ByteBuf buf, int length)
case 5:
position.set(Position.PREFIX_IN + (id - 1), readValue(buf, length, false));
break;
+ case 13:
+ case 173:
+ position.set(Position.KEY_MOTION, readValue(buf, length, false) > 0);
+ break;
case 20:
position.set(Position.PREFIX_ADC + 3, readValue(buf, length, false));
break;
@@ -133,8 +137,11 @@ private void decodeParameter(Position position, int id, ByteBuf buf, int length)
case 32:
position.set(Position.KEY_DEVICE_TEMP, readValue(buf, length, true));
break;
+ case 39:
+ position.set(Position.KEY_ENGINE_LOAD, readValue(buf, length, false));
+ break;
case 65:
- position.set(Position.KEY_ODOMETER, readValue(buf, length, true));
+ position.set(Position.KEY_ODOMETER, readValue(buf, length, false));
break;
case 74:
position.set(Position.PREFIX_TEMP + 3, readValue(buf, length, true) * 0.1);
@@ -149,8 +156,17 @@ private void decodeParameter(Position position, int id, ByteBuf buf, int length)
position.set(Position.KEY_ALARM, Position.ALARM_JAMMING);
}
break;
+ case 94:
+ position.set(Position.KEY_RPM, readValue(buf, length, false) * 0.25);
+ break;
case 95:
- position.set(Position.KEY_OBD_SPEED, readValue(buf, length, true));
+ position.set(Position.KEY_OBD_SPEED, readValue(buf, length, false));
+ break;
+ case 98:
+ position.set(Position.KEY_FUEL_LEVEL, readValue(buf, length, false) * 100 / 255.0);
+ break;
+ case 100:
+ position.set(Position.KEY_FUEL_CONSUMPTION, readValue(buf, length, false) / 20.0);
break;
case 134:
if (readValue(buf, length, false) > 0) {
@@ -165,15 +181,31 @@ private void decodeParameter(Position position, int id, ByteBuf buf, int length)
case 150:
position.set(Position.KEY_OPERATOR, readValue(buf, length, false));
break;
- case 170:
- position.set(Position.KEY_CHARGE, readValue(buf, length, false) > 0);
+ case 163:
+ position.set(Position.KEY_ODOMETER, readValue(buf, length, false) * 5);
break;
- case 173:
- position.set(Position.KEY_MOTION, readValue(buf, length, false) > 0);
+ case 164:
+ position.set(Position.KEY_ODOMETER_TRIP, readValue(buf, length, false) * 5);
+ break;
+ case 165:
+ position.set(Position.KEY_OBD_SPEED, readValue(buf, length, false) / 256.0);
break;
+ case 166:
case 197:
position.set(Position.KEY_RPM, readValue(buf, length, false) * 0.125);
break;
+ case 170:
+ position.set(Position.KEY_CHARGE, readValue(buf, length, false) > 0);
+ break;
+ case 205:
+ position.set(Position.KEY_FUEL_LEVEL, readValue(buf, length, false));
+ break;
+ case 207:
+ position.set(Position.KEY_FUEL_LEVEL, readValue(buf, length, false) * 0.4);
+ break;
+ case 208:
+ position.set(Position.KEY_FUEL_USED, readValue(buf, length, false) * 0.5);
+ break;
case 251:
case 409:
position.set(Position.KEY_IGNITION, readValue(buf, length, false) > 0);
@@ -194,7 +226,7 @@ private void decodeParameter(Position position, int id, ByteBuf buf, int length)
}
break;
case 645:
- position.set(Position.KEY_OBD_ODOMETER, readValue(buf, length, true) * 1000);
+ position.set(Position.KEY_OBD_ODOMETER, readValue(buf, length, false) * 1000);
break;
case 758:
if (readValue(buf, length, false) == 1) {
diff --git a/src/main/java/org/traccar/protocol/SigfoxProtocolDecoder.java b/src/main/java/org/traccar/protocol/SigfoxProtocolDecoder.java
index 6f739a1a4d3..e0dfab9b258 100644
--- a/src/main/java/org/traccar/protocol/SigfoxProtocolDecoder.java
+++ b/src/main/java/org/traccar/protocol/SigfoxProtocolDecoder.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017 - 2020 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 - 2024 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.
@@ -156,18 +156,30 @@ protected Object decode(
ByteBuf buf = Unpooled.wrappedBuffer(DataConverter.parseHex(json.getString("data")));
try {
- int event = buf.readUnsignedByte();
- if (event == 0x0f || event == 0x1f) {
+ int header = buf.readUnsignedByte();
+ if ("Amber".equals(getDeviceModel(deviceSession))) {
+
+ int flags = buf.readUnsignedByte();
+ position.set(Position.KEY_MOTION, BitUtil.check(flags, 1));
+
+ position.set(Position.KEY_BATTERY, buf.readUnsignedByte() * 0.02);
+ position.set(Position.PREFIX_TEMP + 1, (int) buf.readByte());
+
+ position.setValid(true);
+ position.setLatitude(buf.readInt() / 60000.0);
+ position.setLongitude(buf.readInt() / 60000.0);
+
+ } else if (header == 0x0f || header == 0x1f) {
- position.setValid(event >> 4 > 0);
+ position.setValid(header >> 4 > 0);
position.setLatitude(BufferUtil.readSignedMagnitudeInt(buf) * 0.000001);
position.setLongitude(BufferUtil.readSignedMagnitudeInt(buf) * 0.000001);
position.set(Position.KEY_BATTERY, (int) buf.readUnsignedByte());
- } else if (event >> 4 <= 3 && buf.writerIndex() == 12) {
+ } else if (header >> 4 <= 3 && buf.writerIndex() == 12) {
- if (BitUtil.to(event, 4) == 0) {
+ if (BitUtil.to(header, 4) == 0) {
position.setValid(true);
position.setLatitude(buf.readIntLE() * 0.0000001);
position.setLongitude(buf.readIntLE() * 0.0000001);
diff --git a/src/main/java/org/traccar/protocol/StartekFrameDecoder.java b/src/main/java/org/traccar/protocol/StartekFrameDecoder.java
new file mode 100644
index 00000000000..608b128cdac
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/StartekFrameDecoder.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2024 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.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import org.traccar.BaseFrameDecoder;
+
+import java.nio.charset.StandardCharsets;
+
+public class StartekFrameDecoder extends BaseFrameDecoder {
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ if (buf.readableBytes() < 10) {
+ return null;
+ }
+
+ int lengthIndex = buf.readerIndex() + 3;
+ int dividerIndex = buf.indexOf(lengthIndex, buf.writerIndex(), (byte) ',');
+ if (dividerIndex > 0) {
+ int lengthOffset = dividerIndex - buf.readerIndex() + 4;
+ int length = lengthOffset + Integer.parseInt(buf.getCharSequence(
+ lengthIndex, dividerIndex - lengthIndex, StandardCharsets.US_ASCII).toString());
+ if (buf.readableBytes() >= length) {
+ return buf.readRetainedSlice(length);
+ }
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/StartekProtocol.java b/src/main/java/org/traccar/protocol/StartekProtocol.java
index 5505453458a..9c01d24e2e1 100644
--- a/src/main/java/org/traccar/protocol/StartekProtocol.java
+++ b/src/main/java/org/traccar/protocol/StartekProtocol.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 - 2023 Anton Tananaev (anton@traccar.org)
+ * Copyright 2021 - 2024 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.
@@ -15,7 +15,6 @@
*/
package org.traccar.protocol;
-import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import org.traccar.BaseProtocol;
@@ -38,7 +37,7 @@ public StartekProtocol(Config config) {
addServer(new TrackerServer(config, getName(), false) {
@Override
protected void addProtocolHandlers(PipelineBuilder pipeline, Config config) {
- pipeline.addLast(new LineBasedFrameDecoder(1100));
+ pipeline.addLast(new StartekFrameDecoder());
pipeline.addLast(new StringEncoder());
pipeline.addLast(new StringDecoder());
pipeline.addLast(new StartekProtocolEncoder(StartekProtocol.this));
diff --git a/src/main/java/org/traccar/protocol/StartekProtocolDecoder.java b/src/main/java/org/traccar/protocol/StartekProtocolDecoder.java
index 5cfbb36ca77..0eeb5b2aa7b 100644
--- a/src/main/java/org/traccar/protocol/StartekProtocolDecoder.java
+++ b/src/main/java/org/traccar/protocol/StartekProtocolDecoder.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 - 2022 Anton Tananaev (anton@traccar.org)
+ * Copyright 2021 - 2024 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.
@@ -41,12 +41,13 @@ public StartekProtocolDecoder(Protocol protocol) {
.expression(".") // index
.number("d+,") // length
.number("(d+),") // imei
+ .number("(xxx),") // type
.expression("(.+)") // content
.number("xx") // checksum
+ .text("\r\n")
.compile();
private static final Pattern PATTERN_POSITION = new PatternBuilder()
- .number("xxx,") // command
.number("(d+),") // event
.expression("([^,]+)?,") // event data
.number("(dd)(dd)(dd)") // date (yyymmdd)
@@ -103,6 +104,12 @@ private String decodeAlarm(int value) {
case 5:
case 6:
return Position.ALARM_DOOR;
+ case 17:
+ return Position.ALARM_LOW_POWER;
+ case 18:
+ return Position.ALARM_POWER_CUT;
+ case 19:
+ return Position.ALARM_POWER_RESTORED;
case 39:
return Position.ALARM_ACCELERATION;
case 40:
@@ -128,26 +135,24 @@ protected Object decode(
return null;
}
+ String type = parser.next();
String content = parser.next();
- if (content.length() < 100) {
-
- Position position = new Position(getProtocolName());
- position.setDeviceId(deviceSession.getDeviceId());
-
- getLastLocation(position, null);
-
- position.set(Position.KEY_RESULT, content);
-
- return position;
-
- } else {
-
- return decodePosition(deviceSession, content);
-
+ switch (type) {
+ case "000":
+ return decodePosition(deviceSession, content);
+ case "710":
+ return decodeSerial(deviceSession, content);
+ default:
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+ getLastLocation(position, null);
+ position.set(Position.KEY_TYPE, type);
+ position.set(Position.KEY_RESULT, content);
+ return position;
}
}
- protected Object decodePosition(DeviceSession deviceSession, String content) throws Exception {
+ private Object decodePosition(DeviceSession deviceSession, String content) {
Parser parser = new Parser(PATTERN_POSITION, content);
if (!parser.matches()) {
@@ -249,4 +254,81 @@ protected Object decodePosition(DeviceSession deviceSession, String content) thr
return position;
}
+ private Object decodeSerial(DeviceSession deviceSession, String content) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ getLastLocation(position, null);
+
+ String[] frames = content.split("\r\n");
+
+ for (String frame : frames) {
+ String[] values = frame.split(",");
+ int index = 0;
+ String type = values[index++];
+ switch (type) {
+ case "T1":
+ index += 1; // speed
+ position.set(Position.KEY_RPM, Double.parseDouble(values[index++]));
+ index += 1; // fuel consumption
+ position.set(Position.KEY_FUEL_LEVEL, Double.parseDouble(values[index++]));
+ index += 4; // axel weights
+ index += 1; // turbo pressure
+ position.set(Position.KEY_COOLANT_TEMP, Integer.parseInt(values[index++]));
+ index += 1; // accelerator pedal
+ position.set("torque", Integer.parseInt(values[index++]));
+ index += 1; // firmware version
+ position.set(Position.KEY_POWER, Double.parseDouble(values[index++]));
+ index += 1; // coolant level
+ position.set("oilTemp", Double.parseDouble(values[index++]));
+ index += 1; // oil level
+ position.set(Position.KEY_THROTTLE, Double.parseDouble(values[index++]));
+ index += 1; // air inlet pressure
+ index += 1; // fuel tank secondary
+ index += 1; // current gear
+ index += 1; // seatbelt
+ position.set("oilPressure", Integer.parseInt(values[index++]));
+ index += 1; // wet tank air pressure
+ index += 1; // pto state
+ int ignition = Integer.parseInt(values[index++]);
+ if (ignition < 2) {
+ position.set(Position.KEY_IGNITION, ignition > 0);
+ }
+ index += 1; // brake pedal
+ position.set("catalystLevel", Double.parseDouble(values[index++]));
+ index += 1; // fuel type
+ break;
+ case "T2":
+ position.set(Position.KEY_ODOMETER, Double.parseDouble(values[index++]) * 1000);
+ index += 1; // total fuel
+ index += 1; // fuel used cruise
+ index += 1; // fuel used drive
+ index += 1;
+ index += 1;
+ index += 1; // total idle time
+ index += 1; // total time pto
+ index += 1; // time cruise
+ index += 1;
+ index += 1;
+ index += 1;
+ index += 1;
+ index += 1;
+ index += 1; // brake apps
+ index += 1; // clutch apps
+ position.set(Position.KEY_HOURS, Integer.parseInt(values[index++]));
+ index += 1; // time torque
+ position.set(Position.KEY_FUEL_CONSUMPTION, Double.parseDouble(values[index++]));
+ index += 1; // total cruise control distance
+ position.set(Position.KEY_FUEL_USED, Double.parseDouble(values[index++]));
+ index += 1; // total drive time
+ break;
+ default:
+ break;
+ }
+ }
+
+ return position;
+ }
+
}
diff --git a/src/main/java/org/traccar/protocol/TeltonikaFrameDecoder.java b/src/main/java/org/traccar/protocol/TeltonikaFrameDecoder.java
index 3a096258415..aabc24887cb 100644
--- a/src/main/java/org/traccar/protocol/TeltonikaFrameDecoder.java
+++ b/src/main/java/org/traccar/protocol/TeltonikaFrameDecoder.java
@@ -29,7 +29,7 @@ public class TeltonikaFrameDecoder extends BaseFrameDecoder {
protected Object decode(
ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
- while (buf.isReadable() && buf.getByte(buf.readerIndex()) == (byte) 0xff) {
+ if (buf.isReadable() && buf.getByte(buf.readerIndex()) == (byte) 0xff) {
return buf.readRetainedSlice(1);
}
diff --git a/src/main/java/org/traccar/protocol/TeltonikaProtocolDecoder.java b/src/main/java/org/traccar/protocol/TeltonikaProtocolDecoder.java
index 537990265f3..6197c6c1394 100644
--- a/src/main/java/org/traccar/protocol/TeltonikaProtocolDecoder.java
+++ b/src/main/java/org/traccar/protocol/TeltonikaProtocolDecoder.java
@@ -21,7 +21,6 @@
import io.netty.channel.Channel;
import org.traccar.BaseProtocolDecoder;
import org.traccar.helper.BufferUtil;
-import org.traccar.model.Device;
import org.traccar.session.DeviceSession;
import org.traccar.NetworkMessage;
import org.traccar.Protocol;
@@ -231,7 +230,7 @@ private static void register(int id, Set models, BiConsumer p.set(Position.PREFIX_TEMP + 3, b.readInt() * 0.1));
register(75, fmbXXX, (p, b) -> p.set(Position.PREFIX_TEMP + 4, b.readInt() * 0.1));
register(78, null, (p, b) -> {
- long driverUniqueId = b.readLong();
+ long driverUniqueId = b.readLongLE();
if (driverUniqueId > 0) {
p.set(Position.KEY_DRIVER_UNIQUE_ID, String.format("%016X", driverUniqueId));
}
@@ -654,7 +653,6 @@ private List parseData(
if (deviceSession == null) {
return null;
}
- String model = getCacheManager().getObject(Device.class, deviceSession.getDeviceId()).getModel();
for (int i = 0; i < count; i++) {
Position position = new Position(getProtocolName());
@@ -680,7 +678,7 @@ private List parseData(
} else if (codec == CODEC_12) {
decodeSerial(channel, remoteAddress, deviceSession, position, buf);
} else {
- decodeLocation(position, buf, codec, model);
+ decodeLocation(position, buf, codec, getDeviceModel(deviceSession));
}
if (!position.getOutdated() || !position.getAttributes().isEmpty()) {
diff --git a/src/main/java/org/traccar/protocol/Tlt2hProtocolDecoder.java b/src/main/java/org/traccar/protocol/Tlt2hProtocolDecoder.java
index 2e7cdde4e70..4b11cbc74a7 100644
--- a/src/main/java/org/traccar/protocol/Tlt2hProtocolDecoder.java
+++ b/src/main/java/org/traccar/protocol/Tlt2hProtocolDecoder.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2013 - 2022 Anton Tananaev (anton@traccar.org)
+ * Copyright 2013 - 2024 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.
@@ -55,7 +55,7 @@ public Tlt2hProtocolDecoder(Protocol protocol) {
private static final Pattern PATTERN_POSITION = new PatternBuilder()
.text("#")
- .number("(?:(dd)|x*)") // cell or voltage
+ .number("(?:(dd|dddd)|x*)") // cell or voltage
.groupBegin()
.number("#(d+),") // mcc
.number("(d+),") // mnc
@@ -63,8 +63,9 @@ public Tlt2hProtocolDecoder(Protocol protocol) {
.number("(x+)") // cell id
.groupEnd("?")
.text("$GPRMC,")
- .number("(dd)(dd)(dd).d+,") // time (hhmmss.sss)
+ .number("(?:(dd)(dd)(dd).d+)?,") // time (hhmmss.sss)
.expression("([AVL]),") // validity
+ .groupBegin()
.number("(d+)(dd.d+),") // latitude
.expression("([NS]),")
.number("(d+)(dd.d+),") // longitude
@@ -72,12 +73,13 @@ public Tlt2hProtocolDecoder(Protocol protocol) {
.number("(d+.?d*)?,") // speed
.number("(d+.?d*)?,") // course
.number("(dd)(dd)(dd)") // date (ddmmyy)
+ .groupEnd("?")
.any()
.compile();
private static final Pattern PATTERN_WIFI = new PatternBuilder()
.text("#")
- .number("(?:(dd)|x+)") // cell or voltage
+ .number("(?:(dd|dddd)|x+)") // cell or voltage
.expression("#?")
.groupBegin()
.number("(d+),") // mcc
@@ -179,7 +181,8 @@ protected Object decode(
if (parser.matches()) {
if (parser.hasNext()) {
- position.set(Position.KEY_BATTERY, parser.nextInt() * 0.1);
+ int voltage = parser.nextInt();
+ position.set(Position.KEY_BATTERY, voltage > 100 ? voltage * 0.001 : voltage * 0.1);
}
if (parser.hasNext(4)) {
@@ -189,17 +192,26 @@ protected Object decode(
position.setNetwork(network);
}
- DateBuilder dateBuilder = new DateBuilder()
- .setTime(parser.nextInt(), parser.nextInt(), parser.nextInt());
+ DateBuilder dateBuilder = new DateBuilder();
+ if (parser.hasNext(3)) {
+ dateBuilder.setTime(parser.nextInt(), parser.nextInt(), parser.nextInt());
+ }
position.setValid(parser.next().equals("A"));
- position.setLatitude(parser.nextCoordinate());
- position.setLongitude(parser.nextCoordinate());
- position.setSpeed(parser.nextDouble(0));
- position.setCourse(parser.nextDouble(0));
- dateBuilder.setDateReverse(parser.nextInt(), parser.nextInt(), parser.nextInt());
- position.setTime(dateBuilder.getDate());
+ if (parser.hasNext()) {
+
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+
+ dateBuilder.setDateReverse(parser.nextInt(), parser.nextInt(), parser.nextInt());
+ position.setTime(dateBuilder.getDate());
+
+ } else {
+ getLastLocation(position, null);
+ }
} else {
continue;
@@ -211,7 +223,8 @@ protected Object decode(
if (parser.matches()) {
if (parser.hasNext()) {
- position.set(Position.KEY_BATTERY, parser.nextInt() * 0.1);
+ int voltage = parser.nextInt();
+ position.set(Position.KEY_BATTERY, voltage > 100 ? voltage * 0.001 : voltage * 0.1);
}
Network network = new Network();
diff --git a/src/main/java/org/traccar/protocol/UproProtocolDecoder.java b/src/main/java/org/traccar/protocol/UproProtocolDecoder.java
index ed714e46438..8d2e5de0ad4 100644
--- a/src/main/java/org/traccar/protocol/UproProtocolDecoder.java
+++ b/src/main/java/org/traccar/protocol/UproProtocolDecoder.java
@@ -310,6 +310,10 @@ protected Object decode(
position.set("serial", data.toString(StandardCharsets.US_ASCII).substring(3));
}
break;
+ case 'd':
+ position.set(Position.PREFIX_ADC + 1,
+ Integer.parseInt(data.toString(StandardCharsets.US_ASCII)) / 100.0);
+ break;
default:
break;
}
diff --git a/src/main/java/org/traccar/schedule/ScheduleManager.java b/src/main/java/org/traccar/schedule/ScheduleManager.java
index 38e8f281c41..742428fd8fa 100644
--- a/src/main/java/org/traccar/schedule/ScheduleManager.java
+++ b/src/main/java/org/traccar/schedule/ScheduleManager.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 - 2023 Anton Tananaev (anton@traccar.org)
+ * Copyright 2020 - 2024 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.
@@ -20,9 +20,9 @@
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
-import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
+import java.util.stream.Stream;
@Singleton
public class ScheduleManager implements LifecycleObject {
@@ -38,13 +38,15 @@ public ScheduleManager(Injector injector) {
@Override
public void start() {
executor = Executors.newSingleThreadScheduledExecutor();
- var tasks = List.of(
+ Stream.of(
+ TaskHealthCheck.class,
+ TaskClearStatus.class,
+ TaskExpirations.class,
TaskDeleteTemporary.class,
TaskReports.class,
TaskDeviceInactivityCheck.class,
- TaskWebSocketKeepalive.class,
- TaskHealthCheck.class);
- tasks.forEach(task -> injector.getInstance(task).schedule(executor));
+ TaskWebSocketKeepalive.class)
+ .forEachOrdered(task -> injector.getInstance(task).schedule(executor));
}
@Override
diff --git a/src/main/java/org/traccar/schedule/TaskClearStatus.java b/src/main/java/org/traccar/schedule/TaskClearStatus.java
new file mode 100644
index 00000000000..78fecc0ea24
--- /dev/null
+++ b/src/main/java/org/traccar/schedule/TaskClearStatus.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2024 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.schedule;
+
+import jakarta.inject.Inject;
+import org.traccar.broadcast.BroadcastService;
+import org.traccar.helper.model.DeviceUtil;
+import org.traccar.storage.Storage;
+import org.traccar.storage.StorageException;
+
+import java.util.concurrent.ScheduledExecutorService;
+
+public class TaskClearStatus implements ScheduleTask {
+
+ @Inject
+ public TaskClearStatus(BroadcastService broadcastService, Storage storage) throws StorageException {
+ if (broadcastService.singleInstance()) {
+ DeviceUtil.resetStatus(storage);
+ }
+ }
+
+ @Override
+ public void schedule(ScheduledExecutorService executor) {
+ }
+
+ @Override
+ public void run() {
+ }
+
+}
diff --git a/src/main/java/org/traccar/schedule/TaskExpirations.java b/src/main/java/org/traccar/schedule/TaskExpirations.java
new file mode 100644
index 00000000000..94f855c5fb2
--- /dev/null
+++ b/src/main/java/org/traccar/schedule/TaskExpirations.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2024 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.schedule;
+
+import jakarta.inject.Inject;
+import jakarta.mail.MessagingException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.config.Config;
+import org.traccar.config.Keys;
+import org.traccar.mail.MailManager;
+import org.traccar.model.Device;
+import org.traccar.model.Disableable;
+import org.traccar.model.Server;
+import org.traccar.model.User;
+import org.traccar.notification.TextTemplateFormatter;
+import org.traccar.storage.Storage;
+import org.traccar.storage.StorageException;
+import org.traccar.storage.query.Columns;
+import org.traccar.storage.query.Condition;
+import org.traccar.storage.query.Request;
+
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+public class TaskExpirations implements ScheduleTask {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(TaskExpirations.class);
+
+ private static final long CHECK_PERIOD_HOURS = 1;
+
+ private final Config config;
+ private final Storage storage;
+ private final TextTemplateFormatter textTemplateFormatter;
+ private final MailManager mailManager;
+
+ @Inject
+ public TaskExpirations(
+ Config config, Storage storage, TextTemplateFormatter textTemplateFormatter, MailManager mailManager) {
+ this.config = config;
+ this.storage = storage;
+ this.textTemplateFormatter = textTemplateFormatter;
+ this.mailManager = mailManager;
+ }
+
+ @Override
+ public void schedule(ScheduledExecutorService executor) {
+ executor.scheduleAtFixedRate(this, CHECK_PERIOD_HOURS, CHECK_PERIOD_HOURS, TimeUnit.HOURS);
+ }
+
+ private boolean checkTimeTrigger(Disableable disableable, long currentTime, long offsetTime) {
+ if (disableable.getExpirationTime() != null) {
+ long previousTime = currentTime - TimeUnit.HOURS.toMillis(CHECK_PERIOD_HOURS);
+ long expirationTime = disableable.getExpirationTime().getTime() + offsetTime;
+ return previousTime < expirationTime && currentTime >= expirationTime;
+ }
+ return false;
+ }
+
+ private void sendUserExpiration(
+ Server server, User user, String template) throws MessagingException {
+ var velocityContext = textTemplateFormatter.prepareContext(server, user);
+ velocityContext.put("expiration", user.getExpirationTime());
+ var fullMessage = textTemplateFormatter.formatMessage(velocityContext, template, "full");
+ mailManager.sendMessage(user, true, fullMessage.getSubject(), fullMessage.getBody());
+ }
+
+ private void sendDeviceExpiration(
+ Server server, Device device, String template) throws MessagingException, StorageException {
+ var users = storage.getObjects(User.class, new Request(
+ new Columns.All(), new Condition.Permission(User.class, Device.class, device.getId())));
+ for (User user : users) {
+ var velocityContext = textTemplateFormatter.prepareContext(server, user);
+ velocityContext.put("expiration", device.getExpirationTime());
+ velocityContext.put("device", device);
+ var fullMessage = textTemplateFormatter.formatMessage(velocityContext, template, "full");
+ mailManager.sendMessage(user, true, fullMessage.getSubject(), fullMessage.getBody());
+ }
+ }
+
+ @Override
+ public void run() {
+ try {
+
+ long currentTime = System.currentTimeMillis();
+ Server server = storage.getObject(Server.class, new Request(new Columns.All()));
+
+ if (config.getBoolean(Keys.NOTIFICATION_EXPIRATION_USER)) {
+ long reminder = config.getLong(Keys.NOTIFICATION_EXPIRATION_USER_REMINDER);
+ var users = storage.getObjects(User.class, new Request(new Columns.All()));
+ for (User user : users) {
+ if (checkTimeTrigger(user, currentTime, 0)) {
+ sendUserExpiration(server, user, "userExpiration");
+ } else if (reminder > 0 && checkTimeTrigger(user, currentTime, -reminder)) {
+ sendUserExpiration(server, user, "userExpirationReminder");
+ }
+ }
+ }
+
+ if (config.getBoolean(Keys.NOTIFICATION_EXPIRATION_DEVICE)) {
+ long reminder = config.getLong(Keys.NOTIFICATION_EXPIRATION_USER_REMINDER);
+ var devices = storage.getObjects(Device.class, new Request(new Columns.All()));
+ for (Device device : devices) {
+ if (checkTimeTrigger(device, currentTime, 0)) {
+ sendDeviceExpiration(server, device, "deviceExpiration");
+ } else if (reminder > 0 && checkTimeTrigger(device, currentTime, -reminder)) {
+ sendDeviceExpiration(server, device, "deviceExpirationReminder");
+ }
+ }
+ }
+
+ } catch (StorageException | MessagingException e) {
+ LOGGER.warn("Failed to check expirations", e);
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/schedule/TaskHealthCheck.java b/src/main/java/org/traccar/schedule/TaskHealthCheck.java
index abdc5af48ee..a60935f1804 100644
--- a/src/main/java/org/traccar/schedule/TaskHealthCheck.java
+++ b/src/main/java/org/traccar/schedule/TaskHealthCheck.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 - 2022 Anton Tananaev (anton@traccar.org)
+ * Copyright 2020 - 2024 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.
@@ -34,6 +34,8 @@ public class TaskHealthCheck implements ScheduleTask {
private final Config config;
private final Client client;
+ private final long gracePeriod = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1);
+
private SystemD systemD;
private boolean enabled;
@@ -77,14 +79,22 @@ public void schedule(ScheduledExecutorService executor) {
@Override
public void run() {
LOGGER.debug("Health check running");
- int status = client.target(getUrl()).request().get().getStatus();
- if (status == 200) {
- int result = systemD.sd_notify(0, "WATCHDOG=1");
- if (result < 0) {
- LOGGER.warn("Health check notify error {}", result);
+ if (System.currentTimeMillis() > gracePeriod) {
+ int status = client.target(getUrl()).request().get().getStatus();
+ if (status == 200) {
+ notifyWatchdog();
+ } else {
+ LOGGER.warn("Health check failed with status {}", status);
}
} else {
- LOGGER.warn("Health check failed with status {}", status);
+ notifyWatchdog();
+ }
+ }
+
+ private void notifyWatchdog() {
+ int result = systemD.sd_notify(0, "WATCHDOG=1");
+ if (result < 0) {
+ LOGGER.warn("Health check notify error {}", result);
}
}
diff --git a/src/main/java/org/traccar/session/ConnectionManager.java b/src/main/java/org/traccar/session/ConnectionManager.java
index a598260aab2..42dcf5ce97d 100644
--- a/src/main/java/org/traccar/session/ConnectionManager.java
+++ b/src/main/java/org/traccar/session/ConnectionManager.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2015 - 2023 Anton Tananaev (anton@traccar.org)
+ * Copyright 2015 - 2024 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.
@@ -63,6 +63,7 @@ public class ConnectionManager implements BroadcastInterface {
private static final Logger LOGGER = LoggerFactory.getLogger(ConnectionManager.class);
private final long deviceTimeout;
+ private final boolean showUnknownDevices;
private final Map sessionsByDeviceId = new ConcurrentHashMap<>();
private final Map> sessionsByEndpoint = new ConcurrentHashMap<>();
@@ -95,6 +96,7 @@ public ConnectionManager(
this.broadcastService = broadcastService;
this.deviceLookupService = deviceLookupService;
deviceTimeout = config.getLong(Keys.STATUS_TIMEOUT);
+ showUnknownDevices = config.getBoolean(Keys.WEB_SHOW_UNKNOWN_DEVICES);
broadcastService.registerListener(this);
}
@@ -145,7 +147,7 @@ public DeviceSession getDeviceSession(
}
DeviceSession deviceSession = new DeviceSession(
- device.getId(), device.getUniqueId(), protocol, channel, remoteAddress);
+ device.getId(), device.getUniqueId(), device.getModel(), protocol, channel, remoteAddress);
endpointSessions.put(device.getUniqueId(), deviceSession);
sessionsByEndpoint.put(remoteAddress, endpointSessions);
sessionsByDeviceId.put(device.getId(), deviceSession);
@@ -186,17 +188,19 @@ private Device addUnknownDevice(String uniqueId) {
public void deviceDisconnected(Channel channel, boolean supportsOffline) {
SocketAddress remoteAddress = channel.remoteAddress();
- Map endpointSessions = sessionsByEndpoint.remove(remoteAddress);
- if (endpointSessions != null) {
- for (DeviceSession deviceSession : endpointSessions.values()) {
- if (supportsOffline) {
- updateDevice(deviceSession.getDeviceId(), Device.STATUS_OFFLINE, null);
+ if (remoteAddress != null) {
+ Map endpointSessions = sessionsByEndpoint.remove(remoteAddress);
+ if (endpointSessions != null) {
+ for (DeviceSession deviceSession : endpointSessions.values()) {
+ if (supportsOffline) {
+ updateDevice(deviceSession.getDeviceId(), Device.STATUS_OFFLINE, null);
+ }
+ sessionsByDeviceId.remove(deviceSession.getDeviceId());
+ cacheManager.removeDevice(deviceSession.getDeviceId());
}
- sessionsByDeviceId.remove(deviceSession.getDeviceId());
- cacheManager.removeDevice(deviceSession.getDeviceId());
}
+ unknownByEndpoint.remove(remoteAddress);
}
- unknownByEndpoint.remove(remoteAddress);
}
public void deviceUnknown(long deviceId) {
@@ -344,7 +348,7 @@ public synchronized void updateLog(LogRecord record) {
var sessions = sessionsByEndpoint.getOrDefault(record.getAddress(), Map.of());
if (sessions.isEmpty()) {
String unknownUniqueId = unknownByEndpoint.get(record.getAddress());
- if (unknownUniqueId != null) {
+ if (unknownUniqueId != null && showUnknownDevices) {
record.setUniqueId(unknownUniqueId);
listeners.values().stream()
.flatMap(Set::stream)
diff --git a/src/main/java/org/traccar/session/DeviceSession.java b/src/main/java/org/traccar/session/DeviceSession.java
index 009f90f5ae3..f124ca7f9f0 100644
--- a/src/main/java/org/traccar/session/DeviceSession.java
+++ b/src/main/java/org/traccar/session/DeviceSession.java
@@ -29,14 +29,17 @@ public class DeviceSession {
private final long deviceId;
private final String uniqueId;
+ private final String model;
private final Protocol protocol;
private final Channel channel;
private final SocketAddress remoteAddress;
public DeviceSession(
- long deviceId, String uniqueId, Protocol protocol, Channel channel, SocketAddress remoteAddress) {
+ long deviceId, String uniqueId, String model,
+ Protocol protocol, Channel channel, SocketAddress remoteAddress) {
this.deviceId = deviceId;
this.uniqueId = uniqueId;
+ this.model = model;
this.protocol = protocol;
this.channel = channel;
this.remoteAddress = remoteAddress;
@@ -50,6 +53,10 @@ public String getUniqueId() {
return uniqueId;
}
+ public String getModel() {
+ return model;
+ }
+
public Channel getChannel() {
return channel;
}
diff --git a/src/main/java/org/traccar/session/cache/CacheManager.java b/src/main/java/org/traccar/session/cache/CacheManager.java
index bb9b4c99537..064e5672f99 100644
--- a/src/main/java/org/traccar/session/cache/CacheManager.java
+++ b/src/main/java/org/traccar/session/cache/CacheManager.java
@@ -49,7 +49,6 @@
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.Collectors;
-import java.util.stream.Stream;
@Singleton
public class CacheManager implements BroadcastInterface {
@@ -136,14 +135,15 @@ public Set getNotificationUsers(long notificationId, long deviceId) {
}
}
- public Stream getDeviceNotifications(long deviceId) {
+ public Set getDeviceNotifications(long deviceId) {
try {
lock.readLock().lock();
var direct = graph.getObjects(Device.class, deviceId, Notification.class, Set.of(Group.class), true)
.map(BaseModel::getId)
.collect(Collectors.toUnmodifiableSet());
return graph.getObjects(Device.class, deviceId, Notification.class, Set.of(Group.class, User.class), true)
- .filter(notification -> notification.getAlways() || direct.contains(notification.getId()));
+ .filter(notification -> notification.getAlways() || direct.contains(notification.getId()))
+ .collect(Collectors.toUnmodifiableSet());
} finally {
lock.readLock().unlock();
}
diff --git a/src/test/java/org/traccar/BaseTest.java b/src/test/java/org/traccar/BaseTest.java
index 2ace781f37c..ce19d8ace37 100644
--- a/src/test/java/org/traccar/BaseTest.java
+++ b/src/test/java/org/traccar/BaseTest.java
@@ -33,7 +33,8 @@ protected T inject(T decoder) throws Exception {
var connectionManager = mock(ConnectionManager.class);
var uniqueIdsProvided = new HashSet();
when(connectionManager.getDeviceSession(any(), any(), any(), any(String[].class))).thenAnswer(invocation -> {
- var mock = new DeviceSession(1L, "", mock(Protocol.class), mock(Channel.class), mock(SocketAddress.class));
+ var mock = new DeviceSession(
+ 1L, "", null, mock(Protocol.class), mock(Channel.class), mock(SocketAddress.class));
if (uniqueIdsProvided.isEmpty()) {
if (invocation.getArguments().length > 3) {
uniqueIdsProvided.add(true);
diff --git a/src/test/java/org/traccar/protocol/FleetGuideProtocolDecoderTest.java b/src/test/java/org/traccar/protocol/FleetGuideProtocolDecoderTest.java
new file mode 100644
index 00000000000..3cd74766d31
--- /dev/null
+++ b/src/test/java/org/traccar/protocol/FleetGuideProtocolDecoderTest.java
@@ -0,0 +1,33 @@
+package org.traccar.protocol;
+
+import org.junit.jupiter.api.Test;
+import org.traccar.ProtocolTest;
+
+public class FleetGuideProtocolDecoderTest extends ProtocolTest {
+
+ @Test
+ public void testDecode() throws Exception {
+
+ var decoder = inject(new FleetGuideProtocolDecoder(null));
+
+ verifyNull(decoder, binary(
+ "5300188f8a020001f36a"));
+
+ verifyPositions(decoder, false, binary(
+ "5322188f8a0200140700f355831a83ec06030c008065052c06ffffffff033006808001180003200100005ec0"));
+
+ verifyPositions(decoder, binary(
+ "539e598f8a020003020700e378351ac39f040c04ffa92e806a28a13b1f00b638030c000067052c06ffffffff033006808001180003200100000700e478351ac40204303dab2e80cb27a13b1c00ac40021b30e778351ac502043082a72e8054020530bf30021b30e978351ac69f0d0c0433a72e80c123a13b2000df3807002579351ac79f06020a170000df28021b476179351ac8020f30021c81279d79351ac9020f30021c8207d979351aca020f30021c8157157a351acb022b8140517a351acc022b608d7a351acd022b60c97a351ace022b30057b351acf022b30417b351ad0022b307d7b351ad1020f3048021b30b97b351ad2020f3070030c004066021630f57b351ad30213841080021730317c351ad4021330c00217306d7c351ad5020f3050021b30a97c351ad602138170021830e57c351ad7020f3058021b30217d351ad8021385500218305d7d351ad9022b8110997d351ada022b8170d57d351adb022b30117e351adc020f3068030ce184680216304d7e351add020f3060030ce34469021630897e351ade021260e4021830c57e351adf021230e5021830017f351ae002293022f2"));
+
+ verifyPositions(decoder, binary(
+ "538958235a02008408070027398f1a7da3040c0485c7437f9db8a635ca016189030c76a567052c06bd05ffff033006138009340f69fb0080008000800118010320018005070029398f1a7e08043b39f0437f14b9a635cb010288030c7108233b2b398f1a7f08043bec18447f7fbca6357c010b8008263b2d398f1a8008043be53d447f61c0a63562011480030c7708213b6f3b"));
+
+ verifyPositions(decoder, binary(
+ "53361886b60e00540700cf3b8f1a838d060e041dc683a1b672a04b0000ba885d00030c0ea567052c06ffffffff03300680800118000720070000000000006bc3"));
+
+ verifyPositions(decoder, binary(
+ "53dd581fc10e0094080700373d8f1a5e2f090e04cbb6c2a3d3078f4b5d023090b500030cac6567052c06ffffffff0330068080054810fdfdfdff0548121bf0000005481321d4830005481454cf08040e15eee20100054816770308040e171c0208040718281f0804071984000804071a583a000001180107200781050000000007003b3d8f1a5f2f040e04dcfbc2a3b15a8f4b67023890b4081c7c1f08067c2e08067c61081b7c842008057c8308067c443c08107c3e3d8f1a6008047cf125c3a31ca48f4b7c0242081e7c2208067c3908067c6b08067cef08147cac21080c7ce03e080e7cbb62"));
+
+ }
+
+}
diff --git a/src/test/java/org/traccar/protocol/Gl200TextProtocolDecoderTest.java b/src/test/java/org/traccar/protocol/Gl200TextProtocolDecoderTest.java
index 199012ca02a..6327f65792f 100644
--- a/src/test/java/org/traccar/protocol/Gl200TextProtocolDecoderTest.java
+++ b/src/test/java/org/traccar/protocol/Gl200TextProtocolDecoderTest.java
@@ -11,18 +11,17 @@ public void testDecode() throws Exception {
var decoder = inject(new Gl200TextProtocolDecoder(null));
- verifyAttribute(decoder, buffer(
- "+RESP:GTFRI,710303,868487004352084,GL530MG,0,0,1,1,16.6,0,9.4,121.307910,31.127837,20230815050629,0460,0000,1815,B93B,26,0,8964,90,1,0,26.6,20230815130830,0174$"),
- Position.PREFIX_TEMP + 1, 26.6);
+ verifyPositions(decoder, buffer(
+ "+RESP:GTFRI,8020040305,866314060272661,,,50,1,1,0.0,0,2957.9,-78.691727,-0.951205,20231227162916,,,,,00,0.0,,,,,100,210100,,,,20231227162916,0117$"));
- verifyPosition(decoder, buffer(
+ verifyPositions(decoder, buffer(
"+BUFF:GTFRI,8020040200,866314060249032,,12194,10,1,3,0.0,0,20.1,-71.596533,-33.524718,20230926200338,0730,0001,772A,052B253E,02,0,0.0,,,,,0,420000,,,,20230926200340,1549$"));
verifyAttribute(decoder, buffer(
"+RESP:GTFRI,423037,866884047716519,GT501,0,1,1,5,12,0.1,0,46.8,-95.559173,30.109955,20231110185836,6,0e36c9916485,-50,,,,e831cd5eb79d,-73,,,,ccf4110c4bd5,-79,,,,acdb48973168,-79,,,,80ab4dc323c4,-82,,,,ec8eb5cfa1c6,-89,,,,310,10,711D,81ECF0F,00,,93,20231110185839,0005$"),
Position.KEY_BATTERY_LEVEL, 93);
- verifyPosition(decoder, buffer(
+ verifyPositions(decoder, buffer(
"+RESP:GTFRI,8020040200,866314060109269,,,10,1,1,0.0,0,9.0,-71.596601,-33.524595,20230722145338,0730,0001,772A,052B253E,00,0.0,,,,,100,210100,,,,20230722145341,0F4C$"));
verifyAttributes(decoder, buffer(
@@ -479,6 +478,11 @@ public void testDecode() throws Exception {
verifyAttributes(decoder, buffer(
"+ACK:GTGEO,1A0102,135790246811220,,0,0008,20100310172830,11F0"));
+ decoder.setModelOverride("GV355CEU");
+
+ verifyAttributes(decoder, buffer(
+ "+RESP:GTCAN,8020050605,867488060270575,,00,1,FFFFFFFF,8LBETF3W4N0001613,,,22.54,0,,,,,,,7.84,4.61,3.24,3.33,,8080,,,00,0.00,0.00,1,14,14,2371,0,001FFFFF,,,,,,,,,7158,9998,0,7.84,0.00,0.00,558,,,,,,,C0,,,,,0,0.0,346,2848.5,-78.592371,-0.968132,20240202083437,0740,0002,526C,00AE7907,00,20240202083440,3F6D$"));
+
}
}
diff --git a/src/test/java/org/traccar/protocol/MeitrackProtocolDecoderTest.java b/src/test/java/org/traccar/protocol/MeitrackProtocolDecoderTest.java
index 24dc9d18efa..74a180b1171 100644
--- a/src/test/java/org/traccar/protocol/MeitrackProtocolDecoderTest.java
+++ b/src/test/java/org/traccar/protocol/MeitrackProtocolDecoderTest.java
@@ -11,6 +11,10 @@ public void testDecode() throws Exception {
var decoder = inject(new MeitrackProtocolDecoder(null));
+ verifyAttribute(decoder, binary(
+ "24246D3230312C3836393430393036323730323834332C4343452C000000000100A7002A000D05010626070914001502930194009500960097629D209E63A1640E0824000956000A07000B2A001606001704001901001A0E0B91280092280099EF049C52009F1B004023000C0215B9F2FF035855F506041E0F142D0C01708C010D748AC2001C012000009A000000009BD0623E02A0889FF201A2D61A0000A542020000FEF4A3D50900030E0CFE010A007F466FFC0000000049090401000000000000004B0501010232472A44360D0A"),
+ Position.KEY_HOURS, 644515 * 60000L);
+
verifyAttribute(decoder, binary(
"2424593136312c3836323039303035303031363139332c4343452c0000000001007f0017000705010607071714001500fe69601b00070800000971000a13000b19001605001acc0440230006029779570103eb5bcc06041ff0e8290c430100000d780400001c01000000030e0ccc010000922781abb90ca4fffe731e0109746e6873656e736f72ac233f6e219064051b753b00000000000000004b060101034c54452a42380d0a"),
"tagName", "tnhsensor");
diff --git a/src/test/java/org/traccar/protocol/Minifinder2ProtocolDecoderTest.java b/src/test/java/org/traccar/protocol/Minifinder2ProtocolDecoderTest.java
index 7c6a1de084a..126334767f3 100644
--- a/src/test/java/org/traccar/protocol/Minifinder2ProtocolDecoderTest.java
+++ b/src/test/java/org/traccar/protocol/Minifinder2ProtocolDecoderTest.java
@@ -14,6 +14,9 @@ public void testDecode() throws Exception {
verifyPositions(decoder, false, binary(
"ab105a0512e19404011001383632333131303632373037333735093743c3ec640000000009374dc3ec6400000000093750c3ec6400000080092455c3ec640203935e0f22a318d6c7baacd6a2546751467bd009246ac3ec640203b35e0f22a318d6c7baacd6a2546751467bd009246cc3ec640203b35e0f22a318d6c7baacd6a2546751467bd009247ec3ec640203b35e0f22a318d6c7baacd6a2546751467bd0092492c3ec640203b35e0f22a318d6c7baacd6a2546751467bd00924a6c3ec640203b35e0f22a318d6c7baacd6a2546751467bd00924bac3ec640203b35e0f22a318d6c7baacd6a2546751467bd00924d2c3ec640203b35e0f22a7083a2f201a83a3f8084f84ae560924e7c3ec640203b35e0f22a7083a2f201a83a3f8084f84ae560924fbc3ec640203b35e0f22a7083a2f201a83a3f8084f84ae5609240fc4ec640203b35e0f22a7083a2f201a83a3f8084f84ae56092423c4ec640203b35d0f22a7083a2f201a83a3f8084f84ae56092437c4ec640203cb5d0f22a7083a2f201a83a3f8084f84ae5609244fc4ec640003cb5d092464c4ec640003cb5d092478c4ec640003cb5d09248cc4ec640003cb5d0924a0c4ec640003cb5d0924b4c4ec640003cb5d0924ccc4ec640003cb5d0924e5c4ec640003cb5d0924fec4ec6400037b5d092413c5ec6400037b5d092427c5ec6400017b5d0924b785ed640003cb530924d085ed640003ab530924e985ed640003ab530924fe85ed640003ab5309241286ed640003ab5309242686ed640003ab5309243a86ed640003ab5309244e86ed640003ab5309246786ed640003ab5309248086ed640003ab5309249986ed6400037b530924b286ed6400037b530924c686ed6400037b530924da86ed6400037b530924ee86ed6400037b5309240287ed6400037b5309241687ed6400037b5309242f87ed6400037b5309244787ed640003835309246187ed640003835309247a87ed640003835309249287ed64000383530924ab87ed64000383530924c487ed64000383530924d987ed64000383530924ed87ed640003835309240188ed640003835309241588ed640003835309242988ed640003d35309243a88ed640003d3530d02000000803788ed640000000009374188ed640400000009244188ed640003d35309244288ed640003d35309374b88ed640500000009244b88ed640003d35309375588ed640500000009245588ed640003d35309245788ed640003d35309375f88ed640700000009245f88ed640003d35309376988ed640800000009246988ed640003d35309246b88ed640203d3530f22a502184a2cfba0a42c768af4ab5009247188ed640203d3530f22a502184a2cfba0a42c768af4ab5009377388ed640a00000009247688ed640203d3530f22a502184a2cfba0a42c768af4ab5009247b88ed640203d3530f22a502184a2cfba0a42c768af4ab5009377d88ed640300000009247e88ed640203d3530f22a502184a2cfba0a42c768af4ab5009248088ed640203d3530f22a502184a2cfba0a42c768af4ab5009248588ed640203d3530f22a502184a2cfba0a42c768af4ab5009378788ed640000000009248a88ed640203d3530f22a502184a2cfba0a42c768af4ab5009248f88ed640203d3530f22a502184a2cfba0a42c768af4ab5009379188ed640000000009379288ed640000008009249288ed640203d3530f22a502184a2cfba0a42c768af4ab5009249488ed640203d3530f22a502184a2cfba0a42c768af4ab5009249988ed640203d3530f22a502184a2cfba0a42c768af4ab5009249e88ed640203d3530f22a502184a2cfba0a42c768af4ab500924a388ed640203d3530f22a502184a2cfba0a42c768af4ab500924a688ed640203d3530f22a502184a2cfba0a42c768af4ab50"));
+ verifyAttributes(decoder, binary(
+ "ab00cc029c9b0000020501040518200502cf290001100338363233313130363534393538393515043839343632303338303735303031383830343034070539eed3f9cec705064f93a7650507010b00002908cf2900010020050000d0000068915b00ae0000004f637420313920323032330031303a35393a3332261b53494d37353030457c4231315630325f3231303330337c50312e30325f3230323231313034050900000000050a00000003070b000000001e01070b010000001e01070b020000001e01070b030000001e01060c0000000000050d6e600580020e04050f0708008002100002110f02126406134d46303758041404a20e08160000000000000008160100000000000008160200000000000008160300000000000008160400000000000008160500000000000008160600000000000008160700000000000008160800000000000008160900000000000020177777772e676f6f676c652e636f6d2f6d6170733f713d252e37662c252e3766211868747470733a2f2f6c6f632e6d696e6966696e6465722e636f6d2f25732f25730519000001fe101a4d4630372e343631302e3233303300021c640a1d802acee6212966cf0803207b3e03217b040230000230010230020230030230040230050230060230070230080230090231000532580214010533820000000e406d326d2e74656c65322e636f6d014101421c4380431468756e7465726465762e6d696e6966696e6465722e636f6d0d440e01008005000000100e0000054505002c01014705500f1400f00d511002e803b84d7f0d0246f6430d511100f40100000000000000000d511200f40100000000000000000d511300f401000000000000000005527800030005532c0100000354500005551e002d400256050b57201c00002c010000ed61025d01055c890a0000057000000000037100001472000000000000000000000000000000000000000568f0000100057500000000074de36000000000"));
+
verifyAttribute(decoder, binary(
"ab101c00d6f61e000110013836333932313033393939363038300937efd201640c000000"),
"barkCount", 12L);
diff --git a/src/test/java/org/traccar/protocol/Minifinder2ProtocolEncoderTest.java b/src/test/java/org/traccar/protocol/Minifinder2ProtocolEncoderTest.java
index ef6ff6dd9f5..32c8a9ce68d 100644
--- a/src/test/java/org/traccar/protocol/Minifinder2ProtocolEncoderTest.java
+++ b/src/test/java/org/traccar/protocol/Minifinder2ProtocolEncoderTest.java
@@ -14,8 +14,7 @@ public void testEncodeNano() throws Exception {
var encoder = inject(new Minifinder2ProtocolEncoder(null));
- var device = encoder.getCacheManager().getObject(Device.class, 1);
- when(device.getModel()).thenReturn("Nano");
+ encoder.setModelOverride("Nano");
Command command = new Command();
command.setDeviceId(1);
diff --git a/src/test/java/org/traccar/protocol/RstProtocolDecoderTest.java b/src/test/java/org/traccar/protocol/RstProtocolDecoderTest.java
index fc932fe9e2b..a750d031150 100644
--- a/src/test/java/org/traccar/protocol/RstProtocolDecoderTest.java
+++ b/src/test/java/org/traccar/protocol/RstProtocolDecoderTest.java
@@ -11,6 +11,10 @@ public void testDecode() throws Exception {
var decoder = inject(new RstProtocolDecoder(null));
+ verifyAttribute(decoder, text(
+ "RST;A;RST-MINI-4Gv3;V9.10;009521405;1;134;FIM;"),
+ Position.KEY_RESULT, "134");
+
verifyAttribute(decoder, text(
"RST;A;RST-MINIv5;V9.08;009767055;248;55;14-12-2023 19:34:20;14-12-2023 19:34:21;-12.923640;-38.388313;0;14;17;1;4;15;00;B0;00;1A;02;12.18;4.02;65;21;FE;0000;01;C0;001606017031;0002;FIM;"),
Position.KEY_DRIVER_UNIQUE_ID, "001606017031");
diff --git a/src/test/java/org/traccar/protocol/SigfoxProtocolDecoderTest.java b/src/test/java/org/traccar/protocol/SigfoxProtocolDecoderTest.java
index cc6c1701494..c7d0671c00f 100644
--- a/src/test/java/org/traccar/protocol/SigfoxProtocolDecoderTest.java
+++ b/src/test/java/org/traccar/protocol/SigfoxProtocolDecoderTest.java
@@ -3,8 +3,11 @@
import io.netty.handler.codec.http.HttpMethod;
import org.junit.jupiter.api.Test;
import org.traccar.ProtocolTest;
+import org.traccar.model.Device;
import org.traccar.model.Position;
+import static org.mockito.Mockito.when;
+
public class SigfoxProtocolDecoderTest extends ProtocolTest {
@Test
@@ -43,6 +46,11 @@ public void testDecode() throws Exception {
verifyPosition(decoder, request(HttpMethod.POST, "/",
buffer("%7B++%22device%22%3A%222BF839%22%2C++%22time%22%3A1510605882%2C++%22duplicate%22%3Afalse%2C++%22snr%22%3A45.61%2C++%22station%22%3A%2235A9%22%2C++%22data%22%3A%2200bd6475e907398e562d01b9%22%2C++%22avgSnr%22%3A45.16%2C++%22lat%22%3A-38.0%2C++%22lng%22%3A145.0%2C++%22rssi%22%3A-98.00%2C++%22seqNumber%22%3A228+%7D=")));
+ decoder.setModelOverride("Amber");
+
+ verifyPosition(decoder, request(HttpMethod.POST, "/",
+ buffer("{ \"deviceId\":\"284019F\", \"timestamp\":\"1707375610\", \"seqNo\":\"42\", \"data\":\"0100b019ffe8645d0019e513\", \"linkQuality\":\"Excellent\", \"operator\":\"SIGFOX_South_Africa_Sqwidnet\", \"country\":\"710\" }")));
+
}
}
diff --git a/src/test/java/org/traccar/protocol/StartekFrameDecoderTest.java b/src/test/java/org/traccar/protocol/StartekFrameDecoderTest.java
new file mode 100644
index 00000000000..789126471d7
--- /dev/null
+++ b/src/test/java/org/traccar/protocol/StartekFrameDecoderTest.java
@@ -0,0 +1,23 @@
+package org.traccar.protocol;
+
+import org.junit.jupiter.api.Test;
+import org.traccar.ProtocolTest;
+
+public class StartekFrameDecoderTest extends ProtocolTest {
+
+ @Test
+ public void testDecode() throws Exception {
+
+ var decoder = inject(new StartekFrameDecoder());
+
+ verifyFrame(
+ binary("26265c3134332c3836353439313036393537333134302c3030302c302c2c3234303130343136303031362c412c2d362e3239303633382c3130362e3830393537382c31352c302e382c302c3238322c35372c3338382c3531307c31307c303444457c30303030373442452c33312c30303030303033432c30302c30302c303444347c303141327c303030307c303030302c312c2c2c31350d0a"),
+ decoder.decode(null, null, binary("26265c3134332c3836353439313036393537333134302c3030302c302c2c3234303130343136303031362c412c2d362e3239303633382c3130362e3830393537382c31352c302e382c302c3238322c35372c3338382c3531307c31307c303444457c30303030373442452c33312c30303030303033432c30302c30302c303444347c303141327c303030307c303030302c312c2c2c31350d0a")));
+
+ verifyFrame(
+ binary("26265c3534362c3836353439313036313134353937302c3731302c54312c302e302c302e302c302e302c302e302c302c302c302c302c302e302c302c302e302c302c46302c302e302c302e302c302e302c302e302c302e302c302c302e302c302c302c302c302c302c312c302c302e302c30302c302e302c302e302c302e302c300d0a54322c302e3030302c302e302c393232333337323033363835343737353830382e382c393232333337323033363835343737353830382e382c343239343936373239352c343239343936373239352c302c3432393439363732392c302c302c302c302c302c302c302c302c302c302c302e302c32313437343833362e302c393232333337323033363835343737353830382e382c302c30302c302c302c302e30300d0a54352c302c302c302c302c302c302c302c302c302c302c302c302c302e302c302e302c2a2c2a2c300d0a54362c30302c30332c30302c31462c31462c31462c30452c30332c30302c30302c30302c30302c31462c31460d0a54372c302c302c302c3432393439363732392c32313437343833362e3030302c3432393439363732392c302e3030302c302c302e3030302c302c302e3030302c302c302e3030302c302c302e3030302c302c302e3030302c302c302e3030302c302c302e3030302c302c302e3030302c302c302c302c302e3030300d0a54782c2a2c2a2c2a2c2a2c2a2c302e302c302c302c302c302c2d3132352c302c302c302c302c302c302c300d0a46330d0a"),
+ decoder.decode(null, null, binary("26265c3534362c3836353439313036313134353937302c3731302c54312c302e302c302e302c302e302c302e302c302c302c302c302c302e302c302c302e302c302c46302c302e302c302e302c302e302c302e302c302e302c302c302e302c302c302c302c302c302c312c302c302e302c30302c302e302c302e302c302e302c300d0a54322c302e3030302c302e302c393232333337323033363835343737353830382e382c393232333337323033363835343737353830382e382c343239343936373239352c343239343936373239352c302c3432393439363732392c302c302c302c302c302c302c302c302c302c302c302e302c32313437343833362e302c393232333337323033363835343737353830382e382c302c30302c302c302c302e30300d0a54352c302c302c302c302c302c302c302c302c302c302c302c302c302e302c302e302c2a2c2a2c300d0a54362c30302c30332c30302c31462c31462c31462c30452c30332c30302c30302c30302c30302c31462c31460d0a54372c302c302c302c3432393439363732392c32313437343833362e3030302c3432393439363732392c302e3030302c302c302e3030302c302c302e3030302c302c302e3030302c302c302e3030302c302c302e3030302c302c302e3030302c302c302e3030302c302c302e3030302c302c302c302c302e3030300d0a54782c2a2c2a2c2a2c2a2c2a2c302e302c302c302c302c302c2d3132352c302c302c302c302c302c302c300d0a46330d0a")));
+
+ }
+
+}
diff --git a/src/test/java/org/traccar/protocol/StartekProtocolDecoderTest.java b/src/test/java/org/traccar/protocol/StartekProtocolDecoderTest.java
index 94b3fd2568a..0d951625620 100644
--- a/src/test/java/org/traccar/protocol/StartekProtocolDecoderTest.java
+++ b/src/test/java/org/traccar/protocol/StartekProtocolDecoderTest.java
@@ -11,49 +11,58 @@ public void testDecode() throws Exception {
var decoder = inject(new StartekProtocolDecoder(null));
+ verifyAttributes(decoder, text(
+ "&&\\546,865491061145970,710,T1,0.0,0.0,0.0,0.0,0,0,0,0,0.0,0,0.0,0,F0,0.0,0.0,0.0,0.0,0.0,0,0.0,0,0,0,0,0,1,0,0.0,00,0.0,0.0,0.0,0\r\n",
+ "T2,0.000,0.0,9223372036854775808.8,9223372036854775808.8,4294967295,4294967295,0,429496729,0,0,0,0,0,0,0,0,0,0,0.0,21474836.0,9223372036854775808.8,0,00,0,0,0.00\r\n",
+ "T5,0,0,0,0,0,0,0,0,0,0,0,0,0.0,0.0,*,*,0\r\n",
+ "T6,00,03,00,1F,1F,1F,0E,03,00,00,00,00,1F,1F\r\n",
+ "T7,0,0,0,429496729,21474836.000,429496729,0.000,0,0.000,0,0.000,0,0.000,0,0.000,0,0.000,0,0.000,0,0.000,0,0.000,0,0,0,0.000\r\n",
+ "Tx,*,*,*,*,*,0.0,0,0,0,0,-125,0,0,0,0,0,0,0\r\n",
+ "F3\r\n"));
+
verifyPosition(decoder, text(
- "&&l141,863911061945394,000,0,,230918072531,A,22.678598,114.045970,26,0.6,0,0,74,2286304571,460|0|249F|00001093,20,001C,00,00,04A7|019C|0000|0000,1,C0"));
+ "&&l141,863911061945394,000,0,,230918072531,A,22.678598,114.045970,26,0.6,0,0,74,2286304571,460|0|249F|00001093,20,001C,00,00,04A7|019C|0000|0000,1,C0\r\n"));
verifyAttribute(decoder, text(
- "&&s148,868703050178631,000,37,,230704040211,A,22.678565,114.046011,31,0.5,0,339,77,8,460|0|249F|0AC2620D,27,0000001D,02,00,04F2|01A1|0000|0000,129,,,,949037"),
+ "&&s148,868703050178631,000,37,,230704040211,A,22.678565,114.046011,31,0.5,0,339,77,8,460|0|249F|0AC2620D,27,0000001D,02,00,04F2|01A1|0000|0000,129,,,,949037\r\n"),
Position.KEY_HOURS, 9490000L);
verifyAttribute(decoder, text(
- "&&x164,869926040743375,000,0,,220705205955,A,33.326001,44.445318,10,1.2,0,57,8,925,418|40|038C|000083CD,31,00000015,00,00,0016|016A|0000|0000,1,,,686|33||44|99|14|124|11|8D"),
+ "&&x164,869926040743375,000,0,,220705205955,A,33.326001,44.445318,10,1.2,0,57,8,925,418|40|038C|000083CD,31,00000015,00,00,0016|016A|0000|0000,1,,,686|33||44|99|14|124|11|8D\r\n"),
Position.KEY_FUEL_CONSUMPTION, 1.1);
verifyAttribute(decoder, text(
- "&&R187,860294046453690,000,0,,220105160656,A,22.994986,72.499711,15,0.9,2,222,55,121135784,404|98|147B|0000376A,24,0000001F,02,00,052E|01A3|0000|0000,1,010000|020000,,853|6|10|105|73|41|125|34|52"),
+ "&&R187,860294046453690,000,0,,220105160656,A,22.994986,72.499711,15,0.9,2,222,55,121135784,404|98|147B|0000376A,24,0000001F,02,00,052E|01A3|0000|0000,1,010000|020000,,853|6|10|105|73|41|125|34|52\r\n"),
Position.KEY_FUEL_LEVEL, null);
verifyPosition(decoder, text(
- "&&o142,860262050066062,000,27,,211111070826,V,28.653435,-106.077455,0,0.0,0,151,1412,918,0|0|4708|01402D19,6,0000001A,02,00,04C0|016C|0000|0000,1,,,BB"));
+ "&&o142,860262050066062,000,27,,211111070826,V,28.653435,-106.077455,0,0.0,0,151,1412,918,0|0|4708|01402D19,6,0000001A,02,00,04C0|016C|0000|0000,1,,,BB\r\n"));
verifyPosition(decoder, text(
- "&&W149,865429043319537,000,0,,211103013512,A,22.679003,114.045085,16,1.1,0,271,76,109075,460|0|249F|000010C5,19,0000003E,00,00,0A57|0168|0000|0000,1,0100000C"));
+ "&&W149,865429043319537,000,0,,211103013512,A,22.679003,114.045085,16,1.1,0,271,76,109075,460|0|249F|000010C5,19,0000003E,00,00,0A57|0168|0000|0000,1,0100000C\r\n"));
verifyAttribute(decoder, text(
- "&&:23,860262050015424,129,OKA2"),
- Position.KEY_RESULT, "129,OK");
+ "&&:23,860262050015424,129,OKA2\r\n"),
+ Position.KEY_RESULT, "OK");
verifyPosition(decoder, text(
- "&&X152,861157040151686,000,18,,210907163833,A,10.232715,-67.880423,11,1.4,0,275,437,34804,734|2|3EE4|00579406,28,00000015,00,00,0000|017D|0000|0000,1,010000,,9A"));
+ "&&X152,861157040151686,000,18,,210907163833,A,10.232715,-67.880423,11,1.4,0,275,437,34804,734|2|3EE4|00579406,28,00000015,00,00,0000|017D|0000|0000,1,010000,,9A\r\n"));
verifyPosition(decoder, text(
- "&&o125,861157040554384,000,0,,210702235150,A,27.263505,153.037061,11,1.2,0,0,31,5125,505|1|7032|8C89802,20,0000002D,00,00,01E2|019DF0"));
+ "&&o125,861157040554384,000,0,,210702235150,A,27.263505,153.037061,11,1.2,0,0,31,5125,505|1|7032|8C89802,20,0000002D,00,00,01E2|019DF0\r\n"));
verifyAttribute(decoder, text(
- "&&a152,860262050010565,000,53,8F5300,210528015706,A,-38.229746,145.043446,6,1.5,0,285,84,2102994,505|1|306E|082D6101,31,0000003D,02,02,04C0|01A0|0000|0000,1,,DC"),
+ "&&a152,860262050010565,000,53,8F5300,210528015706,A,-38.229746,145.043446,6,1.5,0,285,84,2102994,505|1|306E|082D6101,31,0000003D,02,02,04C0|01A0|0000|0000,1,,DC\r\n"),
Position.KEY_DRIVER_UNIQUE_ID, "8F5300");
verifyPosition(decoder, text(
- "&&>141,860262050010565,000,36,,210407094323,V,-38.229711,145.043161,0,0.0,0,0,0,14222,505|1|306E|082D6115,24,00000039,00,00,04C0|0164|0000|0000,1,,41"));
+ "&&>141,860262050010565,000,36,,210407094323,V,-38.229711,145.043161,0,0.0,0,0,0,14222,505|1|306E|082D6115,24,00000039,00,00,04C0|0164|0000|0000,1,,41\r\n"));
verifyPosition(decoder, text(
- "&&A147,021104023195429,000,0,,180106093046,A,22.646430,114.065730,8,0.9,54,86,76,326781,460|0|27B3|0EA7,27,0000000F,02,01,04E2|018C|01C8|0000,1,0104B0,01013D|02813546"));
+ "&&A147,021104023195429,000,0,,180106093046,A,22.646430,114.065730,8,0.9,54,86,76,326781,460|0|27B3|0EA7,27,0000000F,02,01,04E2|018C|01C8|0000,1,0104B0,01013D|02813546\r\n"));
verifyPosition(decoder, text(
- "&&y139,860262050009146,000,0,,210323131512,A,22.678655,114.046223,14,1.1,0,231,71,5,460|0|249F|0099C257,28,0000003D,00,00,0493|0199|0000|0000,1,,33"));
+ "&&y139,860262050009146,000,0,,210323131512,A,22.678655,114.046223,14,1.1,0,231,71,5,460|0|249F|0099C257,28,0000003D,00,00,0493|0199|0000|0000,1,,33\r\n"));
}
diff --git a/src/test/java/org/traccar/protocol/Tlt2hProtocolDecoderTest.java b/src/test/java/org/traccar/protocol/Tlt2hProtocolDecoderTest.java
index cdfae465c07..8a7ba84ab7b 100644
--- a/src/test/java/org/traccar/protocol/Tlt2hProtocolDecoderTest.java
+++ b/src/test/java/org/traccar/protocol/Tlt2hProtocolDecoderTest.java
@@ -11,6 +11,19 @@ public void testDecode() throws Exception {
var decoder = inject(new Tlt2hProtocolDecoder(null));
+ verifyPositions(decoder, false, text(
+ "#868105044690301#MT600+#0000#0#0#129#40#0#AUTOLOW#1\r\n" +
+ "#072030fa20c$GPRMC,,V,,,,,,,,,,A*5C\r\n"));
+
+ verifyPositions(decoder, text(
+ "#868105044690301#MT600+#0000#0#0#143#40#0#AUTO#1\r\n",
+ "#072030fcf21$GPRMC,155616.00,A,4931.9210,N,09652.5290,W,53.80,90.00,150224,,,A*48\r\n"));
+
+ verifyAttribute(decoder, text(
+ "#867665041689485#MT700N#0000#HT#1\r\n",
+ "#5065$GPRMC,148996.00,A,2485.2458,N,01258.4535,E,,,151348,,,A*5D\r\n"),
+ Position.KEY_BATTERY, 5.065);
+
verifyPositions(decoder, false, text(
"#862255061983166#MT700NW#0000#TOWED#1\r\n",
"#4502$WIFI,051550.00,A,-50,7683C2CBC0B0,-51,7683C29BC0B0,-51,7683C2BBC0B0,-51,7483C2DBC0B0,-51,7683C2ABC0B0,221123*78\r\n"));
@@ -19,7 +32,7 @@ public void testDecode() throws Exception {
"#862255061825896#MT710#0000#TOWED#1\r\n",
"#39#$WIFI,015259.00,A,-47,7483C2DBC0B0,-48,7683C2ABC0B0,-48,7683C29BC0B0,-48,7683C2CBC0B0,-48,7683C2BBC0B0,151123*74\r\n"));
- verifyNull(decoder, text(
+ verifyPositions(decoder, false, text(
"#860517049471362#MT700#0000#AUTO#1\r\n",
"#36$GPRMC,,V,,,,,,,,,,A*5C\r\n"));
diff --git a/src/test/java/org/traccar/protocol/UproProtocolDecoderTest.java b/src/test/java/org/traccar/protocol/UproProtocolDecoderTest.java
index c99166374b7..f070c620169 100644
--- a/src/test/java/org/traccar/protocol/UproProtocolDecoderTest.java
+++ b/src/test/java/org/traccar/protocol/UproProtocolDecoderTest.java
@@ -83,6 +83,14 @@ public void testDecode() throws Exception {
verifyPosition(decoder, buffer(
"*AI2000905300036,AD1&A1703054913231101844949860000251115&B0500000000&C0;4?72:9&F0000"));
+ verifyAttribute(decoder, buffer(
+ "*HQ200862312328000001,AD1&A1520441548253003503696640017270124&B0000000000&C00000117&F0000&R2118&N01&V0125&X(J01E0)&K00300&Z000&d01286"),
+ Position.PREFIX_ADC + 1, 12.86);
+
+ verifyAttribute(decoder, buffer(
+ "*HQ200862312328000001,BA&A1520461548253003503696640017270124&B0000000000&C00000117&F0000&R2218&N01&V0125&X(J01E0)&K00300&Z000&d01287"),
+ Position.PREFIX_ADC + 1, 12.87);
+
}
}
diff --git a/swagger.json b/swagger.json
index 982e1bff1e2..b2209a20e64 100644
--- a/swagger.json
+++ b/swagger.json
@@ -2,7 +2,7 @@
"openapi": "3.0.1",
"info": {
"title": "Traccar",
- "version": "5.10",
+ "version": "5.12",
"description": "Traccar GPS tracking server API documentation. To use the API you need to have a server instance. For testing purposes you can use one of free [demo servers](https://www.traccar.org/demo-server/). For production use you can install your own server or get a [subscription service](https://www.traccar.org/product/tracking-server/).",
"contact": {
"name": "Traccar Support",
diff --git a/templates/full/deviceExpiration.vm b/templates/full/deviceExpiration.vm
new file mode 100644
index 00000000000..879b31778ba
--- /dev/null
+++ b/templates/full/deviceExpiration.vm
@@ -0,0 +1,7 @@
+#set($subject = "Device expiration")
+
+
+
+Your device $device.name has expired.
+
+
diff --git a/templates/full/deviceExpirationReminder.vm b/templates/full/deviceExpirationReminder.vm
new file mode 100644
index 00000000000..aa47ac0edce
--- /dev/null
+++ b/templates/full/deviceExpirationReminder.vm
@@ -0,0 +1,7 @@
+#set($subject = "Device expiration reminder")
+
+
+
+Your device $device.name will expire on $dateTool.format("YYYY-MM-dd", $expiration, $locale, $timezone).
+
+
diff --git a/templates/full/userExpiration.vm b/templates/full/userExpiration.vm
new file mode 100644
index 00000000000..43fe2563e9e
--- /dev/null
+++ b/templates/full/userExpiration.vm
@@ -0,0 +1,7 @@
+#set($subject = "Account expiration")
+
+
+
+Your user account has expired.
+
+
diff --git a/templates/full/userExpirationReminder.vm b/templates/full/userExpirationReminder.vm
new file mode 100644
index 00000000000..ecdee05888c
--- /dev/null
+++ b/templates/full/userExpirationReminder.vm
@@ -0,0 +1,7 @@
+#set($subject = "Account expiration reminder")
+
+
+
+Your user account will expire on $dateTool.format("YYYY-MM-dd", $expiration, $locale, $timezone).
+
+
diff --git a/traccar-web b/traccar-web
index dc46059f6bf..42db1a41cbf 160000
--- a/traccar-web
+++ b/traccar-web
@@ -1 +1 @@
-Subproject commit dc46059f6bfeedca04333c2839872055db066dc6
+Subproject commit 42db1a41cbf733ea4f82c86e5d45a6fbccc8b8f2