Исправлен парсер АИС. Добавленно логгирование на сервер

This commit is contained in:
2025-09-08 16:43:53 +03:00
parent 2b0afe4d79
commit 8d63f9d719
6 changed files with 513 additions and 32 deletions
@@ -3,6 +3,7 @@ package com.grigowashere.aismap.controllers;
import android.util.Log;
import com.grigowashere.aismap.models.Vessel;
import com.grigowashere.aismap.models.AISVessel;
import com.grigowashere.aismap.utils.LogSender;
import java.util.List;
import java.util.ArrayList;
@@ -87,6 +88,9 @@ public class NMEAParser {
}
Log.d(TAG, "Парсим NMEA: " + cleanedSentence);
// Отправляем NMEA сообщение на внешний ресурс
LogSender.logNMEA(cleanedSentence);
try {
// Разбираем сообщение по запятым
String[] fields = cleanedSentence.split(",");
@@ -657,6 +661,7 @@ public class NMEAParser {
// Разбираем AIS сообщение по запятым
String[] fields = ais.split(",");
Log.d(TAG, "AIS поля (" + fields.length + "): " + java.util.Arrays.toString(fields));
if (fields.length < 7) {
Log.w(TAG, "AIS сообщение слишком короткое: " + ais);
return;
@@ -676,8 +681,20 @@ public class NMEAParser {
// Поле 5: payload (данные)
String payload = getField(fields, 5);
// Поле 6: количество бит заполнения
int fillBits = parseIntField(fields, 6, 0);
// Поле 6: количество бит заполнения (может содержать *checksum)
String fillBitsField = getField(fields, 6);
int fillBits = 0;
if (fillBitsField != null) {
// Если поле содержит *, берем только часть до *
if (fillBitsField.contains("*")) {
fillBitsField = fillBitsField.split("\\*")[0];
}
try {
fillBits = Integer.parseInt(fillBitsField);
} catch (NumberFormatException e) {
Log.w(TAG, "Не удалось распарсить fillBits из поля 6: '" + fillBitsField + "'");
}
}
// Контрольная сумма находится в последнем поле после *
String lastField = fields[fields.length - 1];
@@ -883,7 +900,18 @@ public class NMEAParser {
", payloadLength=" + payload.length() +
", binaryLength=" + fullBinary.length()
);
return fullBinary.substring(startBit, Math.min(startBit + length, fullBinary.length()));
// Если поле выходит за границы, возвращаем то что есть, дополняя нулями
if (startBit >= fullBinary.length()) {
// Если startBit уже за границами, возвращаем строку из нулей
return "0".repeat(length);
} else {
// Возвращаем доступную часть, дополняя нулями до нужной длины
String available = fullBinary.substring(startBit);
if (available.length() < length) {
available += "0".repeat(length - available.length());
}
return available;
}
}
}
@@ -962,6 +990,11 @@ public class NMEAParser {
vessel.setNavigationalStatus(getNavigationStatus(status));
vessel.setLastUpdate(java.time.LocalDateTime.now());
// Отправляем информацию о корабле на внешний ресурс
String vesselInfo = String.format("lat=%.6f, lon=%.6f, course=%.1f, speed=%.1f, status=%s",
latitude, longitude, course, speed, getNavigationStatus(status));
LogSender.logShipUpdate(String.valueOf(mmsi), vesselInfo);
// Уведомляем слушателя
if (listener != null) {
listener.onAISVesselUpdated(vessel);
@@ -978,6 +1011,7 @@ public class NMEAParser {
private void decodeStaticData(String payload) {
try {
Log.d(TAG, "Декодируем Static Data, payload: " + payload + " (длина: " + payload.length() + ")");
Log.d(TAG, "Общая длина в битах: " + (payload.length() * 6));
// MMSI (30 бит) - начинается с бита 8
String mmsiBits = decodeAISField(payload, 8, 30);
@@ -1040,13 +1074,103 @@ public class NMEAParser {
int eta = Integer.parseInt(etaBits, 2);
Log.d(TAG, "ETA bits: " + etaBits + " = " + eta);
// Destination (120 бит) - бит 314
String destBits = decodeAISField(payload, 314, 120);
String destination = decodeAISString(destBits);
Log.d(TAG, "Destination bits: " + destBits + " = '" + destination + "'");
// Парсим ETA согласно стандарту: MMDDHHMM UTC
// Bits 19-16: month; 1-12; 0 = not available = default
// Bits 15-11: day; 1-31; 0 = not available = default
// Bits 10-6: hour; 0-23; 24 = not available = default
// Bits 5-0: minute; 0-59; 60 = not available = default
java.time.LocalDateTime etaDateTime = parseETA(eta);
Log.d(TAG, "ETA parsed: " + etaDateTime);
Log.d(TAG, String.format("AIS Static: MMSI=%d, IMO=%d, name='%s', callSign='%s', type=%d, L=%.1f, W=%.1f, D=%.1f, ETA=%d, dest='%s'",
mmsi, imo, vesselName, callSign, vesselTypeCode, length, width, draft, eta, destination));
// Вычисляем доступную длину для оставшихся полей
int totalBits = payload.length() * 6;
int remainingBits = totalBits - 314; // Остается после ETA
Log.d(TAG, "Remaining bits after ETA: " + remainingBits + " (total: " + totalBits + ")");
String destination = "";
double maxDraught = 0.0;
String epfdDescription = "Unknown";
boolean dteReady = false;
// Для коротких сообщений (426 бит) используем упрощенную структуру
if (totalBits <= 426) {
// В коротких сообщениях может не быть всех полей
// Пробуем разные позиции для Destination
int[] possibleDestStarts = {302, 314, 320, 328};
for (int destStartBit : possibleDestStarts) {
if (destStartBit + 120 <= totalBits) {
String destBits = decodeAISField(payload, destStartBit, 120);
String testDest = decodeAISString(destBits);
Log.d(TAG, "Пробуем Destination с бита " + destStartBit + ": " + testDest);
if (testDest.contains("DEFAULT") || testDest.contains("FAULT")) {
destination = testDest;
Log.d(TAG, "Найден Destination с бита " + destStartBit + ": '" + destination + "'");
break;
}
}
}
// Если не нашли, используем стандартную позицию
if (destination.isEmpty() && remainingBits > 0) {
int destStartBit = 314;
int destLength = Math.min(remainingBits, 120);
String destBits = decodeAISField(payload, destStartBit, destLength);
destination = decodeAISString(destBits);
Log.d(TAG, "Destination bits (fallback): " + destBits + " = '" + destination + "' (length: " + destLength + ")");
}
} else {
// Для полных сообщений используем стандартную структуру
if (remainingBits >= 8) {
// Maximum present static draught (8 бит) - бит 314
String draughtBits = decodeAISField(payload, 314, 8);
int draughtValue = Integer.parseInt(draughtBits, 2);
maxDraught = (draughtValue == 0) ? 0.0 : (draughtValue == 255) ? 25.5 : draughtValue / 10.0;
Log.d(TAG, "Max Draught bits: " + draughtBits + " = " + maxDraught + "m");
remainingBits -= 8;
}
if (remainingBits >= 4) {
// Type of electronic position fixing device (4 бита)
int epfdStartBit = 314 + 8;
String epfdBits = decodeAISField(payload, epfdStartBit, 4);
int epfdType = Integer.parseInt(epfdBits, 2);
epfdDescription = getEPFDType(epfdType);
Log.d(TAG, "EPFD Type bits: " + epfdBits + " = " + epfdType + " (" + epfdDescription + ")");
remainingBits -= 4;
}
if (remainingBits >= 1) {
// DTE (1 бит)
int dteStartBit = 314 + 8 + 4;
String dteBits = decodeAISField(payload, dteStartBit, 1);
dteReady = Integer.parseInt(dteBits, 2) == 0;
Log.d(TAG, "DTE bits: " + dteBits + " = " + dteReady + " (ready: " + dteReady + ")");
remainingBits -= 1;
}
if (remainingBits >= 1) {
// Spare (1 бит)
int spareStartBit = 314 + 8 + 4 + 1;
String spareBits = decodeAISField(payload, spareStartBit, 1);
int spare = Integer.parseInt(spareBits, 2);
Log.d(TAG, "Spare bits: " + spareBits + " = " + spare);
remainingBits -= 1;
}
if (remainingBits > 0) {
// Destination (оставшиеся биты)
int destStartBit = 314 + 8 + 4 + 1 + 1;
int destLength = Math.min(remainingBits, 120); // Максимум 120 бит
String destBits = decodeAISField(payload, destStartBit, destLength);
destination = decodeAISString(destBits);
Log.d(TAG, "Destination bits (full): " + destBits + " = '" + destination + "' (length: " + destLength + ")");
} else {
Log.w(TAG, "Destination поле недоступно - недостаточно битов");
}
}
Log.d(TAG, String.format("AIS Static: MMSI=%d, IMO=%d, name='%s', callSign='%s', type=%d, L=%.1f, W=%.1f, D=%.1f, maxD=%.1f, ETA=%s, EPFD=%s, DTE=%s, dest='%s'",
mmsi, imo, vesselName, callSign, vesselTypeCode, length, width, draft, maxDraught, etaDateTime, epfdDescription, dteReady, destination));
// Обновляем AIS судно
AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi));
@@ -1058,8 +1182,14 @@ public class NMEAParser {
vessel.setWidth(width);
vessel.setDraft(draft);
vessel.setDestination(destination);
vessel.setEta(etaDateTime); // Добавляем ETA в модель
vessel.setLastUpdate(java.time.LocalDateTime.now());
// Отправляем информацию о корабле на внешний ресурс
String vesselInfo = String.format("name='%s', callSign='%s', type=%s, L=%.1f, W=%.1f, D=%.1f, dest='%s'",
vesselName, callSign, getVesselType(vesselTypeCode), length, width, draft, destination);
LogSender.logShipUpdate(String.valueOf(mmsi), vesselInfo);
// Уведомляем слушателя
if (listener != null) {
listener.onAISVesselUpdated(vessel);
@@ -1070,6 +1200,51 @@ public class NMEAParser {
}
}
/**
* Парсит ETA (Estimated Time of Arrival) из 20-битного значения
* Формат: MMDDHHMM UTC
* Bits 19-16: month; 1-12; 0 = not available = default
* Bits 15-11: day; 1-31; 0 = not available = default
* Bits 10-6: hour; 0-23; 24 = not available = default
* Bits 5-0: minute; 0-59; 60 = not available = default
*/
private java.time.LocalDateTime parseETA(int eta) {
if (eta == 0) {
return null; // Not available
}
Log.d(TAG, "ETA raw value: " + eta + " (binary: " + Integer.toBinaryString(eta) + ")");
// Извлекаем компоненты из 20-битного значения
// Правильный порядок битов: MMMM DDDDD HHHHH MMMMMM
int month = (eta >> 16) & 0x0F; // Bits 19-16 (4 бита)
int day = (eta >> 11) & 0x1F; // Bits 15-11 (5 бит)
int hour = (eta >> 6) & 0x1F; // Bits 10-6 (5 бит)
int minute = eta & 0x3F; // Bits 5-0 (6 бит)
Log.d(TAG, String.format("ETA components: month=%d, day=%d, hour=%d, minute=%d",
month, day, hour, minute));
// Проверяем на значения по умолчанию
if (month == 0 || month > 12) return null; // Not available
if (day == 0 || day > 31) return null; // Not available
if (hour == 24 || hour > 23) return null; // Not available
if (minute == 60 || minute > 59) return null; // Not available
try {
// Создаем LocalDateTime для текущего года
int currentYear = java.time.LocalDate.now().getYear();
java.time.LocalDateTime etaDateTime = java.time.LocalDateTime.of(
currentYear, month, day, hour, minute);
Log.d(TAG, "ETA parsed as LocalDateTime: " + etaDateTime);
return etaDateTime;
} catch (Exception e) {
Log.w(TAG, "Ошибка создания LocalDateTime для ETA: " + e.getMessage());
return null;
}
}
/**
* Парсит AIS координаты
*/
@@ -1103,38 +1278,46 @@ public class NMEAParser {
private String decodeAISString(String bits) {
StringBuilder result = new StringBuilder();
Log.d(TAG, "Декодируем AIS строку из битов: " + bits + " (длина: " + bits.length() + ")");
for (int i = 0; i < bits.length(); i += 6) {
if (i + 6 <= bits.length()) {
String charBits = bits.substring(i, i + 6);
int value = Integer.parseInt(charBits, 2);
if (value == 0) {
Log.d(TAG, "Найден конец строки (0)");
break; // Конец строки
}
char decodedChar;
if (value >= 1 && value <= 26) {
Log.d(TAG, "Обрабатываем значение: " + value + " (биты: " + charBits + ")");
// Приоритет специальных случаев (пробелы)
if (value == 32 || value == 63) {
decodedChar = ' '; // Пробел
Log.d(TAG, "Найден пробел (" + value + ")");
} else if (value >= 1 && value <= 26) {
// Заглавные буквы A-Z
decodedChar = (char)('A' + value - 1);
} else if (value >= 27 && value <= 52) {
decodedChar = (char)('a' + value - 27);
} else if (value >= 53 && value <= 62) {
decodedChar = (char)('0' + value - 53);
} else if (value == 63) {
decodedChar = ' ';
Log.d(TAG, "Диапазон A-Z: " + value + " -> " + decodedChar);
} else if (value >= 49 && value <= 58) {
// Цифры 1-9 (кастомное сопоставление на основе AIS1)
decodedChar = (char)('1' + (value - 49));
Log.d(TAG, "Диапазон 1-9: " + value + " -> " + decodedChar);
} else if (value == 59) {
// Цифра 0 (кастомное сопоставление)
decodedChar = '0';
Log.d(TAG, "Декодирован символ (59) -> '0'");
} else if (value == 0) {
decodedChar = '@'; // Специальный символ
// Нулевое значение - конец строки, но не останавливаемся сразу
decodedChar = ' '; // Заменяем на пробел для продолжения
Log.d(TAG, "Найден ноль, заменяем на пробел");
} else {
decodedChar = '?'; // Неизвестный символ
Log.w(TAG, "Неизвестное значение AIS символа: " + value);
// Неизвестный или зарезервированный символ
decodedChar = '?';
Log.w(TAG, "Неизвестное или зарезервированное значение AIS символа: " + value + " (биты: " + charBits + ")");
}
result.append(decodedChar);
Log.d(TAG, "Декодирован символ: " + charBits + " (" + value + ") -> '" + decodedChar + "'");
}
}
String resultStr = result.toString().trim();
Log.d(TAG, "Результат декодирования строки: '" + resultStr + "'");
return resultStr;
@@ -1165,6 +1348,31 @@ public class NMEAParser {
}
}
/**
* Получает описание типа электронного устройства позиционирования
*/
private String getEPFDType(int epfdType) {
switch (epfdType) {
case 0: return "Undefined";
case 1: return "GPS";
case 2: return "GLONASS";
case 3: return "Combined GPS/GLONASS";
case 4: return "Loran-C";
case 5: return "Chayka";
case 6: return "Integrated navigation system";
case 7: return "Surveyed";
case 8:
case 9:
case 10:
case 11:
case 12:
case 13:
case 14:
case 15: return "Not used";
default: return "Unknown";
}
}
/**
* Получает тип судна по коду
*/
@@ -1559,6 +1767,11 @@ public class NMEAParser {
vessel.setLastUpdate(java.time.LocalDateTime.now());
vessel.setVesselClass("Class B");
// Отправляем информацию о корабле на внешний ресурс
String vesselInfo = String.format("Class B: lat=%.6f, lon=%.6f, course=%.1f, speed=%.1f, heading=%.1f, accuracy=%s",
latitude, longitude, course, speed, heading, accuracy == 1 ? "high" : "low");
LogSender.logShipUpdate(String.valueOf(mmsi), vesselInfo);
// Уведомляем слушателя
if (listener != null) {
listener.onAISVesselUpdated(vessel);
@@ -1667,6 +1880,11 @@ public class NMEAParser {
vessel.setLastUpdate(java.time.LocalDateTime.now());
vessel.setVesselClass("Extended Class B");
// Отправляем информацию о корабле на внешний ресурс
String vesselInfo = String.format("Extended Class B: name='%s', lat=%.6f, lon=%.6f, course=%.1f, speed=%.1f, type=%s, L=%.1f, W=%.1f, D=%.1f",
vesselName, latitude, longitude, course, speed, getVesselType(vesselTypeCode), length, width, draft);
LogSender.logShipUpdate(String.valueOf(mmsi), vesselInfo);
// Уведомляем слушателя
if (listener != null) {
listener.onAISVesselUpdated(vessel);