Files
AndroidAisMap/app/src/main/java/com/grigowashere/aismap/controllers/NMEAParser.java
T
Grigo 41432665ea Подготовка к крупным изменениям: карта, AIS и UI
- Яндекс/MapForge: правки в менеджерах и обёртках маркеров (улучшена отрисовка/логика)
- NMEAParser: корректировки парсинга и стабильности
- Модель AISVessel: уточнение полей/логики
- Настройки: правки в SettingsActivity и SettingsManager, актуализация AppController
- UI: обновлены activity_main, activity_settings, bottom_sheet_ais_vessel; меню main_menu
- Ресурсы: добавлен drawable/targetclassa.xml, обновлён drawable/target.xml
- Конфигурация: правки AndroidManifest и app/build.gradle
- Прочее: изменения в .idea (не влияют на сборку)
2025-09-23 11:53:23 +03:00

2450 lines
119 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
/**
* Контроллер для парсинга NMEA сообщений
* Работает в гибридном режиме: координаты через Location API, остальное через NMEA
*
* ВАЖНО: Размеры судна в AIS сообщениях рассчитываются относительно положения антенны:
* - Длина = Dim.A + Dim.B (от носа до антенны + от антенны до кормы)
* - Ширина = Dim.C + Dim.D (от левого борта до антенны + от антенны до правого борта)
* Координаты в AIS указывают положение антенны, а не центра судна.
*/
/**
* Контроллер для парсинга NMEA сообщений
* Использует простой разбор по запятым вместо регулярных выражений
*/
public class NMEAParser {
private static final String TAG = "NMEAParser";
private Vessel ownVessel;
private List<AISVessel> aisVessels;
private NMEAParserListener listener;
private GPSLocationListener gpsLocationListener;
// Поля для работы с AIS фрагментами
private java.util.Map<String, java.util.Map<Integer, String>> aisFragments = new java.util.HashMap<>();
private java.util.Map<String, Long> aisFragmentTimestamps = new java.util.HashMap<>();
private static final long AIS_FRAGMENT_TIMEOUT = 10000; // 10 секунд
// Флаг для гибридного режима
private boolean hybridMode = true;
// Поля для отслеживания спутников по системам
private int gpsSatellites = 0;
private int glonassSatellites = 0;
private int galileoSatellites = 0;
public interface NMEAParserListener {
void onVesselUpdated(Vessel vessel);
void onAISVesselUpdated(AISVessel vessel);
void onParseError(String error);
void onDOPUpdated(double pdop, double hdop, double vdop);
}
public NMEAParser() {
this.ownVessel = new Vessel();
this.aisVessels = new ArrayList<>();
}
public void setListener(NMEAParserListener listener) {
this.listener = listener;
}
/**
* Устанавливает GPS Location Listener для гибридного режима
*/
public void setGPSLocationListener(GPSLocationListener gpsLocationListener) {
this.gpsLocationListener = gpsLocationListener;
}
/**
* Включает/выключает гибридный режим
*/
public void setHybridMode(boolean enabled) {
this.hybridMode = enabled;
Log.i(TAG, "🔄 Гибридный режим: " + (enabled ? "включен" : "отключен"));
Log.i(TAG, "📍 В режиме " + (enabled ? "гибридном" : "только NMEA") + " координаты будут " +
(enabled ? "браться из Android GPS API" : "браться из NMEA сообщений"));
}
/**
* Парсит NMEA сообщение
*/
public void parseNMEA(String nmeaSentence) {
if (nmeaSentence == null || nmeaSentence.trim().isEmpty()) {
return;
}
// Очищаем сообщение от лишних символов
String cleanedSentence = cleanNMEASentence(nmeaSentence);
if (cleanedSentence == null) {
Log.w(TAG, "NMEA сообщение не удалось очистить или слишком короткое: " + nmeaSentence);
return;
}
Log.d(TAG, "Парсим NMEA: " + cleanedSentence);
// Отправляем NMEA сообщение на внешний ресурс
LogSender.logNMEA(cleanedSentence);
try {
// Разбираем сообщение по запятым
String[] fields = cleanedSentence.split(",");
if (fields.length < 2) {
Log.w(TAG, "NMEA сообщение слишком короткое: " + cleanedSentence);
return;
}
// Извлекаем приамбуду (первые 6 символов после $)
String preamble = fields[0];
if (preamble.length() < 6) {
Log.w(TAG, "Некорректная приамбула: " + preamble);
return;
}
// Определяем тип сообщения по последним трем символам приамбуды
String messageType = preamble.substring(preamble.length() - 3);
switch (messageType) {
case "GGA":
parseGGA(fields);
break;
case "RMC":
parseRMC(fields);
break;
case "VTG":
parseVTG(fields);
break;
case "GLL":
parseGLL(fields);
break;
case "GSV":
parseGSV(fields);
break;
case "GNS":
parseGNS(fields);
break;
case "GSA":
parseGSA(fields);
break;
case "ZDA":
parseZDA(fields);
break;
default:
// Проверяем AIS сообщения
if (cleanedSentence.startsWith("!AIVDM")) {
parseAIS(cleanedSentence);
} else {
Log.d(TAG, "Неподдерживаемый тип NMEA сообщения: " + messageType);
}
break;
}
} catch (Exception e) {
Log.e(TAG, "Ошибка парсинга NMEA: " + e.getMessage(), e);
if (listener != null) {
listener.onParseError("Ошибка парсинга NMEA: " + e.getMessage());
}
}
}
/**
* Безопасно получает поле по индексу
*/
private String getField(String[] fields, int index) {
if (index < fields.length && !fields[index].trim().isEmpty()) {
return fields[index].trim();
}
return null;
}
/**
* Безопасно парсит double значение из поля
*/
private double parseDoubleField(String[] fields, int index, double defaultValue) {
String field = getField(fields, index);
if (field != null) {
try {
return Double.parseDouble(field);
} catch (NumberFormatException e) {
Log.w(TAG, "Не удалось распарсить double из поля " + index + ": '" + field + "'");
}
}
return defaultValue;
}
/**
* Безопасно парсит int значение из поля
*/
private int parseIntField(String[] fields, int index, int defaultValue) {
String field = getField(fields, index);
if (field != null) {
try {
return Integer.parseInt(field);
} catch (NumberFormatException e) {
Log.w(TAG, "Не удалось распарсить int из поля " + index + ": '" + field + "'");
}
}
return defaultValue;
}
/**
* Очищает NMEA сообщение от лишних символов
*/
private String cleanNMEASentence(String sentence) {
if (sentence == null || sentence.trim().isEmpty()) {
return null;
}
// Убираем пробелы в начале и конце
String cleaned = sentence.trim();
// Проверяем минимальную длину NMEA сообщения
if (cleaned.length() < 6) { // Минимум: $GPGGA*XX
Log.w(TAG, "Слишком короткое NMEA сообщение: '" + cleaned + "'");
return null;
}
// Исправляем двойной $ ($$GNGGA -> $GNGGA)
if (cleaned.startsWith("$$")) {
cleaned = cleaned.substring(1);
Log.d(TAG, "Исправлен двойной $: " + cleaned);
}
// Обрабатываем смешанные сообщения (например, VTG содержит GGA)
if (cleaned.contains("$G") && cleaned.indexOf("$G") > 0) {
// Находим первое полное NMEA сообщение
int firstDollar = cleaned.indexOf("$G");
if (firstDollar > 0) {
String firstMessage = cleaned.substring(firstDollar);
int asteriskIndex = firstMessage.indexOf('*');
if (asteriskIndex > 0) {
// Проверяем, что после * есть достаточно символов для контрольной суммы
if (asteriskIndex + 2 < firstMessage.length()) {
cleaned = firstMessage.substring(0, asteriskIndex + 3);
} else if (asteriskIndex + 1 < firstMessage.length()) {
cleaned = firstMessage.substring(0, asteriskIndex + 2);
} else {
cleaned = firstMessage.substring(0, asteriskIndex + 1);
}
Log.d(TAG, "Извлечено первое NMEA сообщение: " + cleaned);
}
}
}
// Убираем все символы после последнего *
int asteriskIndex = cleaned.lastIndexOf('*');
if (asteriskIndex >= 0) {
// Проверяем, что после * есть достаточно символов для контрольной суммы
if (asteriskIndex + 2 < cleaned.length()) {
cleaned = cleaned.substring(0, asteriskIndex + 3); // включаем * и 2 символа контрольной суммы
} else if (asteriskIndex + 1 < cleaned.length()) {
cleaned = cleaned.substring(0, asteriskIndex + 2); // включаем * и 1 символ контрольной суммы
} else {
cleaned = cleaned.substring(0, asteriskIndex + 1); // включаем только *
}
}
// Убираем все непечатаемые символы
cleaned = cleaned.replaceAll("[^\\x20-\\x7E]", "");
Log.d(TAG, "Очищено NMEA: '" + cleaned + "' (длина: " + cleaned.length() + ")");
return cleaned;
}
/**
* Парсит GGA сообщение (Global Positioning System Fix Data)
* В гибридном режиме используем только количество спутников и высоту
* Формат: $GPGGA,time,lat,N/S,lon,E/W,quality,numSV,HDOP,alt,M,sep,M,diffAge,diffStation*checksum
*/
private void parseGGA(String[] fields) {
Log.d(TAG, "Парсим GGA с " + fields.length + " полями");
// Поле 7: количество спутников
int satellites = parseIntField(fields, 7, 0);
// Поле 9: высота над эллипсоидом
double altitude = parseDoubleField(fields, 9, 0.0);
Log.d(TAG, String.format("GGA: sat=%d, alt=%.1f", satellites, altitude));
// В гибридном режиме не обновляем координаты
if (!hybridMode) {
// Поля 2,3: широта и направление
String latStr = getField(fields, 2);
String latDir = getField(fields, 3);
if (latStr != null && latDir != null) {
double latitude = parseCoordinate(latStr, latDir.equals("N"));
ownVessel.setLatitude(latitude);
}
// Поля 4,5: долгота и направление
String lonStr = getField(fields, 4);
String lonDir = getField(fields, 5);
if (lonStr != null && lonDir != null) {
double longitude = parseCoordinate(lonStr, lonDir.equals("E"));
ownVessel.setLongitude(longitude);
}
}
ownVessel.setSatellites(satellites);
ownVessel.setAltitude(altitude);
// Синхронизируем с GPSLocationListener для получения активных спутников
if (gpsLocationListener != null) {
gpsLocationListener.setSatellitesInVessel(ownVessel);
}
if (listener != null) {
listener.onVesselUpdated(ownVessel);
}
}
/**
* Парсит RMC сообщение (Recommended Minimum Navigation Information)
* В гибридном режиме используем только курс и скорость
* Формат: $GPRMC,time,status,lat,N/S,lon,E/W,speed,course,date,magVar,E/W,mode*checksum
*/
private void parseRMC(String[] fields) {
Log.d(TAG, "Парсим RMC с " + fields.length + " полями");
// Поле 2: статус валидности (A = валидный, V = невалидный)
String status = getField(fields, 2);
boolean isValid = status != null && status.startsWith("A");
Log.d(TAG, "RMC статус: " + status + " (валидный: " + isValid + ")");
// Поле 7: скорость в узлах
double speed = parseDoubleField(fields, 7, 0.0);
// Поле 8: курс в градусах
double course = parseDoubleField(fields, 8, 0.0);
Log.d(TAG, String.format("RMC: speed=%.1f, course=%.1f, valid=%s", speed, course, isValid));
// В гибридном режиме не обновляем координаты
if (!hybridMode && isValid) {
Log.d(TAG, "Режим НЕ гибридный - обрабатываем координаты из RMC");
// Поля 3,4: широта и направление
String latStr = getField(fields, 3);
String latDir = getField(fields, 4);
if (latStr != null && latDir != null) {
double latitude = parseCoordinate(latStr, latDir.equals("N"));
Log.d(TAG, "RMC широта: " + latStr + " " + latDir + " = " + latitude);
ownVessel.setLatitude(latitude);
}
// Поля 5,6: долгота и направление
String lonStr = getField(fields, 5);
String lonDir = getField(fields, 6);
if (lonStr != null && lonDir != null) {
double longitude = parseCoordinate(lonStr, lonDir.equals("E"));
Log.d(TAG, "RMC долгота: " + lonStr + " " + lonDir + " = " + longitude);
ownVessel.setLongitude(longitude);
}
} else if (hybridMode) {
Log.d(TAG, "Гибридный режим - координаты из RMC игнорируются");
} else {
Log.d(TAG, "RMC данные невалидны (статус V) - координаты не обновляем");
}
// Обновляем скорость и курс только если данные валидны
if (isValid) {
ownVessel.setSpeed(speed);
ownVessel.setCourse(course);
}
Log.d(TAG, "RMC обновлено судно: lat=" + ownVessel.getLatitude() +
", lon=" + ownVessel.getLongitude() +
", speed=" + speed +
", course=" + course);
if (listener != null) {
listener.onVesselUpdated(ownVessel);
}
}
/**
* Парсит VTG сообщение (Course Over Ground and Ground Speed)
* Формат: $GPVTG,course,T,course,M,speed,N,speed,K,mode*checksum
*/
private void parseVTG(String[] fields) {
Log.d(TAG, "Парсим VTG с " + fields.length + " полями");
// Поле 1: курс в градусах (True)
double course = parseDoubleField(fields, 1, 0.0);
// Поле 5: скорость в узлах
double speed = parseDoubleField(fields, 5, 0.0);
Log.d(TAG, String.format("VTG: course=%.1f, speed=%.1f", course, speed));
ownVessel.setCourse(course);
ownVessel.setSpeed(speed);
if (listener != null) {
listener.onVesselUpdated(ownVessel);
}
}
/**
* Парсит GLL сообщение (Geographic Position - Latitude/Longitude)
* В гибридном режиме игнорируем
* Формат: $GPGLL,lat,N/S,lon,E/W,time,status,mode*checksum
*/
private void parseGLL(String[] fields) {
if (hybridMode) {
Log.d(TAG, "GLL игнорируется в гибридном режиме");
return;
}
Log.d(TAG, "Парсим GLL с " + fields.length + " полями");
// Поля 1,2: широта и направление
String latStr = getField(fields, 1);
String latDir = getField(fields, 2);
if (latStr != null && latDir != null) {
double latitude = parseCoordinate(latStr, latDir.equals("N"));
ownVessel.setLatitude(latitude);
}
// Поля 3,4: долгота и направление
String lonStr = getField(fields, 3);
String lonDir = getField(fields, 4);
if (lonStr != null && lonDir != null) {
double longitude = parseCoordinate(lonStr, lonDir.equals("E"));
ownVessel.setLongitude(longitude);
}
Log.d(TAG, String.format("GLL: lat=%.6f, lon=%.6f", ownVessel.getLatitude(), ownVessel.getLongitude()));
if (listener != null) {
listener.onVesselUpdated(ownVessel);
}
}
/**
* Парсит GSV сообщение (GPS Satellites in View)
* Формат: $GPGSV,totalMsgs,msgNum,totalSats,satId1,elev1,azim1,snr1,satId2,elev2,azim2,snr2,...*checksum
*/
private void parseGSV(String[] fields) {
Log.d(TAG, "Парсим GSV с " + fields.length + " полями");
// Поля 1,2,3: общее количество сообщений, номер сообщения, общее количество спутников
int totalMessages = parseIntField(fields, 1, 1);
int messageNumber = parseIntField(fields, 2, 1);
int satellitesInView = parseIntField(fields, 3, 0);
// Определяем тип системы спутников по приамбуде
String systemType = "Unknown";
String preamble = fields[0];
if (preamble.startsWith("$GPGSV")) {
systemType = "GPS";
} else if (preamble.startsWith("$GLGSV")) {
systemType = "GLONASS";
} else if (preamble.startsWith("$GAGSV")) {
systemType = "Galileo";
} else if (preamble.startsWith("$GBGSV")) {
systemType = "BeiDou";
}
Log.d(TAG, String.format("GSV [%s]: %d/%d, спутников в поле зрения: %d",
systemType, messageNumber, totalMessages, satellitesInView));
// Парсим данные о спутниках (начиная с поля 4, каждые 4 поля = 1 спутник)
for (int i = 4; i < fields.length - 1; i += 4) { // -1 чтобы исключить контрольную сумму
if (i + 3 < fields.length) {
String satId = getField(fields, i);
String elevation = getField(fields, i + 1);
String azimuth = getField(fields, i + 2);
String snr = getField(fields, i + 3);
if (satId != null) {
Log.d(TAG, String.format("Спутник %s: elev=%s, azim=%s, SNR=%s",
satId, elevation, azimuth, snr));
}
}
}
// Обновляем количество спутников только для последнего сообщения в серии
if (messageNumber == totalMessages) {
// Обновляем количество спутников для соответствующей системы
switch (systemType) {
case "GPS":
gpsSatellites = satellitesInView;
break;
case "GLONASS":
glonassSatellites = satellitesInView;
break;
case "Galileo":
galileoSatellites = satellitesInView;
break;
case "BeiDou":
// Пока не добавляем отдельный счетчик для BeiDou, считаем как GPS
gpsSatellites = Math.max(gpsSatellites, satellitesInView);
break;
}
// Обновляем общее количество спутников
int totalSatellites = gpsSatellites + glonassSatellites + galileoSatellites;
ownVessel.setSatellites(totalSatellites);
// Синхронизируем с GPSLocationListener для получения активных спутников
if (gpsLocationListener != null) {
gpsLocationListener.setSatellitesInVessel(ownVessel);
}
Log.d(TAG, String.format("GSV [%s] завершен: %d спутников. Общий счет: GPS=%d, GLONASS=%d, Galileo=%d, Всего=%d",
systemType, satellitesInView, gpsSatellites, glonassSatellites, galileoSatellites, totalSatellites));
if (listener != null) {
listener.onVesselUpdated(ownVessel);
}
}
}
/**
* Парсит GNS сообщение (GNSS Fix Data)
* В гибридном режиме используем только количество спутников и высоту
* Формат: $GNGNS,time,lat,N/S,lon,E/W,mode,numSV,HDOP,alt,sep,diffAge,diffStation,navStatus*checksum
*/
private void parseGNS(String[] fields) {
Log.d(TAG, "Парсим GNS с " + fields.length + " полями");
// Поле 7: количество спутников
int satellites = parseIntField(fields, 7, 0);
// Поле 9: высота над эллипсоидом
double altitude = parseDoubleField(fields, 9, 0.0);
Log.d(TAG, String.format("GNS: sat=%d, alt=%.1f", satellites, altitude));
// В гибридном режиме не обновляем координаты
if (!hybridMode) {
// Поля 2,3: широта и направление
String latStr = getField(fields, 2);
String latDir = getField(fields, 3);
if (latStr != null && latDir != null) {
double latitude = parseCoordinate(latStr, latDir.equals("N"));
ownVessel.setLatitude(latitude);
}
// Поля 4,5: долгота и направление
String lonStr = getField(fields, 4);
String lonDir = getField(fields, 5);
if (lonStr != null && lonDir != null) {
double longitude = parseCoordinate(lonStr, lonDir.equals("E"));
ownVessel.setLongitude(longitude);
}
}
ownVessel.setSatellites(satellites);
ownVessel.setAltitude(altitude);
// Синхронизируем с GPSLocationListener для получения активных спутников
if (gpsLocationListener != null) {
gpsLocationListener.setSatellitesInVessel(ownVessel);
}
if (listener != null) {
listener.onVesselUpdated(ownVessel);
}
}
/**
* Парсит ZDA сообщение (Date and Time)
* Формат: $GPZDA,time,day,month,year,timezoneHours,timezoneMinutes*checksum
*/
private void parseZDA(String[] fields) {
Log.d(TAG, "Парсим ZDA с " + fields.length + " полями");
try {
// Поле 1: время (HHMMSS.SS)
String timeStr = getField(fields, 1);
// Поля 2,3,4: день, месяц, год
int day = parseIntField(fields, 2, 0);
int month = parseIntField(fields, 3, 0);
int year = parseIntField(fields, 4, 0);
// Поля 5,6: часовой пояс (часы и минуты)
int timezoneHours = parseIntField(fields, 5, 0);
int timezoneMinutes = parseIntField(fields, 6, 0);
Log.d(TAG, String.format("ZDA: %04d-%02d-%02d %s, TZ: %+03d:%02d",
year, month, day, timeStr, timezoneHours, timezoneMinutes));
// Обновляем время последнего обновления
ownVessel.setLastUpdate(java.time.LocalDateTime.now());
if (listener != null) {
listener.onVesselUpdated(ownVessel);
}
} catch (Exception e) {
Log.w(TAG, "Ошибка парсинга ZDA: " + e.getMessage());
}
}
/**
* Парсит GSA сообщение (GPS DOP and Active Satellites)
* КЛЮЧЕВОЕ сообщение для получения DOP и активных спутников
* Формат: $GPGSA,mode,fixType,sat1,sat2,...,sat12,PDOP,HDOP,VDOP*checksum
*/
private void parseGSA(String[] fields) {
Log.d(TAG, "Парсим GSA с " + fields.length + " полями");
// Подсчитываем активные спутники (поля 3-14 содержат ID спутников)
int activeSatellites = 0;
for (int i = 3; i <= 14 && i < fields.length; i++) {
String satId = getField(fields, i);
if (satId != null && !satId.equals("0")) {
activeSatellites++;
Log.d(TAG, "Активный спутник: " + satId);
}
}
// Получаем DOP значения - могут быть в разных позициях в зависимости от количества полей
double pdop = 0.0;
double hdop = 0.0;
double vdop = 0.0;
// DOP значения обычно в последних полях перед контрольной суммой
if (fields.length >= 17) {
// Полное GSA сообщение
pdop = parseDoubleField(fields, 15, 0.0); // PDOP
hdop = parseDoubleField(fields, 16, 0.0); // HDOP
vdop = parseDoubleField(fields, 17, 0.0); // VDOP
} else if (fields.length >= 6) {
// Обрезанное GSA сообщение - DOP в последних полях
int dopStartIndex = fields.length - 4; // -4 чтобы исключить контрольную сумму
if (dopStartIndex >= 3) {
pdop = parseDoubleField(fields, dopStartIndex, 0.0);
hdop = parseDoubleField(fields, dopStartIndex + 1, 0.0);
vdop = parseDoubleField(fields, dopStartIndex + 2, 0.0);
}
}
Log.d(TAG, String.format("GSA: активных спутников=%d, PDOP=%.2f, HDOP=%.2f, VDOP=%.2f",
activeSatellites, pdop, hdop, vdop));
// Обновляем информацию о спутниках
ownVessel.setActiveSatellites(activeSatellites);
ownVessel.setPdop(pdop);
ownVessel.setHdop(hdop);
ownVessel.setVdop(vdop);
// Отправляем DOP значения в GPS Location Listener
if (gpsLocationListener != null) {
gpsLocationListener.setDOPValues(pdop, hdop, vdop);
// Синхронизируем с GPSLocationListener для получения активных спутников
gpsLocationListener.setSatellitesInVessel(ownVessel);
}
// Уведомляем слушателя о DOP
if (listener != null) {
listener.onDOPUpdated(pdop, hdop, vdop);
listener.onVesselUpdated(ownVessel);
}
}
/**
* Парсит AIS сообщение (Automatic Identification System)
* Формат: !AIVDM,totalFragments,fragmentNumber,sequenceId,channel,payload,fillBits*checksum
*/
private void parseAIS(String ais) {
Log.d(TAG, "Парсим AIS: " + ais);
// Разбираем 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;
}
try {
// Поля 1,2: общее количество фрагментов, номер фрагмента
int totalFragments = parseIntField(fields, 1, 1);
int fragmentNumber = parseIntField(fields, 2, 1);
// Поле 3: ID последовательности
String sequenceId = getField(fields, 3);
// Поле 4: канал (A или B)
String channel = getField(fields, 4);
// Поле 5: payload (данные)
String payload = getField(fields, 5);
// Поле 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];
String checksum = null;
if (lastField != null && lastField.contains("*")) {
String[] parts = lastField.split("\\*");
if (parts.length > 1) {
checksum = parts[1];
}
}
Log.d(TAG, String.format("AIS: %d/%d, seq='%s', ch='%s', payload='%s', fillBits=%d, checksum='%s'",
fragmentNumber, totalFragments, sequenceId, channel, payload, fillBits, checksum));
// Проверяем контрольную сумму
if (!validateChecksum(ais)) {
Log.w(TAG, "AIS сообщение с неверной контрольной суммой: " + ais);
return;
}
// Проверяем, что payload не пустой
if (payload != null && !payload.trim().isEmpty()) {
if (totalFragments == 1) {
// Одноканальное сообщение - декодируем сразу
decodeAISPayload(payload, channel != null && channel.equals("A") ? 0 : 1);
} else {
// Многочастное сообщение - собираем фрагменты
// Используем номер фрагмента как sequenceId если поле пустое
String actualSequenceId = (sequenceId != null && !sequenceId.trim().isEmpty()) ?
sequenceId : String.valueOf(fragmentNumber);
collectAISFragments(actualSequenceId, fragmentNumber, totalFragments, payload, channel != null && channel.equals("A") ? 0 : 1);
}
} else {
Log.w(TAG, "AIS payload пустой, пропускаем сообщение");
}
} catch (Exception e) {
Log.e(TAG, "Ошибка парсинга AIS сообщения: " + e.getMessage() + " для сообщения: " + ais);
if (listener != null) {
listener.onParseError("Ошибка парсинга AIS: " + e.getMessage());
}
}
}
/**
* Декодирует AIS payload
*/
private void decodeAISPayload(String payload, int channel) {
try {
// Определяем тип AIS сообщения по первым 6 битам
String messageTypeBits = decodeAISField(payload, 0, 6);
int messageType = Integer.parseInt(messageTypeBits, 2);
Log.d(TAG, "Декодируем AIS тип " + messageType + " на канале " + channel + " (биты: " + messageTypeBits + ")");
switch (messageType) {
case 1:
case 2:
case 3:
// Position Report
Log.d(TAG, "Обрабатываем Position Report (тип " + messageType + ")");
decodePositionReport(payload, messageType);
break;
case 5:
// Static Data
Log.d(TAG, "Обрабатываем Static Data (тип " + messageType + ")");
decodeStaticData(payload);
break;
case 4: // Base Station Report
Log.d(TAG, "Обрабатываем Base Station Report (тип " + messageType + ")");
decodeBaseStationReport(payload);
break;
case 14: // Safety Related Broadcast Message
Log.d(TAG, "Обрабатываем Safety Broadcast (тип " + messageType + ")");
decodeSafetyBroadcast(payload);
break;
case 18: // Standard Class B Equipment Position Report
Log.d(TAG, "Обрабатываем Class B Position Report (тип " + messageType + ")");
decodeClassBPositionReport(payload);
break;
case 19: // Extended Class B Equipment Position Report
Log.d(TAG, "Обрабатываем Extended Class B Position Report (тип " + messageType + ")");
decodeExtendedClassBPositionReport(payload);
break;
case 21: // Aid-to-Navigation Report
Log.d(TAG, "Обрабатываем Aid-to-Navigation Report (тип " + messageType + ")");
decodeAidToNavigationReport(payload);
break;
case 24: // Static Data Report
Log.d(TAG, "Обрабатываем Static Data Report (тип " + messageType + ")");
decodeStaticDataReport(payload);
break;
default:
Log.d(TAG, "Неподдерживаемый тип AIS сообщения: " + messageType);
break;
}
} catch (Exception e) {
Log.e(TAG, "Ошибка декодирования AIS payload: " + e.getMessage(), e);
}
}
/**
* Собирает фрагменты многочастного AIS сообщения
*/
private void collectAISFragments(String sequenceId, int fragmentNumber, int totalFragments,
String payload, int channel) {
String key = sequenceId + "_" + channel;
Log.d(TAG, String.format("Собираем AIS фраг мент: %d/%d для %s",
fragmentNumber, totalFragments, key));
// Очищаем старые фрагменты
cleanupOldFragments();
// Получаем или создаем карту фрагментов для этой последовательности
java.util.Map<Integer, String> fragments = aisFragments.get(key);
if (fragments == null) {
fragments = new java.util.HashMap<>();
aisFragments.put(key, fragments);
aisFragmentTimestamps.put(key, System.currentTimeMillis());
Log.d(TAG, "Создан новый набор фрагментов для: " + key);
}
// Добавляем фрагмент
fragments.put(fragmentNumber, payload);
Log.d(TAG, String.format("Добавлен фрагмент %d/%d для %s",
fragmentNumber, totalFragments, key));
// Проверяем, все ли фрагменты получены
if (fragments.size() == totalFragments) {
Log.d(TAG, "Все фрагменты получены для " + key + ", собираем сообщение");
// Собираем полное сообщение
StringBuilder fullPayload = new StringBuilder();
for (int i = 1; i <= totalFragments; i++) {
String fragment = fragments.get(i);
if (fragment != null) {
fullPayload.append(fragment);
} else {
Log.w(TAG, "Отсутствует фрагмент " + i + " для " + key);
return;
}
}
String completePayload = fullPayload.toString();
Log.d(TAG, "Собрано полное AIS сообщение длиной " + completePayload.length() + " символов");
// Декодируем полное сообщение
decodeAISPayload(completePayload, channel);
// Удаляем собранные фрагменты
aisFragments.remove(key);
aisFragmentTimestamps.remove(key);
Log.d(TAG, "Фрагменты удалены для " + key);
} else {
Log.d(TAG, String.format("Ожидаем еще %d фрагментов для %s",
totalFragments - fragments.size(), key));
}
}
/**
* Очищает старые AIS фрагменты
*/
private void cleanupOldFragments() {
long currentTime = System.currentTimeMillis();
java.util.Iterator<java.util.Map.Entry<String, Long>> iterator = aisFragmentTimestamps.entrySet().iterator();
while (iterator.hasNext()) {
java.util.Map.Entry<String, Long> entry = iterator.next();
if (currentTime - entry.getValue() > AIS_FRAGMENT_TIMEOUT) {
String key = entry.getKey();
aisFragments.remove(key);
iterator.remove();
Log.d(TAG, "Удален устаревший AIS фрагмент: " + key);
}
}
}
/**
* Декодирует AIS поле из битовой строки
*/
private String decodeAISField(String payload, int startBit, int length) {
StringBuilder result = new StringBuilder();
// Преобразуем каждый символ payload в 6-битное значение
for (int i = 0; i < payload.length(); i++) {
int ascii = payload.charAt(i);
int value;
if (ascii >= 48 && ascii <= 87) {
value = ascii - 48; // '0'..'W'
} else if (ascii >= 88 && ascii <= 119) {
value = ascii - 56; // 'X'..'w'
} else {
throw new IllegalArgumentException("Недопустимый символ AIS payload: " + (char)ascii);
}
// Дополняем до 6 бит слева нулями и добавляем в общую строку
String binary = String.format("%6s", Integer.toBinaryString(value)).replace(' ', '0');
result.append(binary);
}
String fullBinary = result.toString();
// Вырезаем нужный диапазон битов
if (startBit + length <= fullBinary.length()) {
String fieldResult = fullBinary.substring(startBit, startBit + length);
// Дополнительное логирование для первых 6 бит (тип сообщения)
if (startBit == 0 && length == 6) {
Log.d(TAG, "AIS Message Type bits: " + fieldResult + " (payload: " + payload + ")");
}
return fieldResult;
} else {
Log.w(TAG,
"AIS поле выходит за границы: startBit=" + startBit +
", length=" + length +
", payloadLength=" + payload.length() +
", binaryLength=" + 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;
}
}
}
/**
* Декодирует AIS сообщение типа 1, 2, 3 (Position Report)
*/
private void decodePositionReport(String payload, int messageType) {
try {
Log.d(TAG, "Декодируем Position Report тип " + messageType + ", payload: " + payload + " (длина: " + payload.length() + ")");
// MMSI (30 бит) - начинается с бита 8
String mmsiBits = decodeAISField(payload, 8, 30);
int mmsi = Integer.parseInt(mmsiBits, 2);
Log.d(TAG, "MMSI bits: " + mmsiBits + " = " + mmsi);
// Navigation Status (4 бита) - бит 38
String statusBits = decodeAISField(payload, 38, 4);
int status = Integer.parseInt(statusBits, 2);
Log.d(TAG, "Status bits: " + statusBits + " = " + status);
// Rate of Turn (8 бит) - бит 42
String rotBits = decodeAISField(payload, 42, 8);
int rotRaw = Integer.parseInt(rotBits, 2);
if (rotRaw > 127) {
rotRaw -= 256;
}
double rateOfTurn = parseRateOfTurn(rotRaw);
Log.d(TAG, "Rate of Turn bits: " + rotBits + " = " + rotRaw + " -> " + rateOfTurn + " °/min");
// Дополнительная отладка - показываем все биты payload
String fullBinary = payloadToBinary(payload);
Log.d(TAG, "Full payload binary: " + fullBinary);
Log.d(TAG, "ROT bits 42-49: " + fullBinary.substring(42, Math.min(50, fullBinary.length())));
// Ищем ROT в разных позициях для отладки
// for (int pos = 0; pos < Math.min(fullBinary.length() - 8, 100); pos++) {
// String testBits = fullBinary.substring(pos, pos + 8);
// int testValue = Integer.parseInt(testBits, 2);
// double testRot = parseRateOfTurn(testValue);
// Log.d(TAG, String.format("Position %d: bits=%s, value=%d, rot=%.1f",
// pos, testBits, testValue, testRot));
// }
// Speed Over Ground (10 бит) - бит 50
String speedBits = decodeAISField(payload, 50, 10);
double speed = Integer.parseInt(speedBits, 2) / 10.0;
Log.d(TAG, "Speed bits: " + speedBits + " = " + speed);
// Position Accuracy (1 бит) - бит 60
String accuracyBits = decodeAISField(payload, 60, 1);
int accuracy = Integer.parseInt(accuracyBits, 2);
Log.d(TAG, "Accuracy bits: " + accuracyBits + " = " + accuracy);
// Longitude (28 бит) - бит 61
String lonBits = decodeAISField(payload, 61, 28);
double longitude = parseAISCoordinate(lonBits, 28);
Log.d(TAG, "Longitude bits: " + lonBits + " (длина: " + lonBits.length() + ") = " + longitude);
// Latitude (27 бит) - бит 89
String latBits = decodeAISField(payload, 89, 27);
double latitude = parseAISCoordinate(latBits, 27);
Log.d(TAG, "Latitude bits: " + latBits + " (длина: " + latBits.length() + ") = " + latitude);
// Course Over Ground (12 бит) - бит 116
String courseBits = decodeAISField(payload, 116, 12);
double course = Integer.parseInt(courseBits, 2) / 10.0;
Log.d(TAG, "Course bits: " + courseBits + " = " + course);
// True Heading (9 бит) - бит 128
String headingBits = decodeAISField(payload, 128, 9);
double heading = Integer.parseInt(headingBits, 2);
Log.d(TAG, "Heading bits: " + headingBits + " = " + heading);
// Time Stamp (6 бит) - бит 137
String timestampBits = decodeAISField(payload, 137, 6);
int timestamp = Integer.parseInt(timestampBits, 2);
Log.d(TAG, "Timestamp bits: " + timestampBits + " = " + timestamp);
// Проверяем, что координаты в разумных пределах
if (latitude < -90 || latitude > 90) {
Log.w(TAG, "Широта вне допустимых пределов: " + latitude);
}
if (longitude < -180 || longitude > 180) {
Log.w(TAG, "Долгота вне допустимых пределов: " + longitude);
}
Log.d(TAG, String.format("AIS Position: MMSI=%d, lat=%.6f, lon=%.6f, course=%.1f, speed=%.1f, status=%d, heading=%.1f, rot=%.1f",
mmsi, latitude, longitude, course, speed, status, heading, rateOfTurn));
// Создаем или обновляем AIS судно
AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi));
vessel.updatePosition(latitude, longitude, course, speed, rateOfTurn);
vessel.setPositionAccuracy(accuracy == 1);
vessel.setHeading(heading);
vessel.setNavigationalStatus(getNavigationStatus(status));
vessel.setLastUpdate(java.time.LocalDateTime.now());
// Помечаем класс судна как Class A, чтобы предотвратить дальнейшее перезаписывание Class B сообщениями
vessel.setVesselClass("Class A");
// Отправляем информацию о корабле на внешний ресурс (помечаем как Class A и добавляем статические поля, если известны)
StringBuilder infoA = new StringBuilder(
String.format(java.util.Locale.US,
"Class A: lat=%.6f, lon=%.6f, course=%.1f, speed=%.1f, rot=%.1f, status=%s",
latitude, longitude, course, speed, rateOfTurn, getNavigationStatus(status))
);
if (vessel.getVesselName() != null && !vessel.getVesselName().trim().isEmpty()) {
infoA.append(String.format(java.util.Locale.US, ", name='%s'", vessel.getVesselName()));
}
if (vessel.getCallSign() != null && !vessel.getCallSign().trim().isEmpty()) {
infoA.append(String.format(java.util.Locale.US, ", callSign='%s'", vessel.getCallSign()));
}
if (vessel.getVesselType() != null && !vessel.getVesselType().trim().isEmpty()) {
infoA.append(String.format(java.util.Locale.US, ", type=%s", vessel.getVesselType()));
}
if (vessel.getLength() > 0 || vessel.getWidth() > 0) {
infoA.append(String.format(java.util.Locale.US, ", L=%.1f, W=%.1f", vessel.getLength(), vessel.getWidth()));
}
if (vessel.getDraft() > 0) {
infoA.append(String.format(java.util.Locale.US, ", D=%.1f", vessel.getDraft()));
}
if (vessel.getDestination() != null && !vessel.getDestination().trim().isEmpty()) {
infoA.append(String.format(java.util.Locale.US, ", dest='%s'", vessel.getDestination()));
}
LogSender.logShipUpdate(String.valueOf(mmsi), infoA.toString());
// Уведомляем слушателя
if (listener != null) {
listener.onAISVesselUpdated(vessel);
}
} catch (Exception e) {
Log.e(TAG, "Ошибка декодирования Position Report: " + e.getMessage(), e);
}
}
/**
* Декодирует AIS сообщение типа 5 (Static Data)
*/
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);
int mmsi = Integer.parseInt(mmsiBits, 2);
Log.d(TAG, "MMSI bits: " + mmsiBits + " = " + mmsi);
// AIS Version (2 бита) - бит 38
String aisVersionBits = decodeAISField(payload, 38, 2);
int aisVersion = Integer.parseInt(aisVersionBits, 2);
Log.d(TAG, "AIS Version bits: " + aisVersionBits + " = " + aisVersion);
// IMO Number (30 бит) - бит 40
String imoBits = decodeAISField(payload, 40, 30);
int imo = Integer.parseInt(imoBits, 2);
Log.d(TAG, "IMO bits: " + imoBits + " = " + imo);
// Call Sign (42 бита) - бит 70
String callSignBits = decodeAISField(payload, 70, 42);
String callSign = decodeAISString(callSignBits);
Log.d(TAG, "Call Sign bits: " + callSignBits + " = '" + callSign + "'");
// Vessel Name (120 бит) - бит 112
String nameBits = decodeAISField(payload, 112, 120);
String vesselName = decodeAISString(nameBits);
Log.d(TAG, "Name bits: " + nameBits + " = '" + vesselName + "'");
// Ship Type (8 бит) - бит 232
String typeBits = decodeAISField(payload, 232, 8);
int vesselTypeCode = Integer.parseInt(typeBits, 2);
Log.d(TAG, "Type bits: " + typeBits + " = " + vesselTypeCode);
// Dimension Reference (9, 9, 6, 6 бит) - бит 240
String dimRefABits = decodeAISField(payload, 240, 9);
String dimRefBBits = decodeAISField(payload, 249, 9);
String dimRefCBits = decodeAISField(payload, 258, 6);
String dimRefDBits = decodeAISField(payload, 264, 6);
int dimRefA = Integer.parseInt(dimRefABits, 2);
int dimRefB = Integer.parseInt(dimRefBBits, 2);
int dimRefC = Integer.parseInt(dimRefCBits, 2);
int dimRefD = Integer.parseInt(dimRefDBits, 2);
Log.d(TAG, "Dimension Reference: A=" + dimRefA + ", B=" + dimRefB + ", C=" + dimRefC + ", D=" + dimRefD);
// Для сообщения типа 5 используем Dimension Reference поля (9, 9, 6, 6 бит)
// Размеры судна рассчитываются как:
// Длина = Dim.A + Dim.B (от носа до антенны + от антенны до кормы)
// Ширина = Dim.C + Dim.D (от левого борта до антенны + от антенны до правого борта)
double length = dimRefA + dimRefB;
double width = dimRefC + dimRefD;
// Draft (8 бит) - осадка - бит 294
String draftBits = decodeAISField(payload, 294, 8);
double draft = Integer.parseInt(draftBits, 2) / 10.0;
Log.d(TAG, "Static Data - используем Dimension Reference поля (9, 9, 6, 6 бит):");
Log.d(TAG, " Dim.A (нос-антенна): " + dimRefABits + " = " + dimRefA + " м");
Log.d(TAG, " Dim.B (антенна-корма): " + dimRefBBits + " = " + dimRefB + " м");
Log.d(TAG, " Dim.C (левый борт-антенна): " + dimRefCBits + " = " + dimRefC + " м");
Log.d(TAG, " Dim.D (антенна-правый борт): " + dimRefDBits + " = " + dimRefD + " м");
Log.d(TAG, " Total Length (A+B): " + length + " м");
Log.d(TAG, " Total Width (C+D): " + width + " м");
Log.d(TAG, " Draft: " + draftBits + " = " + draft + " м");
// ETA (20 бит) - бит 274
String etaBits = decodeAISField(payload, 274, 20);
int eta = Integer.parseInt(etaBits, 2);
Log.d(TAG, "ETA bits: " + etaBits + " = " + eta);
// Парсим 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);
// Вычисляем доступную длину для оставшихся полей
int totalBits = payload.length() * 6;
int remainingBits = totalBits - 294; // Остается после ETA
Log.d(TAG, "Remaining bits after ETA: " + remainingBits + " (total: " + totalBits + ")");
String destination = "";
double maxDraught = 0.0;
String epfdDescription = "Unknown";
boolean dteReady = false;
// Destination (120 бит) - бит 302
if (totalBits >= 302 + 120) {
String destBits = decodeAISField(payload, 302, 120);
destination = decodeAISString(destBits);
Log.d(TAG, "Destination bits: " + destBits + " = '" + destination + "'");
} else if (remainingBits > 0) {
// Если сообщение короткое, читаем доступные биты
int destStartBit = 302;
int destLength = Math.min(remainingBits, 120);
String destBits = decodeAISField(payload, destStartBit, destLength);
destination = decodeAISString(destBits);
Log.d(TAG, "Destination bits (short): " + destBits + " = '" + destination + "' (length: " + destLength + ")");
}
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));
vessel.setVesselName(vesselName);
vessel.setCallSign(callSign);
vessel.setImo(imo);
vessel.setVesselType(getVesselType(vesselTypeCode));
vessel.setLength(length);
vessel.setWidth(width);
vessel.setDraft(draft);
vessel.setDestination(destination);
vessel.setEta(etaDateTime); // Добавляем ETA в модель
vessel.setLastUpdate(java.time.LocalDateTime.now());
// Отправляем информацию о корабле на внешний ресурс (помечаем как Class A Static)
String vesselInfo = String.format(java.util.Locale.US,
"Class A Static: 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);
}
} catch (Exception e) {
Log.e(TAG, "Ошибка декодирования Static Data: " + e.getMessage(), e);
}
}
/**
* Парсит 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 payload в полную битовую строку для отладки
*/
private String payloadToBinary(String payload) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < payload.length(); i++) {
int ascii = payload.charAt(i);
int value;
if (ascii >= 48 && ascii <= 87) {
value = ascii - 48;
} else if (ascii >= 88 && ascii <= 119) {
value = ascii - 56;
} else {
value = 0;
}
String binary = String.format("%6s", Integer.toBinaryString(value)).replace(' ', '0');
result.append(binary);
}
return result.toString();
}
/**
* Парсит Rate of Turn согласно стандарту AIS
* ROTAIS = 4.733 SQRT(ROTINDICATED) degrees/min
* Значения: 0-126 = поворот вправо, 127 = поворот влево >5°/30с, 128-255 = поворот влево
*/
private double parseRateOfTurn(int rotRaw) {
if (rotRaw == -128) {
return Double.NaN; // Нет данных
}
if (rotRaw == -127) {
return -720.0; // Влево > 708°/мин
}
if (rotRaw == 127) {
return 720.0; // Вправо > 708°/мин
}
// В диапазоне -126..126
double rot = rotRaw / 4.733;
return Math.signum(rotRaw) * rot * rot;
}
/**
* Парсит AIS координаты
*/
private double parseAISCoordinate(String bits, int bitLength) {
// Проверяем знаковый бит
boolean isNegative = bits.charAt(0) == '1';
// Преобразуем в беззнаковое число
long value = Long.parseLong(bits, 2);
if (bitLength == 27) {
// Широта: 27 бит, диапазон -90 до +90
if (isNegative) {
// Для отрицательных чисел применяем дополнение до двух
value = value - (1L << 27);
}
return value / 600000.0;
} else {
// Долгота: 28 бит, диапазон -180 до +180
if (isNegative) {
// Для отрицательных чисел применяем дополнение до двух
value = value - (1L << 28);
}
return value / 600000.0;
}
}
/**
* Декодирует AIS строку согласно стандарту ITU-R M.1371-5, таблица 44
* Простой switch case для всех 64 возможных значений 6-битной кодировки
*/
private String decodeAISString(String bits) {
StringBuilder result = new StringBuilder();
Log.d(TAG, "Декодируем AIS строку, биты: " + bits + " (длина: " + bits.length() + ")");
for (int i = 0; i + 6 <= bits.length(); i += 6) {
String charBits = bits.substring(i, i + 6);
int value = Integer.parseInt(charBits, 2);
char decodedChar;
// Простой switch case для всех 64 возможных значений
switch (value) {
case 0: decodedChar = ' '; break;
case 1: decodedChar = 'A'; break;
case 2: decodedChar = 'B'; break;
case 3: decodedChar = 'C'; break;
case 4: decodedChar = 'D'; break;
case 5: decodedChar = 'E'; break;
case 6: decodedChar = 'F'; break;
case 7: decodedChar = 'G'; break;
case 8: decodedChar = 'H'; break;
case 9: decodedChar = 'I'; break;
case 10: decodedChar = 'J'; break;
case 11: decodedChar = 'K'; break;
case 12: decodedChar = 'L'; break;
case 13: decodedChar = 'M'; break;
case 14: decodedChar = 'N'; break;
case 15: decodedChar = 'O'; break;
case 16: decodedChar = 'P'; break;
case 17: decodedChar = 'Q'; break;
case 18: decodedChar = 'R'; break;
case 19: decodedChar = 'S'; break;
case 20: decodedChar = 'T'; break;
case 21: decodedChar = 'U'; break;
case 22: decodedChar = 'V'; break;
case 23: decodedChar = 'W'; break;
case 24: decodedChar = 'X'; break;
case 25: decodedChar = 'Y'; break;
case 26: decodedChar = 'Z'; break;
case 27: decodedChar = '0'; break;
case 28: decodedChar = '1'; break;
case 29: decodedChar = '2'; break;
case 30: decodedChar = '3'; break;
case 31: decodedChar = '4'; break;
case 32: decodedChar = ' '; break; // пробел
case 33: decodedChar = '5'; break;
case 34: decodedChar = '6'; break;
case 35: decodedChar = '7'; break;
case 36: decodedChar = '8'; break;
case 37: decodedChar = '9'; break;
case 38: decodedChar = ' '; break; // пробел
case 39: decodedChar = ' '; break; // пробел
case 40: decodedChar = ' '; break; // пробел
case 41: decodedChar = ' '; break; // пробел
case 42: decodedChar = ' '; break; // пробел
case 43: decodedChar = ' '; break; // пробел
case 44: decodedChar = ' '; break; // пробел
case 45: decodedChar = ' '; break; // пробел
case 46: decodedChar = ' '; break; // пробел
case 47: decodedChar = ' '; break; // пробел
case 48: decodedChar = '0'; break; // пробел
case 49: decodedChar = '1'; break; // пробел
case 50: decodedChar = '2'; break; // пробел
case 51: decodedChar = '3'; break; // пробел
case 52: decodedChar = '4'; break; // пробел
case 53: decodedChar = '5'; break; // пробел
case 54: decodedChar = '6'; break; // пробел
case 55: decodedChar = '7'; break; // пробел
case 56: decodedChar = '8'; break; // пробел
case 57: decodedChar = '9'; break; // пробел
case 58: decodedChar = ' '; break; // пробел
case 59: decodedChar = ' '; break; // пробел
case 60: decodedChar = ' '; break; // пробел
case 61: decodedChar = ' '; break; // пробел
case 62: decodedChar = ' '; break; // пробел
case 63: decodedChar = ' '; break; // пробел
default: decodedChar = ' '; break; // на всякий случай
}
Log.d(TAG, "Символ " + (i/6 + 1) + ": биты=" + charBits + ", значение=" + value + ", символ='" + decodedChar + "'");
result.append(decodedChar);
}
String resultStr = result.toString().trim();
Log.d(TAG, "Результат декодирования: '" + resultStr + "'");
return resultStr;
}
/**
* Получает навигационный статус по коду
*/
private String getNavigationStatus(int status) {
switch (status) {
case 0: return "Under way using engine";
case 1: return "At anchor";
case 2: return "Not under command";
case 3: return "Restricted manoeuvrability";
case 4: return "Constrained by her draught";
case 5: return "Moored";
case 6: return "Aground";
case 7: return "Engaged in fishing";
case 8: return "Under way sailing";
case 9: return "Reserved";
case 10: return "Reserved";
case 11: return "Reserved";
case 12: return "Reserved";
case 13: return "Reserved";
case 14: return "AIS-SART";
case 15: return "Not defined";
default: return "Unknown";
}
}
/**
* Получает описание типа электронного устройства позиционирования
*/
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";
}
}
/**
* Получает тип судна по коду согласно стандарту AIS
*/
private String getVesselType(int typeCode) {
switch (typeCode) {
case 0: return "Not available";
case 1:
case 2:
case 3:
case 4:
case 5:
case 6:
case 7:
case 8:
case 9:
case 10:
case 11:
case 12:
case 13:
case 14:
case 15:
case 16:
case 17:
case 18:
case 19: return "Reserved for future use";
case 20: return "Wing in ground (WIG), all ships";
case 21: return "Wing in ground (WIG), Hazardous category A";
case 22: return "Wing in ground (WIG), Hazardous category B";
case 23: return "Wing in ground (WIG), Hazardous category C";
case 24: return "Wing in ground (WIG), Hazardous category D";
case 25:
case 26:
case 27:
case 28:
case 29: return "Wing in ground (WIG), Reserved";
case 30: return "Fishing";
case 31: return "Towing";
case 32: return "Towing: length exceeds 200m or breadth exceeds 25m";
case 33: return "Dredging or underwater ops";
case 34: return "Diving ops";
case 35: return "Military ops";
case 36: return "Sailing";
case 37: return "Pleasure Craft";
case 38:
case 39: return "Reserved";
case 40: return "High speed craft (HSC), all ships";
case 41: return "High speed craft (HSC), Hazardous category A";
case 42: return "High speed craft (HSC), Hazardous category B";
case 43: return "High speed craft (HSC), Hazardous category C";
case 44: return "High speed craft (HSC), Hazardous category D";
case 45:
case 46:
case 47:
case 48: return "High speed craft (HSC), Reserved";
case 49: return "High speed craft (HSC), No additional information";
case 50: return "Pilot Vessel";
case 51: return "Search and Rescue vessel";
case 52: return "Tug";
case 53: return "Port Tender";
case 54: return "Anti-pollution equipment";
case 55: return "Law Enforcement";
case 56:
case 57: return "Spare - Local Vessel";
case 58: return "Medical Transport";
case 59: return "Noncombatant ship according to RR Resolution No. 18";
case 60: return "Passenger, all ships";
case 61: return "Passenger, Hazardous category A";
case 62: return "Passenger, Hazardous category B";
case 63: return "Passenger, Hazardous category C";
case 64: return "Passenger, Hazardous category D";
case 65:
case 66:
case 67:
case 68: return "Passenger, Reserved";
case 69: return "Passenger, No additional information";
case 70: return "Cargo, all ships";
case 71: return "Cargo, Hazardous category A";
case 72: return "Cargo, Hazardous category B";
case 73: return "Cargo, Hazardous category C";
case 74: return "Cargo, Hazardous category D";
case 75:
case 76:
case 77:
case 78: return "Cargo, Reserved";
case 79: return "Cargo, No additional information";
case 80: return "Tanker, all ships";
case 81: return "Tanker, Hazardous category A";
case 82: return "Tanker, Hazardous category B";
case 83: return "Tanker, Hazardous category C";
case 84: return "Tanker, Hazardous category D";
case 85:
case 86:
case 87:
case 88: return "Tanker, Reserved";
case 89: return "Tanker, No additional information";
case 90: return "Other Type, all ships";
case 91: return "Other Type, Hazardous category A";
case 92: return "Other Type, Hazardous category B";
case 93: return "Other Type, Hazardous category C";
case 94: return "Other Type, Hazardous category D";
case 95:
case 96:
case 97:
case 98: return "Other Type, Reserved";
case 99: return "Other Type, no additional information";
default: return "Unknown";
}
}
/**
* Находит существующее AIS судно или создает новое
*/
private AISVessel findOrCreateAISVessel(String mmsi) {
for (AISVessel vessel : aisVessels) {
if (mmsi.equals(vessel.getMmsi())) {
return vessel;
}
}
// Создаем новое судно
AISVessel newVessel = new AISVessel(mmsi);
aisVessels.add(newVessel);
Log.d(TAG, "Создано новое AIS судно: " + mmsi);
return newVessel;
}
/**
* Очищает устаревшие AIS суда (данные старше 10 минут)
*/
public void cleanupStaleAISVessels() {
java.util.Iterator<AISVessel> iterator = aisVessels.iterator();
int removedCount = 0;
while (iterator.hasNext()) {
AISVessel vessel = iterator.next();
if (vessel.isDataStale()) {
iterator.remove();
removedCount++;
Log.d(TAG, "Удалено устаревшее AIS судно: " + vessel.getMmsi());
}
}
if (removedCount > 0) {
Log.i(TAG, "Удалено " + removedCount + " устаревших AIS судов");
}
}
/**
* Получает количество активных AIS судов
*/
public int getActiveAISVesselCount() {
cleanupStaleAISVessels();
return aisVessels.size();
}
/**
* Получает AIS судно по MMSI
*/
public AISVessel getAISVesselByMMSI(String mmsi) {
for (AISVessel vessel : aisVessels) {
if (mmsi.equals(vessel.getMmsi())) {
return vessel;
}
}
return null;
}
/**
* Обновляет статус активности AIS судов
*/
public void updateAISVesselActivity() {
long currentTime = System.currentTimeMillis();
for (AISVessel vessel : aisVessels) {
// Считаем судно активным, если данные получены менее 5 минут назад
boolean isActive = (currentTime - vessel.getLastUpdate().toInstant(java.time.ZoneOffset.UTC).toEpochMilli()) < 300000;
vessel.setActive(isActive);
}
}
/**
* Парсит координаты из NMEA формата
*/
private double parseCoordinate(String coordinate, boolean isPositive) {
// Проверяем, что координата не пустая
if (coordinate == null || coordinate.trim().isEmpty()) {
return 0.0;
}
try {
double value = Double.parseDouble(coordinate);
int degrees = (int) (value / 100);
double minutes = value - (degrees * 100);
double result = degrees + (minutes / 60.0);
return isPositive ? result : -result;
} catch (NumberFormatException e) {
Log.w(TAG, "Ошибка парсинга координаты: " + coordinate + ", ошибка: " + e.getMessage());
return 0.0;
}
}
/**
* Проверяет контрольную сумму NMEA сообщения
*/
public boolean validateChecksum(String nmeaSentence) {
if (nmeaSentence == null || !nmeaSentence.contains("*")) {
return false;
}
int asteriskIndex = nmeaSentence.indexOf('*');
String sentence = nmeaSentence.substring(1, asteriskIndex);
String checksum = nmeaSentence.substring(asteriskIndex + 1);
int calculatedChecksum = 0;
for (char c : sentence.toCharArray()) {
calculatedChecksum ^= c;
}
String hexChecksum = String.format("%02X", calculatedChecksum);
return hexChecksum.equals(checksum);
}
public Vessel getOwnVessel() {
return ownVessel;
}
public List<AISVessel> getAISVessels() {
return new ArrayList<>(aisVessels);
}
/**
* Получает количество спутников GPS
*/
public int getGPSSatellites() {
return gpsSatellites;
}
/**
* Получает количество спутников GLONASS
*/
public int getGLONASSSatellites() {
return glonassSatellites;
}
/**
* Получает количество спутников Galileo
*/
public int getGalileoSatellites() {
return galileoSatellites;
}
/**
* Получает общее количество спутников всех систем
*/
public int getTotalSatellites() {
return gpsSatellites + glonassSatellites + galileoSatellites;
}
/**
* Сбрасывает счетчики спутников
*/
public void resetSatelliteCounters() {
gpsSatellites = 0;
glonassSatellites = 0;
galileoSatellites = 0;
ownVessel.setSatellites(0);
Log.d(TAG, "Счетчики спутников сброшены");
}
/**
* Синхронизирует данные о спутниках с GPSLocationListener
*/
public void syncSatelliteData() {
if (gpsLocationListener != null) {
gpsLocationListener.setSatellitesInVessel(ownVessel);
}
}
/**
* Получает текущее состояние объекта Vessel
*/
public String getVesselStatus() {
return String.format("Vessel: satellites=%d, activeSatellites=%d, GPS=%d, GLONASS=%d, Galileo=%d",
ownVessel.getSatellites(), ownVessel.getActiveSatellites(),
gpsSatellites, glonassSatellites, galileoSatellites);
}
/**
* Декодирует AIS сообщение типа 4 (Base Station Report)
*/
private void decodeBaseStationReport(String payload) {
try {
Log.d(TAG, "Декодируем Base Station Report, payload: " + payload + " (длина: " + payload.length() + ")");
// MMSI (30 бит) - начинается с бита 8
String mmsiBits = decodeAISField(payload, 8, 30);
int mmsi = Integer.parseInt(mmsiBits, 2);
Log.d(TAG, "MMSI bits: " + mmsiBits + " = " + mmsi);
// Year (14 бит) - бит 38
String yearBits = decodeAISField(payload, 38, 14);
int year = Integer.parseInt(yearBits, 2);
Log.d(TAG, "Year bits: " + yearBits + " = " + year);
// Month (4 бита) - бит 52
String monthBits = decodeAISField(payload, 52, 4);
int month = Integer.parseInt(monthBits, 2);
Log.d(TAG, "Month bits: " + monthBits + " = " + month);
// Day (5 бит) - бит 56
String dayBits = decodeAISField(payload, 56, 5);
int day = Integer.parseInt(dayBits, 2);
Log.d(TAG, "Day bits: " + dayBits + " = " + day);
// Hour (5 бит) - бит 61
String hourBits = decodeAISField(payload, 61, 5);
int hour = Integer.parseInt(hourBits, 2);
Log.d(TAG, "Hour bits: " + hourBits + " = " + hour);
// Minute (6 бит) - бит 66
String minuteBits = decodeAISField(payload, 66, 6);
int minute = Integer.parseInt(minuteBits, 2);
Log.d(TAG, "Minute bits: " + minuteBits + " = " + minute);
// Second (6 бит) - бит 72
String secondBits = decodeAISField(payload, 72, 6);
int second = Integer.parseInt(secondBits, 2);
Log.d(TAG, "Second bits: " + secondBits + " = " + second);
// Position Accuracy (1 бит) - бит 78
String accuracyBits = decodeAISField(payload, 78, 1);
int accuracy = Integer.parseInt(accuracyBits, 2);
Log.d(TAG, "Accuracy bits: " + accuracyBits + " = " + accuracy);
// Longitude (28 бит) - бит 79
String lonBits = decodeAISField(payload, 79, 28);
double longitude = parseAISCoordinate(lonBits, 28);
Log.d(TAG, "Longitude bits: " + lonBits + " = " + longitude);
// Latitude (27 бит) - бит 107
String latBits = decodeAISField(payload, 107, 27);
double latitude = parseAISCoordinate(latBits, 27);
Log.d(TAG, "Latitude bits: " + latBits + " = " + latitude);
// EPFD Type (4 бита) - бит 134
String epfdBits = decodeAISField(payload, 134, 4);
int epfdType = Integer.parseInt(epfdBits, 2);
Log.d(TAG, "EPFD Type bits: " + epfdBits + " = " + epfdType);
Log.d(TAG, String.format("AIS Base Station: MMSI=%d, date=%04d-%02d-%02d %02d:%02d:%02d, lat=%.6f, lon=%.6f, accuracy=%d, epfd=%d",
mmsi, year, month, day, hour, minute, second, latitude, longitude, accuracy, epfdType));
// Создаем или обновляем AIS судно (базовая станция)
AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi));
vessel.updatePosition(latitude, longitude, 0.0, 0.0);
vessel.setPositionAccuracy(accuracy == 1);
vessel.setVesselClass("Base Station");
vessel.setLastUpdate(java.time.LocalDateTime.now());
// Уведомляем слушателя
if (listener != null) {
listener.onAISVesselUpdated(vessel);
}
} catch (Exception e) {
Log.e(TAG, "Ошибка декодирования Base Station Report: " + e.getMessage(), e);
}
}
/**
* Декодирует AIS сообщение типа 14 (Safety Related Broadcast Message)
*/
private void decodeSafetyBroadcast(String payload) {
try {
Log.d(TAG, "Декодируем Safety Broadcast, payload: " + payload + " (длина: " + payload.length() + ")");
// MMSI (30 бит) - начинается с бита 8
String mmsiBits = decodeAISField(payload, 8, 30);
int mmsi = Integer.parseInt(mmsiBits, 2);
Log.d(TAG, "MMSI bits: " + mmsiBits + " = " + mmsi);
// Spare (2 бита) - бит 38
String spareBits = decodeAISField(payload, 38, 2);
int spare = Integer.parseInt(spareBits, 2);
Log.d(TAG, "Spare bits: " + spareBits + " = " + spare);
// Text (120 бит) - бит 40
String textBits = decodeAISField(payload, 40, 120);
String safetyText = decodeAISString(textBits);
Log.d(TAG, "Safety Text bits: " + textBits + " = '" + safetyText + "'");
Log.d(TAG, String.format("AIS Safety Broadcast: MMSI=%d, text='%s'", mmsi, safetyText));
// Отправляем лог наружу
try {
com.grigowashere.aismap.utils.LogSender.logShipUpdate(String.valueOf(mmsi), "Safety: " + safetyText);
} catch (Throwable t) {
Log.w(TAG, "Ошибка отправки safety-лога: " + t.getMessage());
}
// Создаем или обновляем AIS судно
AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi));
vessel.setLastSafetyMessage(safetyText);
vessel.setLastUpdate(java.time.LocalDateTime.now());
// Уведомляем слушателя
if (listener != null) {
listener.onAISVesselUpdated(vessel);
}
} catch (Exception e) {
Log.e(TAG, "Ошибка декодирования Safety Broadcast: " + e.getMessage(), e);
}
}
/**
* Декодирует AIS сообщение типа 18 (Standard Class B Equipment Position Report)
*/
private void decodeClassBPositionReport(String payload) {
try {
Log.d(TAG, "Декодируем Class B Position Report, payload: " + payload + " (длина: " + payload.length() + ")");
// MMSI (30 бит) - начинается с бита 8
String mmsiBits = decodeAISField(payload, 8, 30);
int mmsi = Integer.parseInt(mmsiBits, 2);
Log.d(TAG, "MMSI bits: " + mmsiBits + " = " + mmsi);
// Speed Over Ground (10 бит) - бит 46
String speedBits = decodeAISField(payload, 46, 10);
double speed = Integer.parseInt(speedBits, 2) / 10.0;
Log.d(TAG, "Speed bits: " + speedBits + " = " + speed);
// Position Accuracy (1 бит) - бит 56
String accuracyBits = decodeAISField(payload, 56, 1);
int accuracy = Integer.parseInt(accuracyBits, 2);
Log.d(TAG, "Accuracy bits: " + accuracyBits + " = " + accuracy);
// Longitude (28 бит) - бит 57
String lonBits = decodeAISField(payload, 57, 28);
double longitude = parseAISCoordinate(lonBits, 28);
Log.d(TAG, "Longitude bits: " + lonBits + " = " + longitude);
// Latitude (27 бит) - бит 85
String latBits = decodeAISField(payload, 85, 27);
double latitude = parseAISCoordinate(latBits, 27);
Log.d(TAG, "Latitude bits: " + latBits + " = " + latitude);
// Course Over Ground (12 бит) - бит 112
String courseBits = decodeAISField(payload, 112, 12);
double course = Integer.parseInt(courseBits, 2) / 10.0;
Log.d(TAG, "Course bits: " + courseBits + " = " + course);
// True Heading (9 бит) - бит 124
String headingBits = decodeAISField(payload, 124, 9);
double heading = Integer.parseInt(headingBits, 2);
Log.d(TAG, "Heading bits: " + headingBits + " = " + heading);
// Time Stamp (6 бит) - бит 133
String timestampBits = decodeAISField(payload, 133, 6);
int timestamp = Integer.parseInt(timestampBits, 2);
Log.d(TAG, "Timestamp bits: " + timestampBits + " = " + timestamp);
// Regional Reserved (2 бита) - бит 139
String regionalBits = decodeAISField(payload, 139, 2);
int regional = Integer.parseInt(regionalBits, 2);
Log.d(TAG, "Regional bits: " + regionalBits + " = " + regional);
// Spare (3 бита) - бит 141
String spareBits = decodeAISField(payload, 141, 3);
int spare = Integer.parseInt(spareBits, 2);
Log.d(TAG, "Spare bits: " + spareBits + " = " + spare);
Log.d(TAG, String.format("AIS Class B Position: MMSI=%d, lat=%.6f, lon=%.6f, course=%.1f, speed=%.1f, heading=%.1f",
mmsi, latitude, longitude, course, speed, heading));
// Создаем или обновляем AIS судно
AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi));
// Логика приоритета классов:
// - Если уже Class A: игнорируем обновление типа 18 полностью
// - Если Extended Class B: обновляем только динамику (позиция, скорость, курс и т.п.), класс не меняем
String existingClass = vessel.getVesselClass();
if ("Class A".equals(existingClass)) {
Log.d(TAG, "Пропускаем обновление Class B (тип 18) для судна класса Class A: " + mmsi);
return;
}
boolean keepExtended = "Extended Class B".equals(existingClass);
vessel.updatePosition(latitude, longitude, course, speed);
vessel.setHeading(heading);
vessel.setPositionAccuracy(accuracy == 1);
vessel.setLastUpdate(java.time.LocalDateTime.now());
if (!keepExtended) {
vessel.setVesselClass("Class B");
}
// В Class B Position Report размеры не передаются, но мы сохраняем существующие
Log.d(TAG, "Class B Position Report - размеры не передаются, сохраняем существующие: L=" + vessel.getLength() + ", W=" + vessel.getWidth());
// Отправляем информацию о корабле на внешний ресурс
// Добавляем статические поля, если они уже известны (из сообщений 24 и др.)
StringBuilder info = new StringBuilder(
String.format(java.util.Locale.US,
"Class B: lat=%.6f, lon=%.6f, course=%.1f, speed=%.1f, heading=%.1f, accuracy=%s",
latitude, longitude, course, speed, heading, (accuracy == 1 ? "high" : "low"))
);
if (vessel.getVesselName() != null && !vessel.getVesselName().trim().isEmpty()) {
info.append(String.format(java.util.Locale.US, ", name='%s'", vessel.getVesselName()));
}
if (vessel.getCallSign() != null && !vessel.getCallSign().trim().isEmpty()) {
info.append(String.format(java.util.Locale.US, ", callSign='%s'", vessel.getCallSign()));
}
if (vessel.getVesselType() != null && !vessel.getVesselType().trim().isEmpty()) {
info.append(String.format(java.util.Locale.US, ", type=%s", vessel.getVesselType()));
}
if (vessel.getLength() > 0 || vessel.getWidth() > 0) {
info.append(String.format(java.util.Locale.US, ", L=%.1f, W=%.1f", vessel.getLength(), vessel.getWidth()));
}
if (vessel.getDraft() > 0) {
info.append(String.format(java.util.Locale.US, ", D=%.1f", vessel.getDraft()));
}
if (vessel.getDestination() != null && !vessel.getDestination().trim().isEmpty()) {
info.append(String.format(java.util.Locale.US, ", dest='%s'", vessel.getDestination()));
}
LogSender.logShipUpdate(String.valueOf(mmsi), info.toString());
// Уведомляем слушателя
if (listener != null) {
listener.onAISVesselUpdated(vessel);
}
} catch (Exception e) {
Log.e(TAG, "Ошибка декодирования Class B Position Report: " + e.getMessage(), e);
}
}
/**
* Декодирует AIS сообщение типа 19 (Extended Class B Equipment Position Report)
*/
private void decodeExtendedClassBPositionReport(String payload) {
try {
Log.d(TAG, "Декодируем Extended Class B Position Report, payload: " + payload + " (длина: " + payload.length() + ")");
// Проверяем длину payload - для Extended Class B должно быть достаточно битов
int totalBits = payload.length() * 6;
Log.d(TAG, "Общая длина payload в битах: " + totalBits);
if (totalBits < 312) { // Минимум для Extended Class B
Log.w(TAG, "Extended Class B payload слишком короткий: " + totalBits + " бит, ожидается минимум 312");
return;
}
// MMSI (30 бит) - начинается с бита 8
String mmsiBits = decodeAISField(payload, 8, 30);
int mmsi = Integer.parseInt(mmsiBits, 2);
Log.d(TAG, "MMSI bits: " + mmsiBits + " = " + mmsi);
// Speed Over Ground (10 бит) - бит 46
String speedBits = decodeAISField(payload, 46, 10);
double speed = Integer.parseInt(speedBits, 2) / 10.0;
Log.d(TAG, "Speed bits: " + speedBits + " = " + speed);
// Position Accuracy (1 бит) - бит 56
String accuracyBits = decodeAISField(payload, 56, 1);
int accuracy = Integer.parseInt(accuracyBits, 2);
Log.d(TAG, "Accuracy bits: " + accuracyBits + " = " + accuracy);
// Longitude (28 бит) - бит 57
String lonBits = decodeAISField(payload, 57, 28);
double longitude = parseAISCoordinate(lonBits, 28);
Log.d(TAG, "Longitude bits: " + lonBits + " = " + longitude);
// Latitude (27 бит) - бит 85
String latBits = decodeAISField(payload, 85, 27);
double latitude = parseAISCoordinate(latBits, 27);
Log.d(TAG, "Latitude bits: " + latBits + " = " + latitude);
// Course Over Ground (12 бит) - бит 112
String courseBits = decodeAISField(payload, 112, 12);
double course = Integer.parseInt(courseBits, 2) / 10.0;
Log.d(TAG, "Course bits: " + courseBits + " = " + course);
// True Heading (9 бит) - бит 124
String headingBits = decodeAISField(payload, 124, 9);
double heading = Integer.parseInt(headingBits, 2);
Log.d(TAG, "Heading bits: " + headingBits + " = " + heading);
// Time Stamp (6 бит) - бит 133
String timestampBits = decodeAISField(payload, 133, 6);
int timestamp = Integer.parseInt(timestampBits, 2);
Log.d(TAG, "Timestamp bits: " + timestampBits + " = " + timestamp);
// Regional Reserved (4 бита) - бит 139
String regionalBits = decodeAISField(payload, 139, 4);
int regional = Integer.parseInt(regionalBits, 2);
Log.d(TAG, "Regional bits: " + regionalBits + " = " + regional);
// Vessel Name (120 бит) - бит 143
String nameBits = decodeAISField(payload, 143, 120);
String vesselName = decodeAISString(nameBits);
Log.d(TAG, "Name bits: " + nameBits + " = '" + vesselName + "'");
// Ship Type (8 бит) - бит 263
String typeBits = decodeAISField(payload, 263, 8);
int vesselTypeCode = Integer.parseInt(typeBits, 2);
Log.d(TAG, "Type bits: " + typeBits + " = " + vesselTypeCode);
// Dimension Reference (4 бита) - бит 271
String dimRefABits = decodeAISField(payload, 271, 4);
String dimRefBBits = decodeAISField(payload, 275, 4);
String dimRefCBits = decodeAISField(payload, 279, 4);
String dimRefDBits = decodeAISField(payload, 283, 4);
int dimRefA = Integer.parseInt(dimRefABits, 2);
int dimRefB = Integer.parseInt(dimRefBBits, 2);
int dimRefC = Integer.parseInt(dimRefCBits, 2);
int dimRefD = Integer.parseInt(dimRefDBits, 2);
Log.d(TAG, "Dimension Reference: A=" + dimRefA + ", B=" + dimRefB + ", C=" + dimRefC + ", D=" + dimRefD);
// Vessel Dimensions (40 бит) - начинаются с бита 287
// Проверяем, есть ли достаточно битов для размеров
if (totalBits < 327) {
Log.w(TAG, "Extended Class B - недостаточно битов для размеров: " + totalBits + " < 327");
// Создаем судно без размеров
AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi));
// Если судно уже Class A, не перезаписываем данными Extended Class B
String existingClassShort = vessel.getVesselClass();
if ("Class A".equals(existingClassShort)) {
Log.d(TAG, "Пропускаем обновление Extended Class B для судна класса Class A: " + mmsi);
return;
}
vessel.updatePosition(latitude, longitude, course, speed);
vessel.setHeading(heading);
vessel.setPositionAccuracy(accuracy == 1);
vessel.setVesselName(vesselName);
vessel.setVesselType(getVesselType(vesselTypeCode));
vessel.setLastUpdate(java.time.LocalDateTime.now());
vessel.setVesselClass("Extended Class B");
if (listener != null) {
listener.onAISVesselUpdated(vessel);
}
// Логируем короткое сообщение типа 19 с доступными данными
StringBuilder shortInfo = new StringBuilder(
String.format(java.util.Locale.US,
"Extended Class B (short): name='%s', lat=%.6f, lon=%.6f, course=%.1f, speed=%.1f, heading=%.1f, accuracy=%s",
vesselName, latitude, longitude, course, speed, heading, (accuracy == 1 ? "high" : "low"))
);
if (vessel.getCallSign() != null && !vessel.getCallSign().trim().isEmpty()) {
shortInfo.append(String.format(java.util.Locale.US, ", callSign='%s'", vessel.getCallSign()));
}
if (vessel.getDestination() != null && !vessel.getDestination().trim().isEmpty()) {
shortInfo.append(String.format(java.util.Locale.US, ", dest='%s'", vessel.getDestination()));
}
LogSender.logShipUpdate(String.valueOf(mmsi), shortInfo.toString());
return;
}
// Dim.A (10 бит) - от носа до антенны
String dimABits = decodeAISField(payload, 287, 10);
// Dim.B (10 бит) - от антенны до кормы
String dimBBits = decodeAISField(payload, 297, 10);
// Dim.C (10 бит) - от левого борта до антенны
String dimCBits = decodeAISField(payload, 307, 10);
// Dim.D (10 бит) - от антенны до правого борта
String dimDBits = decodeAISField(payload, 317, 10);
Log.d(TAG, "Raw dimension bits - Dim.A: " + dimABits + ", Dim.B: " + dimBBits + ", Dim.C: " + dimCBits + ", Dim.D: " + dimDBits);
int dimA = Integer.parseInt(dimABits, 2);
int dimB = Integer.parseInt(dimBBits, 2);
int dimC = Integer.parseInt(dimCBits, 2);
int dimD = Integer.parseInt(dimDBits, 2);
// В AIS стандарте размеры кодируются как 6-битные значения:
// 0 = не указано, 1-62 = размер в метрах, 63 = размер 63+ метра
// Но мы получаем 10-битные значения, поэтому нужно их правильно интерпретировать
// Проверяем, что размеры в разумных пределах (0-1000 метров)
if (dimA > 1000 || dimB > 1000 || dimC > 1000 || dimD > 1000) {
Log.w(TAG, "Размеры судна выходят за разумные пределы: A=" + dimA + ", B=" + dimB + ", C=" + dimC + ", D=" + dimD);
// Возможно, мы неправильно интерпретируем битовые поля
// Попробуем интерпретировать как 6-битные значения
dimA = dimA & 0x3F; // Берем только младшие 6 бит
dimB = dimB & 0x3F;
dimC = dimC & 0x3F;
dimD = dimD & 0x3F;
Log.d(TAG, "Исправленные размеры (6-битные): A=" + dimA + ", B=" + dimB + ", C=" + dimC + ", D=" + dimD);
}
// Дополнительная проверка: если размеры все еще неразумные, используем Dimension Reference
if (dimA > 100 || dimB > 100 || dimC > 100 || dimD > 100) {
Log.w(TAG, "Размеры все еще неразумные, используем Dimension Reference: A=" + dimA + ", B=" + dimB + ", C=" + dimC + ", D=" + dimD);
// Используем Dimension Reference как fallback
dimA = dimRefA;
dimB = dimRefB;
dimC = dimRefC;
dimD = dimRefD;
Log.d(TAG, "Fallback размеры из Dimension Reference: A=" + dimA + ", B=" + dimB + ", C=" + dimC + ", D=" + dimD);
}
// Размеры судна рассчитываются как:
// Длина = Dim.A + Dim.B (от носа до антенны + от антенны до кормы)
// Ширина = Dim.C + Dim.D (от левого борта до антенны + от антенны до правого борта)
double length = dimA + dimB;
double width = dimC + dimD;
Log.d(TAG, "Dimensions - Dim.A (нос-антенна): " + dimABits + " = " + dimA);
Log.d(TAG, "Dimensions - Dim.B (антенна-корма): " + dimBBits + " = " + dimB);
Log.d(TAG, "Dimensions - Dim.C (левый борт-антенна): " + dimCBits + " = " + dimC);
Log.d(TAG, "Dimensions - Dim.D (антенна-правый борт): " + dimDBits + " = " + dimD);
Log.d(TAG, "Dimensions - Total Length (A+B): " + length + "m");
Log.d(TAG, "Dimensions - Total Width (C+D): " + width + "m");
Log.d(TAG, String.format("AIS Extended Class B: MMSI=%d, name='%s', lat=%.6f, lon=%.6f, course=%.1f, speed=%.1f, type=%d, L=%.1f, W=%.1f",
mmsi, vesselName, latitude, longitude, course, speed, vesselTypeCode, length, width));
// Создаем или обновляем AIS судно
AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi));
// Если судно уже Class A, не перезаписываем данными Extended Class B
String existingClassFull = vessel.getVesselClass();
if ("Class A".equals(existingClassFull)) {
Log.d(TAG, "Пропускаем обновление Extended Class B для судна класса Class A: " + mmsi);
return;
}
vessel.updatePosition(latitude, longitude, course, speed);
vessel.setHeading(heading);
vessel.setPositionAccuracy(accuracy == 1);
vessel.setVesselName(vesselName);
vessel.setVesselType(getVesselType(vesselTypeCode));
vessel.setLength(length);
vessel.setWidth(width);
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",
vesselName, latitude, longitude, course, speed, getVesselType(vesselTypeCode), length, width);
LogSender.logShipUpdate(String.valueOf(mmsi), vesselInfo);
// Уведомляем слушателя
if (listener != null) {
listener.onAISVesselUpdated(vessel);
}
} catch (Exception e) {
Log.e(TAG, "Ошибка декодирования Extended Class B Position Report: " + e.getMessage(), e);
}
}
/**
* Декодирует AIS сообщение типа 21 (Aid-to-Navigation Report)
*/
private void decodeAidToNavigationReport(String payload) {
try {
Log.d(TAG, "Декодируем Aid-to-Navigation Report, payload: " + payload + " (длина: " + payload.length() + ")");
// MMSI (30 бит) - начинается с бита 8
String mmsiBits = decodeAISField(payload, 8, 30);
int mmsi = Integer.parseInt(mmsiBits, 2);
Log.d(TAG, "MMSI bits: " + mmsiBits + " = " + mmsi);
// Aid Type (5 бит) - бит 38
String aidTypeBits = decodeAISField(payload, 38, 5);
int aidType = Integer.parseInt(aidTypeBits, 2);
Log.d(TAG, "Aid Type bits: " + aidTypeBits + " = " + aidType);
// Name (120 бит) - бит 43
String nameBits = decodeAISField(payload, 43, 120);
String aidName = decodeAISString(nameBits);
Log.d(TAG, "Name bits: " + nameBits + " = '" + aidName + "'");
// Position Accuracy (1 бит) - бит 163
String accuracyBits = decodeAISField(payload, 163, 1);
int accuracy = Integer.parseInt(accuracyBits, 2);
Log.d(TAG, "Accuracy bits: " + accuracyBits + " = " + accuracy);
// Longitude (28 бит) - бит 164
String lonBits = decodeAISField(payload, 164, 28);
double longitude = parseAISCoordinate(lonBits, 28);
Log.d(TAG, "Longitude bits: " + lonBits + " = " + longitude);
// Latitude (27 бит) - бит 192
String latBits = decodeAISField(payload, 192, 27);
double latitude = parseAISCoordinate(latBits, 27);
Log.d(TAG, "Latitude bits: " + latBits + " = " + latitude);
// Dimension Reference (4 бита) - бит 219
String dimRefABits = decodeAISField(payload, 219, 4);
String dimRefBBits = decodeAISField(payload, 223, 4);
String dimRefCBits = decodeAISField(payload, 227, 4);
String dimRefDBits = decodeAISField(payload, 231, 4);
int dimRefA = Integer.parseInt(dimRefABits, 2);
int dimRefB = Integer.parseInt(dimRefBBits, 2);
int dimRefC = Integer.parseInt(dimRefCBits, 2);
int dimRefD = Integer.parseInt(dimRefDBits, 2);
// Vessel Dimensions (30 бит) - бит 235
// Dim.A (10 бит) - от носа до антенны
String dimABits = decodeAISField(payload, 235, 10);
// Dim.B (10 бит) - от антенны до кормы
String dimBBits = decodeAISField(payload, 245, 10);
// Dim.C (10 бит) - от левого борта до антенны
String dimCBits = decodeAISField(payload, 255, 10);
// Dim.D (10 бит) - от антенны до правого борта
String dimDBits = decodeAISField(payload, 265, 10);
// Draft (8 бит) - осадка
String draftBits = decodeAISField(payload, 275, 8);
int dimA = Integer.parseInt(dimABits, 2);
int dimB = Integer.parseInt(dimBBits, 2);
int dimC = Integer.parseInt(dimCBits, 2);
int dimD = Integer.parseInt(dimDBits, 2);
// Размеры судна рассчитываются как:
// Длина = Dim.A + Dim.B (от носа до антенны + от антенны до кормы)
// Ширина = Dim.C + Dim.D (от левого борта до антенны + от антенны до правого борта)
double length = dimA + dimB;
double width = dimC + dimD;
double draft = Integer.parseInt(draftBits, 2) / 10.0;
Log.d(TAG, String.format("AIS Aid-to-Navigation: MMSI=%d, type=%d, name='%s', lat=%.6f, lon=%.6f, L=%.1f, W=%.1f, D=%.1f",
mmsi, aidType, aidName, latitude, longitude, length, width, draft));
// Создаем или обновляем AIS судно (навигационный знак)
AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi));
vessel.updatePosition(latitude, longitude, 0.0, 0.0);
vessel.setPositionAccuracy(accuracy == 1);
vessel.setVesselName(aidName);
vessel.setVesselType("Aid-to-Navigation");
vessel.setLength(length);
vessel.setWidth(width);
vessel.setDraft(draft);
vessel.setLastUpdate(java.time.LocalDateTime.now());
vessel.setVesselClass("Navigation Aid");
// Уведомляем слушателя
if (listener != null) {
listener.onAISVesselUpdated(vessel);
}
} catch (Exception e) {
Log.e(TAG, "Ошибка декодирования Aid-to-Navigation Report: " + e.getMessage(), e);
}
}
/**
* Декодирует AIS сообщение типа 24 (Static Data Report)
*/
private void decodeStaticDataReport(String payload) {
try {
Log.d(TAG, "Декодируем Static Data Report, payload: " + payload + " (длина: " + payload.length() + ")");
// MMSI (30 бит) - начинается с бита 8
String mmsiBits = decodeAISField(payload, 8, 30);
int mmsi = Integer.parseInt(mmsiBits, 2);
Log.d(TAG, "MMSI bits: " + mmsiBits + " = " + mmsi);
// Part Number (2 бита) - бит 38
String partBits = decodeAISField(payload, 38, 2);
int partNumber = Integer.parseInt(partBits, 2);
Log.d(TAG, "Part Number bits: " + partBits + " = " + partNumber);
if (partNumber == 0) {
// Part A: Vessel Name
String nameBits = decodeAISField(payload, 40, 120);
String vesselName = decodeAISString(nameBits);
Log.d(TAG, "Vessel Name bits: " + nameBits + " = '" + vesselName + "'");
Log.d(TAG, String.format("AIS Static Data Part A: MMSI=%d, name='%s'", mmsi, vesselName));
// Обновляем AIS судно
AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi));
vessel.setVesselName(vesselName);
vessel.setLastUpdate(java.time.LocalDateTime.now());
if (listener != null) {
listener.onAISVesselUpdated(vessel);
}
// Логируем статические данные Class B (Part A)
String vesselInfo = String.format(java.util.Locale.US,
"Class B Static A: name='%s'",
vesselName);
LogSender.logShipUpdate(String.valueOf(mmsi), vesselInfo);
} else if (partNumber == 1) {
// Part B: Vessel Type, Dimensions, etc.
String typeBits = decodeAISField(payload, 40, 8);
int vesselTypeCode = Integer.parseInt(typeBits, 2);
Log.d(TAG, "Vessel Type bits: " + typeBits + " = " + vesselTypeCode);
// Vendor ID (42 бита) - бит 48
String vendorBits = decodeAISField(payload, 48, 42);
String vendorId = decodeAISString(vendorBits);
Log.d(TAG, "Vendor ID bits: " + vendorBits + " = '" + vendorId + "'");
// Call Sign (42 бита) - бит 90
String callSignBits = decodeAISField(payload, 90, 42);
String callSign = decodeAISString(callSignBits);
Log.d(TAG, "Call Sign bits: " + callSignBits + " = '" + callSign + "'");
// Dimension Reference (6 бит каждое) - бит 132
// Согласно онлайн декодеру, размеры находятся в других позициях
// Попробуем позиции, которые соответствуют онлайн декодеру
String dimRefABits = decodeAISField(payload, 132, 9);
String dimRefBBits = decodeAISField(payload, 141, 9);
String dimRefCBits = decodeAISField(payload, 150, 6);
String dimRefDBits = decodeAISField(payload, 156, 6);
int dimRefA = Integer.parseInt(dimRefABits, 2);
int dimRefB = Integer.parseInt(dimRefBBits, 2);
int dimRefC = Integer.parseInt(dimRefCBits, 2);
int dimRefD = Integer.parseInt(dimRefDBits, 2);
Log.d(TAG, "Dimension Reference bits - A: " + dimRefABits + " = " + dimRefA);
Log.d(TAG, "Dimension Reference bits - B: " + dimRefBBits + " = " + dimRefB);
Log.d(TAG, "Dimension Reference bits - C: " + dimRefCBits + " = " + dimRefC);
Log.d(TAG, "Dimension Reference bits - D: " + dimRefDBits + " = " + dimRefD);
// Проверяем, есть ли достаточно битов для размеров
int totalBits = payload.length() * 6;
Log.d(TAG, "Static Data Part B - общая длина payload в битах: " + totalBits);
double length = 0.0;
double width = 0.0;
double draft = 0.0;
// Для коротких сообщений типа 24 Part B (168 бит) используем Dimension Reference
// В коротких сообщениях размеры кодируются в Dimension Reference полях
if (totalBits >= 168) {
// В сообщениях типа 24 Part B для Class B судов
// размеры кодируются в полях Dimension Reference (биты 132-147)
// где каждое поле - 4 бита и представляет размер в метрах
// Эти поля уже правильно декодированы выше
// Размеры судна рассчитываются как:
// Длина = Dim.A + Dim.B (от носа до антенны + от антенны до кормы)
// Ширина = Dim.C + Dim.D (от левого борта до антенны + от антенны до правого борта)
length = dimRefA + dimRefB;
width = dimRefC + dimRefD;
Log.d(TAG, "Static Data Part B - используем Dimension Reference:");
Log.d(TAG, " Dim.A (нос-антенна): " + dimRefA + " м");
Log.d(TAG, " Dim.B (антенна-корма): " + dimRefB + " м");
Log.d(TAG, " Dim.C (левый борт-антенна): " + dimRefC + " м");
Log.d(TAG, " Dim.D (антенна-правый борт): " + dimRefD + " м");
Log.d(TAG, "Static Data Part B - итоговые размеры: L=" + length + ", W=" + width);
} else {
Log.w(TAG, "Static Data Part B - недостаточно битов для размеров: " + totalBits + " < 168");
// Используем нулевые размеры
length = 0.0;
width = 0.0;
}
Log.d(TAG, String.format("AIS Static Data Part B: MMSI=%d, type=%d, vendor='%s', callSign='%s', L=%.1f, W=%.1f, D=%.1f",
mmsi, vesselTypeCode, vendorId, callSign, length, width, draft));
// Обновляем AIS судно
AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi));
vessel.setVesselType(getVesselType(vesselTypeCode));
vessel.setVendorId(vendorId);
vessel.setCallSign(callSign);
vessel.setLength(length);
vessel.setWidth(width);
vessel.setDraft(draft);
vessel.setLastUpdate(java.time.LocalDateTime.now());
if (listener != null) {
listener.onAISVesselUpdated(vessel);
}
// Логируем статические данные Class B (Part B)
String vesselInfoB = String.format(java.util.Locale.US,
"Class B Static B: name='%s', callSign='%s', type=%s, L=%.1f, W=%.1f, D=%.1f",
vessel.getVesselName() != null ? vessel.getVesselName() : "",
callSign,
getVesselType(vesselTypeCode),
length, width, draft);
LogSender.logShipUpdate(String.valueOf(mmsi), vesselInfoB);
}
} catch (Exception e) {
Log.e(TAG, "Ошибка декодирования Static Data Report: " + e.getMessage(), e);
}
}
}