Parcourir la source

Added Fm4exC8Ex parser and tests

Lukas Cerny il y a 2 ans
Parent
commit
3e0b94d04e
30 fichiers modifiés avec 776 ajouts et 73 suppressions
  1. 4 6
      Dockerfile
  2. 6 0
      build.gradle
  3. 1 0
      doc/binaryFile.txt
  4. 16 0
      src/main/java/cz/senslog/telemetry/database/domain/ObsTelemetry.java
  5. 30 0
      src/main/java/cz/senslog/telemetry/database/repository/CachedMapLogRepository.java
  6. 14 5
      src/main/java/cz/senslog/telemetry/database/repository/MapLogRepository.java
  7. 55 0
      src/main/java/cz/senslog/telemetry/database/repository/MockMapLogRepository.java
  8. 20 0
      src/main/java/cz/senslog/telemetry/database/repository/SensLogRepository.java
  9. 8 0
      src/main/java/cz/senslog/telemetry/domain/IOProperty.java
  10. 15 0
      src/main/java/cz/senslog/telemetry/domain/TelemetryAVLRecord.java
  11. 33 12
      src/main/java/cz/senslog/telemetry/protocol/Fm4ex.java
  12. 32 33
      src/main/java/cz/senslog/telemetry/protocol/Fm4exC8.java
  13. 119 0
      src/main/java/cz/senslog/telemetry/protocol/Fm4exC8Ex.java
  14. 1 0
      src/main/java/cz/senslog/telemetry/protocol/Fm4exCodecType.java
  15. 0 1
      src/main/java/cz/senslog/telemetry/protocol/Fm4exParser.java
  16. 40 6
      src/main/java/cz/senslog/telemetry/server/Fm4exSocketHandler.java
  17. 9 3
      src/main/java/cz/senslog/telemetry/server/HttpVertxServer.java
  18. 3 0
      src/main/java/cz/senslog/telemetry/server/LoopInvoker.java
  19. 4 3
      src/main/java/cz/senslog/telemetry/server/OpenAPIHandler.java
  20. 14 3
      src/main/java/cz/senslog/telemetry/server/TCPVertxServer.java
  21. 66 0
      src/main/java/cz/senslog/telemetry/utils/CRC16.java
  22. 45 0
      src/main/java/cz/senslog/telemetry/utils/ResourcesUtils.java
  23. 228 0
      src/test/java/cz/senslog/telemetry/protocol/Fm4exByFileTest.java
  24. 13 1
      src/test/java/cz/senslog/telemetry/protocol/Fm4exTest.java
  25. BIN
      src/test/resources/Fm4exC8Ex_AVLData/test1.bin
  26. BIN
      src/test/resources/Fm4exC8Ex_AVLData/test2.bin
  27. BIN
      src/test/resources/Fm4exC8Ex_AVLData/test3.bin
  28. BIN
      src/test/resources/Fm4exC8Ex_IMEI/test1.bin
  29. BIN
      src/test/resources/Fm4exC8Ex_IMEI/test2.bin
  30. BIN
      src/test/resources/Fm4exC8Ex_IMEI/test3.bin

+ 4 - 6
Dockerfile

@@ -19,13 +19,11 @@ WORKDIR /app
 
 RUN ./gradlew test
 
-FROM openjdk:17 AS production
+FROM openjdk:17-jdk-slim-buster AS production
 
-COPY --from=builder /app/build /app/build
-COPY gradle /app/gradle
-COPY build.gradle settings.gradle gradle.properties gradlew /app/
+COPY --from=builder /app/build/libs/ /app/
+COPY gradle.properties /app/
 
 WORKDIR /app
 
-# CMD java -DhttpServerPort=80 -cp "bin/*" cz.senslog.telemetry.app.Main
-CMD ./gradlew run
+CMD java -cp "telemetry.jar" cz.senslog.telemetry.app.Main

+ 6 - 0
build.gradle

@@ -33,13 +33,19 @@ build {
 }
 
 jar {
+    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
     manifest {
         attributes 'Implementation-Title': 'SensLog Telemetry',
                 'Implementation-Version': version,
                 'Main-Class': 'cz.senslog.telemetry.app.Main'
     }
+    from {
+        configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
+    }
 }
 
+jar.archiveFileName = "telemetry.jar"
+
 test {
     useJUnitPlatform()
 }

Fichier diff supprimé car celui-ci est trop grand
+ 1 - 0
doc/binaryFile.txt


+ 16 - 0
src/main/java/cz/senslog/telemetry/database/domain/ObsTelemetry.java

@@ -3,6 +3,7 @@ package cz.senslog.telemetry.database.domain;
 import io.vertx.core.json.JsonObject;
 
 import java.time.OffsetDateTime;
+import java.time.format.DateTimeFormatter;
 
 public class ObsTelemetry {
 
@@ -100,4 +101,19 @@ public class ObsTelemetry {
     public void setSpeed(float speed) {
         this.speed = speed;
     }
+
+    @Override
+    public String toString() {
+        return "{" +
+                "\"id\":" + id +
+                ", \"timestamp\":" + "\""+timestamp.format(DateTimeFormatter.ISO_DATE_TIME)+"\"" +
+                ", \"unitId\":" + unitId +
+                ", \"observedValues\":" + observedValues +
+                ", \"longitude\":" + longitude +
+                ", \"latitude\":" + latitude +
+                ", \"altitude\":" + altitude +
+                ", \"angle\":" + angle +
+                ", \"speed\":" + speed +
+                '}';
+    }
 }

+ 30 - 0
src/main/java/cz/senslog/telemetry/database/repository/CachedMapLogRepository.java

@@ -0,0 +1,30 @@
+package cz.senslog.telemetry.database.repository;
+
+import cz.senslog.telemetry.database.domain.Unit;
+import io.vertx.core.Future;
+import io.vertx.pgclient.PgPool;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class CachedMapLogRepository extends MapLogRepository {
+
+    public static CachedMapLogRepository create(PgPool client) {
+        return new CachedMapLogRepository(client);
+    }
+
+    protected CachedMapLogRepository(PgPool client) {
+        super(client);
+    }
+
+    private final Map<String, Unit> CACHE_IMEI_TO_UNIT = new HashMap<>();
+
+    @Override
+    public Future<Unit> findUnitByIMEI(String imei) {
+        if (CACHE_IMEI_TO_UNIT.containsKey(imei)) {
+            return Future.succeededFuture(CACHE_IMEI_TO_UNIT.get(imei));
+        }
+        return super.findUnitByIMEI(imei)
+                .andThen(u -> CACHE_IMEI_TO_UNIT.put(imei, u.result()));
+    }
+}

+ 14 - 5
src/main/java/cz/senslog/telemetry/database/repository/MapLogRepository.java

@@ -22,13 +22,13 @@ import java.util.stream.StreamSupport;
 
 import static java.util.stream.Collectors.toList;
 
-public class MapLogRepository {
+public class MapLogRepository implements SensLogRepository {
 
     private static final Logger logger = LogManager.getLogger(MapLogRepository.class);
 
     private final PgPool client;
 
-    private MapLogRepository(PgPool client) {
+    protected MapLogRepository(PgPool client) {
         this.client = client;
     }
 
@@ -37,8 +37,10 @@ public class MapLogRepository {
     }
 
 
+    @Override
     public Future<Integer> saveTelemetry(ObsTelemetry data) {
-        return client.preparedQuery("INSERT INTO maplog.obs_telemetry(time_stamp, unit_id, observed_values, the_geom) VALUES ($1, $2, $3::json, ST_SetSRID(ST_MakePoint($4, $5), 4326)) RETURNING (id)")
+        return client.preparedQuery("INSERT INTO maplog.obs_telemetry(time_stamp, unit_id, observed_values, the_geom) " +
+                        "VALUES ($1, $2, $3::json, ST_SetSRID(ST_MakePoint($4, $5), 4326)) RETURNING (id)")
                 .execute(Tuple.of(data.getTimestamp(),
                         data.getUnitId(),
                         data.getObservedValues().toString(),
@@ -56,6 +58,7 @@ public class MapLogRepository {
                 .onFailure(logger::error);
     }
 
+    @Override
     public Future<Integer> saveAllTelemetry(List<ObsTelemetry> data) {
         List<Tuple> tuples = data.stream().map(d -> Tuple.of(
                 d.getTimestamp(),
@@ -64,11 +67,13 @@ public class MapLogRepository {
                 d.getLongitude(),
                 d.getLatitude()
         )).collect(toList());
-        return client.preparedQuery("INSERT INTO maplog.obs_telemetry(time_stamp, unit_id, observed_values, the_geom) VALUES ($1, $2,  $3::json, ST_SetSRID(ST_MakePoint($4, $5), 4326))")
+        return client.preparedQuery("INSERT INTO maplog.obs_telemetry(time_stamp, unit_id, observed_values, the_geom) " +
+                        "VALUES ($1, $2,  $3::json, ST_SetSRID(ST_MakePoint($4, $5), 4326))")
                 .executeBatch(tuples)
                 .map(SqlResult::rowCount);
     }
 
+    @Override
     public Future<List<Unit>> loadAllUnits() {
         return client.query("SELECT unit_id FROM maplog.unit")
                 .execute()
@@ -81,6 +86,7 @@ public class MapLogRepository {
     private static final Function<Row, Unit> ROW_TO_UNIT = (row) ->
             Unit.of(row.getLong("unit_id"), row.getString("imei"));
 
+    @Override
     public Future<Unit> findUnitByIMEI(String imei) {
         return client.preparedQuery("SELECT unit_id, imei FROM maplog.unit WHERE imei = $1")
                 .execute(Tuple.of(imei))
@@ -95,6 +101,7 @@ public class MapLogRepository {
             row.getOffsetDateTime("to_time")
         );
 
+    @Override
     public Future<List<Campaign>> allCampaigns() {
         return client.query("SELECT campaign_id, description, from_time, to_time FROM maplog.campaign ORDER BY campaign_id")
                 .execute()
@@ -104,6 +111,7 @@ public class MapLogRepository {
                 );
     }
 
+    @Override
     public Future<Campaign> findCampaignById(long campaignId) {
         return client.preparedQuery("SELECT campaign_id, description, from_time, to_time FROM maplog.campaign WHERE campaign_id = $1")
                 .execute(Tuple.of(campaignId))
@@ -113,6 +121,7 @@ public class MapLogRepository {
                 .map(p -> p.orElseThrow(() -> new DataNotFoundException(String.format("Campaign ID '%d' not found.", campaignId))));
     }
 
+    @Override
     public Future<List<Long>> findUnitIdsByCampaignId(long campaignId) {
         return client.preparedQuery("SELECT unit_id FROM maplog.unit_to_campaign WHERE camp_id = $1")
                 .execute(Tuple.of(campaignId))
@@ -122,7 +131,7 @@ public class MapLogRepository {
                 );
     }
 
-
+    @Override
     public Future<List<ObsTelemetry>> findObservationsByCampaignId(long campaignId) {
         return client.preparedQuery(
                     "SELECT obs.obs_id, obs.time_stamp, obs.unit_id, obs.observed_values, ST_X (ST_Transform (obs.the_geom, 4326)) AS long, ST_Y (ST_Transform (obs.the_geom, 4326)) AS lat FROM maplog.obs_telemetry AS obs JOIN maplog.unit AS u ON u.unit_id = obs.unit_id JOIN maplog.unit_to_campaign AS u_cam ON u_cam.unit_id = u.unit_id JOIN maplog.campaign AS c ON c.campaign_id = u_cam.camp_id WHERE c.campaign_id = $1 AND c.from_time <= u_cam.from_time AND c.to_time >= u_cam.to_time")

+ 55 - 0
src/main/java/cz/senslog/telemetry/database/repository/MockMapLogRepository.java

@@ -0,0 +1,55 @@
+package cz.senslog.telemetry.database.repository;
+
+import cz.senslog.telemetry.database.domain.Campaign;
+import cz.senslog.telemetry.database.domain.ObsTelemetry;
+import cz.senslog.telemetry.database.domain.Unit;
+import io.vertx.core.Future;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.time.OffsetDateTime;
+import java.util.Collections;
+import java.util.List;
+
+public class MockMapLogRepository implements SensLogRepository {
+
+    private static final Logger logger = LogManager.getLogger(MockMapLogRepository.class);
+
+    public Future<Integer> saveTelemetry(ObsTelemetry data) {
+        logger.info("SAVE: timestamp {} | unit_id {} | observed_value {} | lon {} | lat {}",
+                data.getTimestamp(), data.getUnitId(), data.getObservedValues().toString(), data.getLongitude(), data.getLatitude());
+        return Future.succeededFuture(1);
+    }
+
+    public Future<Integer> saveAllTelemetry(List<ObsTelemetry> data) {
+        data.forEach(this::saveTelemetry);
+        return Future.succeededFuture(data.size());
+    }
+
+    public Future<List<Unit>> loadAllUnits() {
+        return Future.succeededFuture(Collections.emptyList());
+    }
+
+    public Future<Unit> findUnitByIMEI(String imei) {
+        return Future.succeededFuture(Unit.of(1000L, imei));
+    }
+
+    public Future<List<Campaign>> allCampaigns() {
+        return Future.succeededFuture(List.of(
+                Campaign.of(1L, "mock campaign", OffsetDateTime.MIN, OffsetDateTime.MAX)
+        ));
+    }
+
+    public Future<Campaign> findCampaignById(long campaignId) {
+        return Future.succeededFuture(Campaign.of(campaignId, "mock campaign", OffsetDateTime.MIN, OffsetDateTime.MAX));
+    }
+
+    public Future<List<Long>> findUnitIdsByCampaignId(long campaignId) {
+        return Future.succeededFuture(List.of(1L));
+    }
+
+
+    public Future<List<ObsTelemetry>> findObservationsByCampaignId(long campaignId) {
+        return Future.succeededFuture(Collections.emptyList());
+    }
+}

+ 20 - 0
src/main/java/cz/senslog/telemetry/database/repository/SensLogRepository.java

@@ -0,0 +1,20 @@
+package cz.senslog.telemetry.database.repository;
+
+import cz.senslog.telemetry.database.domain.Campaign;
+import cz.senslog.telemetry.database.domain.ObsTelemetry;
+import cz.senslog.telemetry.database.domain.Unit;
+import io.vertx.core.Future;
+
+import java.util.List;
+
+public interface SensLogRepository {
+
+    Future<Integer> saveTelemetry(ObsTelemetry data);
+    Future<Integer> saveAllTelemetry(List<ObsTelemetry> data);
+    Future<List<Unit>> loadAllUnits();
+    Future<Unit> findUnitByIMEI(String imei);
+    Future<List<Campaign>> allCampaigns();
+    Future<Campaign> findCampaignById(long campaignId);
+    Future<List<Long>> findUnitIdsByCampaignId(long campaignId);
+    Future<List<ObsTelemetry>> findObservationsByCampaignId(long campaignId);
+}

+ 8 - 0
src/main/java/cz/senslog/telemetry/domain/IOProperty.java

@@ -21,4 +21,12 @@ public class IOProperty {
     public long getValue() {
         return value;
     }
+
+    @Override
+    public String toString() {
+        return "{" +
+                "\"id\":" + id +
+                ", \"value\":" + value +
+                "}";
+    }
 }

+ 15 - 0
src/main/java/cz/senslog/telemetry/domain/TelemetryAVLRecord.java

@@ -88,4 +88,19 @@ public class TelemetryAVLRecord {
     public void setSpeed(int speed) {
         this.speed = speed;
     }
+
+    @Override
+    public String toString() {
+        return "{" +
+                "\"timestamp\":" + "\""+timestamp+"\"" +
+                ", \"priority\":" + priority +
+                ", \"longitude\":" + longitude +
+                ", \"latitude\":" + latitude +
+                ", \"altitude\":" + altitude +
+                ", \"angle\":" + angle +
+                ", \"satellites\":" + satellites +
+                ", \"speed\":" + speed +
+                ", \"ioProperties\":" + ioProperties +
+                "}";
+    }
 }

+ 33 - 12
src/main/java/cz/senslog/telemetry/protocol/Fm4ex.java

@@ -1,37 +1,58 @@
 package cz.senslog.telemetry.protocol;
 
 import cz.senslog.telemetry.domain.TelemetryAVLRecord;
+import cz.senslog.telemetry.utils.CRC16;
 import io.vertx.core.buffer.Buffer;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
 
 public class Fm4ex {
 
-    private static final Logger logger = LogManager.getLogger(Fm4ex.class);
+    public static float coordToDecimalDegrees(long coordination) {
+        // TODO solve unsigned int problem and its shift
+//        long firstByte = (coordination >> 1) & 1;
+//        coordination *= (firstByte == 0 ? 1 : -1); // Get first bit of the coordination: if bit is 0 then cord is positive else negative
+        return coordination / 10000000.0f; // Precision division to get Decimal Degrees format D.D°
+    }
 
     public static String parseIMEI(Buffer buffer) {
         return buffer.getString(2, buffer.getShort(0) + 2);
     }
 
-    public static TelemetryAVLRecord[] parseAVL(Buffer buffer) {
+    public static TelemetryAVLRecord[] parseAVL(Buffer avlPacket) {
 
-        int preamble = buffer.getInt(0);
+        int preamble = avlPacket.getInt(0);
         if (preamble != 0) {
             return new TelemetryAVLRecord[0];
         }
 
-        int dataLen = buffer.getInt(4);
+        int dataLen = avlPacket.getInt(4);
         if (dataLen == 0) {
             return new TelemetryAVLRecord[0];
         }
 
-        int crc = buffer.getInt(dataLen+8); // 51151 or 00 00 C7 CF
-        // check CRC16-IBM
+        int nmOfRecords = avlPacket.getByte(dataLen+7);
+
+        Buffer avlData = avlPacket.getBuffer(8, dataLen+8);
 
-        Buffer avlBuffer = buffer.getBuffer(8, dataLen+8);
+        long CRCGet = avlPacket.getUnsignedInt(dataLen+8);
+        int CRCVerify = CRC16.calculate(avlData.getBytes());
+        if (CRCVerify != CRCGet) {
+            throw new IllegalArgumentException(
+                    String.format("Protocol consistency failed at the provided CRC value. " +
+                            "Expected <%d> but was <%d>.", CRCGet, CRCVerify)
+            );
+        }
 
-        return Fm4exCodecType.getType(avlBuffer.getByte(0))
+        TelemetryAVLRecord[] records = Fm4exCodecType.getType(avlData.getUnsignedByte(0))
                 .orElseThrow(() -> new IllegalArgumentException("Unsupported codec type."))
-                .parser().parse(avlBuffer);
+                .parser().parse(avlData);
+
+        if (nmOfRecords != records.length) {
+            throw new IllegalArgumentException(
+                    String.format("Protocol consistency failed at the provided AVL Data length. " +
+                            "Expected <%d> but was <%d>.", nmOfRecords, records.length)
+            );
+        }
+
+        return records;
     }
-}
+}

+ 32 - 33
src/main/java/cz/senslog/telemetry/protocol/Fm4exC8.java

@@ -3,27 +3,26 @@ package cz.senslog.telemetry.protocol;
 import cz.senslog.telemetry.domain.IOProperty;
 import cz.senslog.telemetry.domain.TelemetryAVLRecord;
 import io.vertx.core.buffer.Buffer;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
 
 import java.time.Instant;
 import java.util.Optional;
 
-public class Fm4exC8 implements Fm4exParser {
+import static cz.senslog.telemetry.protocol.Fm4ex.coordToDecimalDegrees;
 
-    private static final Logger logger = LogManager.getLogger(Fm4exC8.class);
+public class Fm4exC8 implements Fm4exParser {
 
     @Override
     public TelemetryAVLRecord[] parse(Buffer buffer) {
-        byte byteIndex = 0;
+        int byteIndex = 0;
 
-        Optional<Fm4exCodecType> codecType = Fm4exCodecType.getType(buffer.getByte(byteIndex++));
+        short codec = buffer.getUnsignedByte(byteIndex++);
+        Optional<Fm4exCodecType> codecType = Fm4exCodecType.getType(codec);
         if (codecType.isEmpty() || codecType.get() != Fm4exCodecType.CODEC_8) {
             return new TelemetryAVLRecord[0];
         }
 
         byte dataNm = buffer.getByte(byteIndex++);
-        if (dataNm == 0) {
+        if (dataNm <= 0) {
             return new TelemetryAVLRecord[0];
         }
 
@@ -31,7 +30,7 @@ public class Fm4exC8 implements Fm4exParser {
         for (int recordIndex = 0; recordIndex < records.length; recordIndex++) {
             TelemetryAVLRecord tel = new TelemetryAVLRecord();
 
-            tel.setTimestamp(Instant.ofEpochMilli(buffer.getLong(byteIndex)));
+            tel.setTimestamp(Instant.ofEpochMilli(buffer.getLongLE(byteIndex)));
             byteIndex+=8;
 
             tel.setPriority(buffer.getByte(byteIndex));
@@ -60,34 +59,45 @@ public class Fm4exC8 implements Fm4exParser {
             }
 
             // IO
-            byte eventIOid = buffer.getByte(byteIndex++);
-            byte nmOfEventIDs = buffer.getByte(byteIndex++);
+            byte eventIOid = buffer.getByte(byteIndex);
+            byteIndex+=1;
+
+            byte nmOfEventIDs = buffer.getByte(byteIndex);
+            byteIndex+=1;
+
+            byte nmOf1BIOs = buffer.getByte(byteIndex);
+            byteIndex+=1;
 
-            byte nmOf1BIOs = buffer.getByte(byteIndex++);
             for (int count = 0; count < nmOf1BIOs; count++, byteIndex+=1) {
-                int io1BId = Byte.toUnsignedInt(buffer.getByte(byteIndex++));
-                byte io1BVal = buffer.getByte(byteIndex);
+                int io1BId = Short.toUnsignedInt(buffer.getUnsignedByte(byteIndex++));
+                short io1BVal = buffer.getUnsignedByte(byteIndex);
                 tel.getIoProperties().add(IOProperty.create(io1BId, io1BVal));
             }
 
-            byte nmOf2BIOs = buffer.getByte(byteIndex++);
+            byte nmOf2BIOs = buffer.getByte(byteIndex);
+            byteIndex+=1;
+
             for (int count = 0; count < nmOf2BIOs; count++, byteIndex+=2) {
-                int io2BId = Byte.toUnsignedInt(buffer.getByte(byteIndex++));
-                short io2BVal = buffer.getShort(byteIndex);
+                int io2BId = Short.toUnsignedInt(buffer.getUnsignedByte(byteIndex++));
+                int io2BVal = buffer.getUnsignedShort(byteIndex);
                 tel.getIoProperties().add(IOProperty.create(io2BId, io2BVal));
             }
 
-            byte nmOf4BIOs = buffer.getByte(byteIndex++);
+            byte nmOf4BIOs = buffer.getByte(byteIndex);
+            byteIndex+=1;
+
             for (int count = 0; count < nmOf4BIOs; count++, byteIndex+=4) {
-                int io4BId = Byte.toUnsignedInt(buffer.getByte(byteIndex++));
-                int io4BVal = buffer.getInt(byteIndex);
+                int io4BId = Short.toUnsignedInt(buffer.getUnsignedByte(byteIndex++));
+                long io4BVal = buffer.getUnsignedInt(byteIndex);
                 tel.getIoProperties().add(IOProperty.create(io4BId, io4BVal));
             }
 
-            byte nmOf8BIOs = buffer.getByte(byteIndex++);
+            byte nmOf8BIOs = buffer.getByte(byteIndex);
+            byteIndex+=1;
+
             for (int count = 0; count < nmOf8BIOs; count++, byteIndex+=8) {
-                int io8BId = Byte.toUnsignedInt(buffer.getByte(byteIndex++));
-                long io8BVal = buffer.getLong(byteIndex);
+                int io8BId = Short.toUnsignedInt(buffer.getUnsignedByte(byteIndex++));
+                long io8BVal = buffer.getLongLE(byteIndex);
                 tel.getIoProperties().add(IOProperty.create(io8BId, io8BVal));
             }
 
@@ -95,17 +105,6 @@ public class Fm4exC8 implements Fm4exParser {
             records[recordIndex] = tel;
         }
 
-        byte nmTotalRecords = buffer.getByte(byteIndex);
-        if (nmTotalRecords != records.length) {
-            logger.error("Number of records does not match, expected {} was {}.", nmTotalRecords, records.length);
-            return new TelemetryAVLRecord[0];
-        }
-
         return records;
     }
-
-    public static float coordToDecimalDegrees(int coordination) {
-        coordination *= ((coordination >> 1) & 1) == 0 ? 1 : -1; // Get first bit of the coordination: if bit is 0 then cord is positive else negative
-        return coordination / 10000000.0f; // Precision division to get Decimal Degrees format D.D°
-    }
 }

+ 119 - 0
src/main/java/cz/senslog/telemetry/protocol/Fm4exC8Ex.java

@@ -0,0 +1,119 @@
+package cz.senslog.telemetry.protocol;
+
+import cz.senslog.telemetry.domain.IOProperty;
+import cz.senslog.telemetry.domain.TelemetryAVLRecord;
+import io.vertx.core.buffer.Buffer;
+
+import java.math.BigInteger;
+import java.time.Instant;
+import java.util.Optional;
+
+import static cz.senslog.telemetry.protocol.Fm4ex.coordToDecimalDegrees;
+
+public class Fm4exC8Ex implements Fm4exParser {
+
+    @Override
+    public TelemetryAVLRecord[] parse(Buffer buffer) {
+        int byteIndex = 0;
+
+        short codec = buffer.getUnsignedByte(byteIndex++);
+        Optional<Fm4exCodecType> codecType = Fm4exCodecType.getType(codec);
+        if (codecType.isEmpty() || codecType.get() != Fm4exCodecType.CODEC_8Ex) {
+            return new TelemetryAVLRecord[0];
+        }
+
+        short dataNm = buffer.getUnsignedByte(byteIndex++);
+        if (dataNm <= 0) {
+            return new TelemetryAVLRecord[0];
+        }
+
+        TelemetryAVLRecord[] records = new TelemetryAVLRecord[dataNm];
+        for (int recordIndex = 0; recordIndex < records.length; recordIndex++) {
+            TelemetryAVLRecord tel = new TelemetryAVLRecord();
+
+            tel.setTimestamp(Instant.ofEpochMilli(buffer.getLong(byteIndex)));
+            byteIndex += 8;
+
+            tel.setPriority(buffer.getUnsignedByte(byteIndex));
+            byteIndex += 1;
+
+            tel.setLongitude(coordToDecimalDegrees(buffer.getUnsignedInt(byteIndex)));
+            byteIndex += 4;
+
+            tel.setLatitude(coordToDecimalDegrees(buffer.getUnsignedInt(byteIndex)));
+            byteIndex += 4;
+
+            tel.setAltitude(buffer.getUnsignedShort(byteIndex));
+            byteIndex += 2;
+
+            tel.setAngle(buffer.getUnsignedShort(byteIndex));
+            byteIndex += 2;
+
+            tel.setSatellites(buffer.getUnsignedByte(byteIndex));
+            byteIndex += 1;
+
+            tel.setSpeed(buffer.getUnsignedShort(byteIndex));
+            byteIndex += 2;
+
+            if (tel.getSpeed() == 0) {
+                // TODO GPS data is invalid
+            }
+
+            // IO
+            int eventIOid = buffer.getUnsignedShort(byteIndex);
+            byteIndex+=2;
+
+            int nmOfEventIDs = buffer.getUnsignedShort(byteIndex);
+            byteIndex+=2;
+
+            int nmOf1BIOs = buffer.getUnsignedShort(byteIndex);
+            byteIndex+=2;
+            for (int count = 0; count < nmOf1BIOs; count++, byteIndex+=1) {
+                int io1BId = buffer.getUnsignedShort(byteIndex);
+                byteIndex+=2;
+
+                short io1BVal = buffer.getUnsignedByte(byteIndex);
+                tel.getIoProperties().add(IOProperty.create(io1BId, io1BVal));
+            }
+
+            int nmOf2BIOs = buffer.getUnsignedShort(byteIndex);
+            byteIndex+=2;
+            for (int count = 0; count < nmOf2BIOs; count++, byteIndex+=2) {
+                int io2BId = buffer.getUnsignedShort(byteIndex);
+                byteIndex+=2;
+
+                int io2BVal = buffer.getUnsignedShort(byteIndex);
+                tel.getIoProperties().add(IOProperty.create(io2BId, io2BVal));
+            }
+
+            int nmOf4BIOs = buffer.getUnsignedShort(byteIndex);
+            byteIndex+=2;
+
+            for (int count = 0; count < nmOf4BIOs; count++, byteIndex+=4) {
+                int io4BId = buffer.getUnsignedShort(byteIndex);
+                byteIndex+=2;
+
+                long io4BVal = buffer.getUnsignedInt(byteIndex);
+                tel.getIoProperties().add(IOProperty.create(io4BId, io4BVal));
+            }
+
+            int nmOf8BIOs = buffer.getUnsignedShort(byteIndex);
+            byteIndex+=2;
+
+            for (int count = 0; count < nmOf8BIOs; count++, byteIndex+=8) {
+                int io8BId = buffer.getUnsignedShort(byteIndex);
+                byteIndex+=2;
+
+                long io8BVal = buffer.getLong(byteIndex);
+                tel.getIoProperties().add(IOProperty.create(io8BId, io8BVal));
+            }
+
+            int nmOfXBIOs = buffer.getUnsignedShort(byteIndex);
+            byteIndex+=2;
+
+            records[recordIndex] = tel;
+        }
+
+        return records;
+    }
+}

+ 1 - 0
src/main/java/cz/senslog/telemetry/protocol/Fm4exCodecType.java

@@ -5,6 +5,7 @@ import java.util.Optional;
 public enum Fm4exCodecType {
 
     CODEC_8      (0x08, new Fm4exC8()),
+    CODEC_8Ex   (0x8E, new Fm4exC8Ex())
 
     ;
     private final int codecId;

+ 0 - 1
src/main/java/cz/senslog/telemetry/protocol/Fm4exParser.java

@@ -6,5 +6,4 @@ import io.vertx.core.buffer.Buffer;
 public interface Fm4exParser {
 
     TelemetryAVLRecord[] parse(Buffer buffer);
-
 }

+ 40 - 6
src/main/java/cz/senslog/telemetry/server/Fm4exSocketHandler.java

@@ -3,6 +3,7 @@ package cz.senslog.telemetry.server;
 import cz.senslog.telemetry.database.domain.ObsTelemetry;
 import cz.senslog.telemetry.database.domain.Unit;
 import cz.senslog.telemetry.database.repository.MapLogRepository;
+import cz.senslog.telemetry.database.repository.SensLogRepository;
 import cz.senslog.telemetry.domain.IOProperty;
 import cz.senslog.telemetry.domain.TelemetryAVLRecord;
 import cz.senslog.telemetry.protocol.Fm4ex;
@@ -12,6 +13,11 @@ import io.vertx.core.json.JsonObject;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.time.Instant;
 import java.util.*;
 import java.util.function.Function;
 
@@ -26,14 +32,21 @@ public class Fm4exSocketHandler {
 
     private final Function<String, LoopInvoker<Buffer, Byte, SocketContext>> newContext;
 
-    private Fm4exSocketHandler(MapLogRepository repo) {
+    private Fm4exSocketHandler(SensLogRepository repo) {
         this.newContext = socId -> invoker.init(new SocketContext(socId, repo));
     }
 
-    public static Fm4exSocketHandler create(MapLogRepository repo) {
+    public static Fm4exSocketHandler create(SensLogRepository repo) {
         return new Fm4exSocketHandler(repo);
     }
 
+    public Future<String> destroySocket(String socketID) {
+        if (!sockets.containsKey(socketID)) {
+            return Future.failedFuture(String.format("Socket ID '%s' does not exist.", socketID));
+        }
+        return Future.succeededFuture(sockets.remove(socketID).context().socketId);
+    }
+
     public Future<Buffer> process(String socketID, Buffer buffer) {
         return sockets.computeIfAbsent(socketID, newContext).invoke(buffer)
                 .map(res -> Buffer.buffer(new byte[]{res}));
@@ -45,20 +58,26 @@ public class Fm4exSocketHandler {
 
         private static final Byte SUCCESS = (byte)0x01, ERROR = (byte)0x00;
 
-        private final MapLogRepository repo;
+        private final SensLogRepository repo;
 
         private final String socketId;
 
         private Unit contextUnit;
 
-        private SocketContext(String socketId, MapLogRepository repo) {
+        private SocketContext(String socketId, SensLogRepository repo) {
             logger.info("[{}] New socket handling.", socketId);
             this.socketId = socketId;
             this.repo = repo;
         }
 
         public Future<Byte> handleIMEI(Buffer buffer) {
-            String imei = Fm4ex.parseIMEI(buffer);
+            String imei;
+            try {
+                imei = Fm4ex.parseIMEI(buffer);
+            } catch (RuntimeException e) {
+                tempFileWriter("IMEI_ERROR", buffer.getBytes(), Instant.now(), "bin");
+                throw new IllegalArgumentException(e);
+            }
             return repo.findUnitByIMEI(imei).map(u -> {
                 if (u != null) {
                     contextUnit = u;
@@ -76,7 +95,6 @@ public class Fm4exSocketHandler {
             if (contextUnit == null) {
                 return Future.succeededFuture(ERROR);
             }
-
             TelemetryAVLRecord[] avlRecords;
             try {
                 avlRecords = Fm4ex.parseAVL(buffer);
@@ -85,6 +103,9 @@ public class Fm4exSocketHandler {
                 return Future.succeededFuture(ERROR);
             }
 
+            Instant timeNow = Instant.now();
+            tempFileWriter("AVLPacket", Arrays.toString(avlRecords).getBytes(StandardCharsets.UTF_8), timeNow, "json");
+
             if (avlRecords.length == 0) {
                 return Future.succeededFuture(ERROR);
             }
@@ -106,6 +127,8 @@ public class Fm4exSocketHandler {
                 }
                 obs.setObservedValues(observedValues);
             }
+            tempFileWriter("Telemetry", telemetries.toString().getBytes(StandardCharsets.UTF_8), timeNow, "json");
+
             Future<Integer> result = repo.saveAllTelemetry(telemetries);
             if (result.failed()) {
                 logger.catching(result.cause());
@@ -113,5 +136,16 @@ public class Fm4exSocketHandler {
             }
             return Future.succeededFuture(result.result() > 0 ? SUCCESS : ERROR);
         }
+
+        @Deprecated
+        public static void tempFileWriter(String fileName, byte[] bytes, Instant at, String ext) {
+            final String dir = "/volume/logs";
+            final String fullName = String.format("%d_%s.%s",at.getEpochSecond(), fileName, ext);
+            try {
+                Files.write(Paths.get(dir, fullName), bytes);
+            } catch (IOException e) {
+                logger.error(e.getMessage());
+            }
+        }
     }
 }

+ 9 - 3
src/main/java/cz/senslog/telemetry/server/HttpVertxServer.java

@@ -2,6 +2,10 @@ package cz.senslog.telemetry.server;
 
 import cz.senslog.telemetry.database.ConnectionPool;
 import cz.senslog.telemetry.database.repository.MapLogRepository;
+import cz.senslog.telemetry.database.repository.MockMapLogRepository;
+import cz.senslog.telemetry.database.repository.SensLogRepository;
+import cz.senslog.telemetry.utils.ResourcesUtils;
+import io.netty.util.internal.ResourcesUtil;
 import io.vertx.core.AbstractVerticle;
 import io.vertx.core.Handler;
 import io.vertx.core.Promise;
@@ -15,7 +19,9 @@ import io.vertx.pgclient.PgPool;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
+import java.io.File;
 import java.net.*;
+import java.nio.file.Path;
 import java.util.function.Function;
 
 public final class HttpVertxServer extends AbstractVerticle {
@@ -23,15 +29,15 @@ public final class HttpVertxServer extends AbstractVerticle {
 
     @Override
     public void start(Promise<Void> startPromise) throws URISyntaxException {
-        URL openApiUrl = this.getClass().getClassLoader().getResource("openAPISpec.yaml");
+        Path openApiUrl = ResourcesUtils.getPath("openAPISpec.yaml");
         logger.info("Loading the OpenAPI spec from '{}'", openApiUrl);
         assert openApiUrl != null;
-        RouterBuilder.create(vertx, openApiUrl.toURI().toString())
+        RouterBuilder.create(vertx, openApiUrl.toString())
                 .onSuccess(routerBuilder -> {
                     logger.info("The OpenAPI specification was loaded successfully.");
 
                     PgPool pgPool = ConnectionPool.createWithVertx(vertx, config());
-                    MapLogRepository repo = MapLogRepository.create(pgPool);
+                    SensLogRepository repo = new MockMapLogRepository(); // MapLogRepository.create(pgPool);
                     OpenAPIHandler apiHandler = OpenAPIHandler.create(repo);
 
                     routerBuilder.operation("infoGET").handler(apiHandler::info);

+ 3 - 0
src/main/java/cz/senslog/telemetry/server/LoopInvoker.java

@@ -17,6 +17,9 @@ public class LoopInvoker<I, O, C> {
 
     private final C telemetryContext;
 
+    public C context() {
+        return telemetryContext;
+    }
 
     public static <I, O, C> Initializer<I, O, C> create() {
         return new Initializer<>();

+ 4 - 3
src/main/java/cz/senslog/telemetry/server/OpenAPIHandler.java

@@ -2,6 +2,7 @@ package cz.senslog.telemetry.server;
 
 import cz.senslog.telemetry.app.Application;
 import cz.senslog.telemetry.database.repository.MapLogRepository;
+import cz.senslog.telemetry.database.repository.SensLogRepository;
 import io.vertx.core.json.JsonArray;
 import io.vertx.core.json.JsonObject;
 import io.vertx.ext.web.RoutingContext;
@@ -15,13 +16,13 @@ import static java.util.stream.Collectors.toList;
 public class OpenAPIHandler {
     private static final Logger logger = LogManager.getLogger(OpenAPIHandler.class);
 
-    private final MapLogRepository repo;
+    private final SensLogRepository repo;
 
-    private OpenAPIHandler(MapLogRepository repo) {
+    private OpenAPIHandler(SensLogRepository repo) {
         this.repo = repo;
     }
 
-    public static OpenAPIHandler create(MapLogRepository repo) {
+    public static OpenAPIHandler create(SensLogRepository repo) {
         return new OpenAPIHandler(repo);
     }
 

+ 14 - 3
src/main/java/cz/senslog/telemetry/server/TCPVertxServer.java

@@ -2,6 +2,8 @@ package cz.senslog.telemetry.server;
 
 import cz.senslog.telemetry.database.ConnectionPool;
 import cz.senslog.telemetry.database.repository.MapLogRepository;
+import cz.senslog.telemetry.database.repository.MockMapLogRepository;
+import cz.senslog.telemetry.database.repository.SensLogRepository;
 import io.vertx.core.AbstractVerticle;
 import io.vertx.core.Promise;
 import io.vertx.core.json.JsonObject;
@@ -11,25 +13,34 @@ import io.vertx.pgclient.PgPool;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
+import java.util.function.Function;
+
 
 public final class TCPVertxServer extends AbstractVerticle {
 
     private static final Logger logger = LogManager.getLogger(TCPVertxServer.class);
 
+
     @Override
     public void start(Promise<Void> startPromise) {
         NetServerOptions serverOpt = new NetServerOptions().setRegisterWriteHandler(true);
         NetServer server = vertx.createNetServer(serverOpt);
 
         PgPool pgPool = ConnectionPool.createWithVertx(vertx, config());
-        MapLogRepository repo = MapLogRepository.create(pgPool);
+        SensLogRepository repo = new MockMapLogRepository(); // MapLogRepository.create(pgPool);
         Fm4exSocketHandler socHandler = Fm4exSocketHandler.create(repo);
         server.connectHandler(socket -> socket.handler(
                             buffer -> socHandler.process(socket.writeHandlerID(), buffer)
                                     .onSuccess(socket::write)
-                                    .onFailure(th -> { logger.catching(th); socket.close(); }))
+                                    .onFailure(th -> {
+                                        logger.error(th);
+                                        socHandler.destroySocket(socket.writeHandlerID())
+                                                .onComplete(res -> socket.close());
+                                    }))
                     .exceptionHandler(logger::error)
-                    .closeHandler(v -> logger.info("The socket '{}' has been closed", socket.writeHandlerID()))
+                    .closeHandler(v -> socHandler.destroySocket(socket.writeHandlerID())
+                            .onSuccess(id -> logger.info("The socket '{}' has been closed", id))
+                            .onFailure(logger::error))
         );
 
         JsonObject serverConfig = config().getJsonObject("server");

+ 66 - 0
src/main/java/cz/senslog/telemetry/utils/CRC16.java

@@ -0,0 +1,66 @@
+package cz.senslog.telemetry.utils;
+
+
+/******************************************************************************
+ *  Compilation:  javac CRC16.java
+ *  Execution:    java CRC16 s
+ *
+ *  Reads in a string s as a command-line argument, and prints out
+ *  its 16-bit Cyclic Redundancy Check (CRC16). Uses a lookup table.
+ *
+ *  Reference:  <a href="http://www.gelato.unsw.edu.au/lxr/source/lib/crc16.c">...</a>
+ *
+ *  % java CRC16 123456789
+ *  CRC16 = bb3d
+ *
+ * Uses irreducible polynomial:  1 + x^2 + x^15 + x^16
+ *
+ *
+ ******************************************************************************/
+
+public class CRC16 {
+
+    private static final int[] table = {
+            0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
+            0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440,
+            0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40,
+            0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841,
+            0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40,
+            0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41,
+            0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641,
+            0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040,
+            0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240,
+            0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441,
+            0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41,
+            0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840,
+            0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41,
+            0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40,
+            0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640,
+            0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041,
+            0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240,
+            0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441,
+            0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41,
+            0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840,
+            0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41,
+            0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40,
+            0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640,
+            0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041,
+            0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241,
+            0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440,
+            0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40,
+            0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841,
+            0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40,
+            0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41,
+            0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641,
+            0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040,
+    };
+
+    public static int calculate(byte[] bytes) {
+        int crc = 0x0000;
+        for (byte b : bytes) {
+            crc = (crc >>> 8) ^ table[(crc ^ b) & 0xff];
+        }
+        return crc;
+    }
+
+}

+ 45 - 0
src/main/java/cz/senslog/telemetry/utils/ResourcesUtils.java

@@ -0,0 +1,45 @@
+package cz.senslog.telemetry.utils;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+public final class ResourcesUtils {
+
+    private static final Logger logger = LogManager.getLogger(ResourcesUtils.class);
+
+
+    public static Path getPath(String resourceFile) {
+        return Paths.get(resourceFile);
+        /*
+        {
+            logger.info("Getting path for the '{}'.", resourceFile);
+            Path path = Paths.get(resourceFile);
+            if (Files.exists(path)) {
+                logger.info("The file located at '{}'.", path);
+                return path;
+            }
+        }
+
+        try {
+            URL url = ResourcesUtils.class.getClassLoader().getResource(resourceFile);
+            Path path = url != null ? Paths.get(url.toURI()) : null;
+            if (path != null && Files.exists(path)) {
+                logger.info("The file located at '{}'.", path);
+                return path;
+            }
+            logger.error("The file '{}' can not be located.", resourceFile);
+            return null;
+        } catch (URISyntaxException e) {
+            logger.error("The file '{}' is not located due to: {}", resourceFile, e.getMessage());
+            return null;
+        }
+
+         */
+    }
+}

+ 228 - 0
src/test/java/cz/senslog/telemetry/protocol/Fm4exByFileTest.java

@@ -0,0 +1,228 @@
+package cz.senslog.telemetry.protocol;
+
+import cz.senslog.telemetry.domain.IOProperty;
+import cz.senslog.telemetry.domain.TelemetryAVLRecord;
+import io.vertx.core.buffer.Buffer;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class Fm4exByFileTest {
+
+    private static void printAsBinary(byte[] bytes) {
+        for (byte b : bytes) {
+            System.out.print(String.format("%8s", Integer.toBinaryString(b & 0xFF)).replace(' ', '0'));
+        }
+        System.out.print("\n");
+    }
+
+    private static Buffer readResourceFile(String filePath) {
+        try {
+            URL url = Fm4exByFileTest.class.getClassLoader().getResource(filePath);
+            Path path = Paths.get(Objects.requireNonNull(url).toURI());
+            byte[] fileBytes = Files.readAllBytes(path);
+            return Buffer.buffer(fileBytes);
+        } catch (URISyntaxException | IOException e) {
+            System.err.println(Arrays.toString(e.getStackTrace()));
+            return Buffer.buffer();
+        }
+    }
+
+    @Test
+    void test1_AVLData() {
+        Buffer buffer = readResourceFile("Fm4exC8Ex_AVLData/test1.bin");
+
+        TelemetryAVLRecord[] records = Fm4ex.parseAVL(buffer);
+        assertEquals(15, records.length);
+
+        TelemetryAVLRecord r1 = records[0];
+        assertEquals(Instant.parse("2023-11-30T17:22:31.542Z"), r1.getTimestamp());
+        assertEquals(0, r1.getPriority());
+        assertEquals(14.699616432189941, r1.getLongitude());
+        assertEquals(49.40732955932617, r1.getLatitude());
+        assertEquals(451, r1.getAltitude());
+        assertEquals(271, r1.getAngle());
+        assertEquals(7, r1.getSatellites());
+        assertEquals(0, r1.getSpeed());
+        List<IOProperty> r1io = r1.getIoProperties();
+        assertEquals(5, r1io.size());
+        assertEquals(239, r1io.get(0).getId());
+        assertEquals(1, r1io.get(0).getValue());
+        assertEquals(67, r1io.get(1).getId());
+        assertEquals(9866, r1io.get(1).getValue());
+        assertEquals(66, r1io.get(2).getId());
+        assertEquals(13509, r1io.get(2).getValue());
+        assertEquals(24, r1io.get(3).getId());
+        assertEquals(0, r1io.get(3).getValue());
+        assertEquals(78, r1io.get(4).getId());
+        assertEquals(0, r1io.get(4).getValue());
+
+        // records r2 - r14
+
+        TelemetryAVLRecord r15 = records[14];
+        assertEquals(Instant.parse("2023-11-30T16:12:31.421Z"), r15.getTimestamp());
+        assertEquals(0, r15.getPriority());
+        assertEquals(14.699616432189941, r15.getLongitude());
+        assertEquals(49.40732955932617, r15.getLatitude());
+        assertEquals(444, r15.getAltitude());
+        assertEquals(271, r15.getAngle());
+        assertEquals(9, r15.getSatellites());
+        assertEquals(0, r15.getSpeed());
+        List<IOProperty> r15io = r15.getIoProperties();
+        assertEquals(5, r15io.size());
+        assertEquals(239, r15io.get(0).getId());
+        assertEquals(1, r15io.get(0).getValue());
+        assertEquals(67, r15io.get(1).getId());
+        assertEquals(9855, r15io.get(1).getValue());
+        assertEquals(66, r15io.get(2).getId());
+        assertEquals(13471, r15io.get(2).getValue());
+        assertEquals(24, r15io.get(3).getId());
+        assertEquals(0, r15io.get(3).getValue());
+        assertEquals(78, r15io.get(4).getId());
+        assertEquals(0, r15io.get(4).getValue());
+    }
+
+    @Test
+    void test2_AVLData() {
+        Buffer buffer = readResourceFile("Fm4exC8Ex_AVLData/test2.bin");
+
+        TelemetryAVLRecord[] records = Fm4ex.parseAVL(buffer);
+
+        assertEquals(15, records.length);
+
+        TelemetryAVLRecord r1 = records[0];
+        assertEquals(Instant.parse("2023-11-30T17:27:31.550Z"), r1.getTimestamp());
+        assertEquals(0, r1.getPriority());
+        assertEquals(14.699616432189941, r1.getLongitude());
+        assertEquals(49.40732955932617, r1.getLatitude());
+        assertEquals(448, r1.getAltitude());
+        assertEquals(271, r1.getAngle());
+        assertEquals(7, r1.getSatellites());
+        assertEquals(0, r1.getSpeed());
+        List<IOProperty> r1io = r1.getIoProperties();
+        assertEquals(5, r1io.size());
+        assertEquals(239, r1io.get(0).getId());
+        assertEquals(1, r1io.get(0).getValue());
+        assertEquals(67, r1io.get(1).getId());
+        assertEquals(9866, r1io.get(1).getValue());
+        assertEquals(66, r1io.get(2).getId());
+        assertEquals(13446, r1io.get(2).getValue());
+        assertEquals(24, r1io.get(3).getId());
+        assertEquals(0, r1io.get(3).getValue());
+        assertEquals(78, r1io.get(4).getId());
+        assertEquals(0, r1io.get(4).getValue());
+
+        // records r2 - r14
+
+        TelemetryAVLRecord r15 = records[14];
+        assertEquals(Instant.parse("2023-11-30T16:17:31.433Z"), r15.getTimestamp());
+        assertEquals(0, r15.getPriority());
+        assertEquals(14.699616432189941, r15.getLongitude());
+        assertEquals(49.40732955932617, r15.getLatitude());
+        assertEquals(444, r15.getAltitude());
+        assertEquals(271, r15.getAngle());
+        assertEquals(8, r15.getSatellites());
+        assertEquals(0, r15.getSpeed());
+        List<IOProperty> r15io = r15.getIoProperties();
+        assertEquals(5, r15io.size());
+        assertEquals(239, r15io.get(0).getId());
+        assertEquals(1, r15io.get(0).getValue());
+        assertEquals(67, r15io.get(1).getId());
+        assertEquals(9866, r15io.get(1).getValue());
+        assertEquals(66, r15io.get(2).getId());
+        assertEquals(13506, r15io.get(2).getValue());
+        assertEquals(24, r15io.get(3).getId());
+        assertEquals(0, r15io.get(3).getValue());
+        assertEquals(78, r15io.get(4).getId());
+        assertEquals(0, r15io.get(4).getValue());
+    }
+
+    @Test
+    void test3_AVLData() {
+        Buffer buffer = readResourceFile("Fm4exC8Ex_AVLData/test3.bin");
+
+        TelemetryAVLRecord[] records = Fm4ex.parseAVL(buffer);
+
+        assertEquals(15, records.length);
+
+        TelemetryAVLRecord r1 = records[0];
+        assertEquals(Instant.parse("2023-11-30T17:42:31.574Z"), r1.getTimestamp());
+        assertEquals(0, r1.getPriority());
+        assertEquals(14.699616432189941, r1.getLongitude());
+        assertEquals(49.40732955932617, r1.getLatitude());
+        assertEquals(456, r1.getAltitude());
+        assertEquals(271, r1.getAngle());
+        assertEquals(9, r1.getSatellites());
+        assertEquals(0, r1.getSpeed());
+        List<IOProperty> r1io = r1.getIoProperties();
+        assertEquals(5, r1io.size());
+        assertEquals(239, r1io.get(0).getId());
+        assertEquals(1, r1io.get(0).getValue());
+        assertEquals(67, r1io.get(1).getId());
+        assertEquals(9865, r1io.get(1).getValue());
+        assertEquals(66, r1io.get(2).getId());
+        assertEquals(13499, r1io.get(2).getValue());
+        assertEquals(24, r1io.get(3).getId());
+        assertEquals(0, r1io.get(3).getValue());
+        assertEquals(78, r1io.get(4).getId());
+        assertEquals(0, r1io.get(4).getValue());
+
+        // records r2 - r14
+
+        TelemetryAVLRecord r15 = records[14];
+        assertEquals(Instant.parse("2023-11-30T16:32:31.457Z"), r15.getTimestamp());
+        assertEquals(0, r15.getPriority());
+        assertEquals(14.699616432189941, r15.getLongitude());
+        assertEquals(49.40732955932617, r15.getLatitude());
+        assertEquals(453, r15.getAltitude());
+        assertEquals(271, r15.getAngle());
+        assertEquals(9, r15.getSatellites());
+        assertEquals(0, r15.getSpeed());
+        List<IOProperty> r15io = r15.getIoProperties();
+        assertEquals(5, r15io.size());
+        assertEquals(239, r15io.get(0).getId());
+        assertEquals(1, r15io.get(0).getValue());
+        assertEquals(67, r15io.get(1).getId());
+        assertEquals(9870, r15io.get(1).getValue());
+        assertEquals(66, r15io.get(2).getId());
+        assertEquals(13507, r15io.get(2).getValue());
+        assertEquals(24, r15io.get(3).getId());
+        assertEquals(0, r15io.get(3).getValue());
+        assertEquals(78, r15io.get(4).getId());
+        assertEquals(0, r15io.get(4).getValue());
+    }
+
+
+    @Test
+    void test1_IMEI() {
+        Buffer buffer = readResourceFile("Fm4exC8Ex_IMEI/test1.bin");
+        String imei = Fm4ex.parseIMEI(buffer);
+        assertEquals("867648041484898", imei);
+    }
+
+    @Test
+    void test2_IMEI() {
+        Buffer buffer = readResourceFile("Fm4exC8Ex_IMEI/test2.bin");
+        String imei = Fm4ex.parseIMEI(buffer);
+        assertEquals("867648041484898", imei);
+    }
+
+    @Test
+    void test3_IMEI() {
+        Buffer buffer = readResourceFile("Fm4exC8Ex_IMEI/test3.bin");
+        String imei = Fm4ex.parseIMEI(buffer);
+        assertEquals("867648041484898", imei);
+    }
+}

+ 13 - 1
src/test/java/cz/senslog/telemetry/protocol/Fm4exTest.java

@@ -6,6 +6,7 @@ import cz.senslog.telemetry.domain.TelemetryAVLRecord;
 import io.vertx.core.buffer.Buffer;
 import org.junit.jupiter.api.Test;
 
+import java.math.BigInteger;
 import java.util.function.Function;
 
 import static org.junit.jupiter.api.Assertions.*;
@@ -13,9 +14,20 @@ import static org.junit.jupiter.api.Assertions.*;
 class Fm4exTest {
 
     @Test
+    void nm_test() {
+
+        long val1 = 146996162; // 2294479810L;
+        long val2 = 494073290; // 2641556938L;
+
+        System.out.println(Fm4ex.coordToDecimalDegrees(val1));
+        System.out.println(Fm4ex.coordToDecimalDegrees(val2));
+
+    }
+
+    @Test
     void test_coordinates() {
 
-        Function<Integer, Float> fnc = Fm4exC8::coordToDecimalDegrees;
+        Function<Integer, Float> fnc = Fm4ex::coordToDecimalDegrees;
         Function<Integer, Integer> setNegative = val -> val | (1 << 1);
 
         // coordinates for Pilsen

BIN
src/test/resources/Fm4exC8Ex_AVLData/test1.bin


BIN
src/test/resources/Fm4exC8Ex_AVLData/test2.bin


BIN
src/test/resources/Fm4exC8Ex_AVLData/test3.bin


BIN
src/test/resources/Fm4exC8Ex_IMEI/test1.bin


BIN
src/test/resources/Fm4exC8Ex_IMEI/test2.bin


BIN
src/test/resources/Fm4exC8Ex_IMEI/test3.bin


Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff