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 aisVessels; private NMEAParserListener listener; private GPSLocationListener gpsLocationListener; // Поля для работы с AIS фрагментами private java.util.Map> aisFragments = new java.util.HashMap<>(); private java.util.Map 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 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> iterator = aisFragmentTimestamps.entrySet().iterator(); while (iterator.hasNext()) { java.util.Map.Entry 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); double rateOfTurn = parseAISRateOfTurn(rotBits); Log.d(TAG, "Rate of Turn bits: " + rotBits + " = " + rateOfTurn + " °/мин"); // 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.setHeading(heading); vessel.setNavigationalStatus(getNavigationStatus(status)); vessel.setLastUpdate(java.time.LocalDateTime.now()); // Отправляем информацию о корабле на внешний ресурс String vesselInfo = String.format("lat=%.6f, lon=%.6f, course=%.1f, speed=%.1f, status=%s, ROT=%.1f", latitude, longitude, course, speed, getNavigationStatus(status), rateOfTurn); LogSender.logShipUpdate(String.valueOf(mmsi), vesselInfo); // Уведомляем слушателя 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()); // Отправляем информацию о корабле на внешний ресурс 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); } } 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 Rate of Turn (скорость поворота) * Согласно стандарту ITU-R M.1371-5, таблица 47 */ private double parseAISRateOfTurn(String bits) { int value = Integer.parseInt(bits, 2); // Специальные значения согласно стандарту if (value == 0) { return 0.0; // Не поворачивается } else if (value == 127) { return 0.0; // Неопределенное значение } else if (value >= 1 && value <= 126) { // Поворот вправо: ROT = value / 4.733 return value / 4.733; } else if (value >= 128 && value <= 255) { // Поворот влево: ROT = -(value - 128) / 4.733 return -(value - 128) / 4.733; } else { return 0.0; // Неопределенное значение } } /** * Парсит AIS координаты */ } else if (value == 2) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 3) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 4) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 5) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 6) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 7) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 8) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 9) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 10) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 11) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 12) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 13) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 14) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 15) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 16) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 17) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 18) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 19) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 20) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 21) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 22) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 23) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 24) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 25) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 26) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 27) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 28) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 29) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 30) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 31) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 32) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 33) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 34) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 35) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 36) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 37) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 38) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 39) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 40) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 41) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 42) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 43) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 44) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 45) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 46) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 47) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 48) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 49) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 50) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 51) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 52) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 53) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 54) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 55) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 56) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 57) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 58) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 59) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 60) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 61) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 62) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 63) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 64) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 65) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 66) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 67) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 68) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 69) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 70) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 71) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 72) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 73) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 74) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 75) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 76) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 77) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 78) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 79) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 80) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 81) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 82) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 83) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 84) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 85) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 86) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 87) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 88) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 89) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 90) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 91) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 92) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 93) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 94) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 95) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 96) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 97) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 98) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 99) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 100) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 101) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 102) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 103) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 104) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 105) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 106) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 107) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 108) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 109) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 110) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 111) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 112) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 113) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 114) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 115) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 116) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 117) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 118) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 119) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 120) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 121) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 122) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 123) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 124) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 125) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 126) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 127) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 128) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 129) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 130) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 131) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 132) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 133) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 134) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 135) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 136) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 137) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 138) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 139) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 140) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 141) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 142) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 143) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 144) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 145) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 146) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 147) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 148) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 149) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 150) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 151) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 152) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 153) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 154) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 155) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 156) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 157) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 158) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 159) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 160) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 161) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 162) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 163) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 164) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 165) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 166) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 167) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 168) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 169) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 170) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 171) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 172) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 173) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 174) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 175) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 176) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 177) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 178) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 179) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 180) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 181) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 182) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 183) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 184) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 185) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 186) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 187) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 188) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 189) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 190) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 191) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 192) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 193) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 194) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 195) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 196) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 197) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 198) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 199) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 200) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 201) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 202) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 203) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 204) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 205) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 206) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 207) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 208) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 209) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 210) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 211) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 212) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 213) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 214) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 215) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 216) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 217) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 218) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 219) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 220) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 221) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 222) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 223) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 224) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 225) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 226) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 227) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 228) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 229) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 230) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 231) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 232) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 233) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 234) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 235) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 236) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 237) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 238) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 239) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 240) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 241) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 242) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 243) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 244) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 245) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 246) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 247) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 248) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 249) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 250) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 251) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 252) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 253) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else if (value == 254) { return 0.0; // Поворачивается влево со скоростью более 5°/мин } else if (value == 255) { return 0.0; // Поворачивается вправо со скоростью более 5°/мин } else { // Для значений 1-126: поворот вправо // Для значений 128-255: поворот влево // Формула: ROT = (value - 128) / 4.733 if (value >= 1 && value <= 126) { // Поворот вправо: ROT = value / 4.733 return value / 4.733; } else if (value >= 128 && value <= 255) { // Поворот влево: ROT = -(value - 128) / 4.733 return -(value - 128) / 4.733; } else { return 0.0; // Неопределенное значение } } } /** * Парсит 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 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 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)); // Создаем или обновляем 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)); vessel.updatePosition(latitude, longitude, course, speed); vessel.setHeading(heading); vessel.setPositionAccuracy(accuracy == 1); vessel.setLastUpdate(java.time.LocalDateTime.now()); vessel.setVesselClass("Class B"); // В Class B Position Report размеры не передаются, но мы сохраняем существующие Log.d(TAG, "Class B Position Report - размеры не передаются, сохраняем существующие: L=" + vessel.getLength() + ", W=" + vessel.getWidth()); // Отправляем информацию о корабле на внешний ресурс 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); } } 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)); 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); } 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)); 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); } } 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); } } } catch (Exception e) { Log.e(TAG, "Ошибка декодирования Static Data Report: " + e.getMessage(), e); } } }