From 332ed3ac0add3502f4c965b50dfba6bc41f348a2 Mon Sep 17 00:00:00 2001 From: grigo Date: Wed, 3 Sep 2025 16:36:09 +0300 Subject: [PATCH] Lucky attempt to accomplish --- .../aismap/controllers/NMEAParser.java | 1023 ++++++++--------- 1 file changed, 451 insertions(+), 572 deletions(-) diff --git a/app/src/main/java/com/grigowashere/aismap/controllers/NMEAParser.java b/app/src/main/java/com/grigowashere/aismap/controllers/NMEAParser.java index 51edd9d..10521d0 100644 --- a/app/src/main/java/com/grigowashere/aismap/controllers/NMEAParser.java +++ b/app/src/main/java/com/grigowashere/aismap/controllers/NMEAParser.java @@ -4,8 +4,6 @@ import android.util.Log; import com.grigowashere.aismap.models.Vessel; import com.grigowashere.aismap.models.AISVessel; -import java.util.regex.Pattern; -import java.util.regex.Matcher; import java.util.List; import java.util.ArrayList; @@ -14,55 +12,14 @@ import java.util.ArrayList; * Работает в гибридном режиме: координаты через Location API, остальное через NMEA */ -//TODO: Вместо регулярных выражений использовать дерево разбора +/** + * Контроллер для парсинга NMEA сообщений + * Использует простой разбор по запятым вместо регулярных выражений + */ public class NMEAParser { private static final String TAG = "NMEAParser"; - // Паттерны для NMEA сообщений - private static final Pattern GGA_PATTERN = Pattern.compile( - "\\$G[PN]GGA,(\\d{6}\\.\\d{2}),(\\d{4}\\.\\d+),([NS]),(\\d{5}\\.\\d+),([EW]),(\\d),(\\d+),([^,]*),([^,]*),([^,]*),([^,]*),([^,]*),([^,]*),\\*([0-9A-F]{2})" - ); - - private static final Pattern RMC_PATTERN = Pattern.compile( - "\\$G[PN]RMC,(\\d{6}\\.\\d{2}),([AV][^,]*),([^,]*),([^,]*),([^,]*),([^,]*),([^,]*),([^,]*),(\\d{6}),([^,]*),([^,]*),([^,]*),?([^,]*)?\\*([0-9A-F]{2})" - ); - - private static final Pattern VTG_PATTERN = Pattern.compile( - "\\$G[PN]VTG,([^,]*),T,([^,]*),M,([^,]*),N,([^,]*),K\\*([0-9A-F]{2})" - ); - - private static final Pattern GLL_PATTERN = Pattern.compile( - "\\$G[PN]GLL,(\\d{4}\\.\\d{5}),([NS]),(\\d{5}\\.\\d{5}),([EW]),(\\d{6}),([AV]),([AV])\\*([0-9A-F]{2})" - ); - - private static final Pattern GSV_PATTERN = Pattern.compile( - "\\$G[APNLQ]GSV,(\\d+),(\\d+),(\\d+),(.*)\\*([0-9A-F]{2})" - ); - - private static final Pattern GNS_PATTERN = Pattern.compile( - "\\$GNGNS,(\\d{6}),(\\d{4}\\.\\d{5}),([NS]),(\\d{5}\\.\\d{5}),([EW]),(\\w+),(\\d+),(\\d+\\.\\d+),(\\d+\\.\\d+),([^,]*),([^,]*),([^,]*),([AV])\\*([0-9A-F]{2})" - ); - - // Паттерн для GSA сообщения (DOP и активные спутники) - private static final Pattern GSA_PATTERN = Pattern.compile( - "\\$G[PN]GSA,([AM]),(\\d+),([^,]*),([^,]*),([^,]*),([^,]*),([^,]*),([^,]*),([^,]*),([^,]*),([^,]*),([^,]*),([^,]*),([^,]*),([^,]*),([^,]*)\\*([0-9A-F]{2})" - ); - - // Паттерн для обрезанных GSA сообщений - private static final Pattern GSA_TRUNCATED_PATTERN = Pattern.compile( - "\\$G[PN]GSA,([^,]*),([^,]*),([^,]*)\\*([0-9A-F]{2})" - ); - - // Паттерн для ZDA сообщения (Date and Time) - private static final Pattern ZDA_PATTERN = Pattern.compile( - "\\$G[PN]ZDA,(\\d{6}\\.\\d{2}),(\\d{2}),(\\d{2}),(\\d{4}),(\\d{2}),(\\d{2})\\*([0-9A-F]{2})" - ); - - private static final Pattern AIS_PATTERN = Pattern.compile( - "!AIVDM,(\\d+),(\\d+),([^,]*),([AB12]),([^,]+),(\\d)\\*([0-9A-F]{2})" - ); - private Vessel ownVessel; private List aisVessels; private NMEAParserListener listener; @@ -131,26 +88,56 @@ public class NMEAParser { Log.d(TAG, "Парсим NMEA: " + cleanedSentence); try { - if (cleanedSentence.startsWith("$GPGGA") || cleanedSentence.startsWith("$GNGGA")) { - parseGGA(cleanedSentence); - } else if (cleanedSentence.startsWith("$GPRMC") || cleanedSentence.startsWith("$GNRMC")) { - parseRMC(cleanedSentence); - } else if (cleanedSentence.startsWith("$GPVTG") || cleanedSentence.startsWith("$GNVTG")) { - parseVTG(cleanedSentence); - } else if (cleanedSentence.startsWith("$GPGLL") || cleanedSentence.startsWith("$GNGLL")) { - parseGLL(cleanedSentence); - } else if (cleanedSentence.startsWith("$GPGSV") || cleanedSentence.startsWith("$GAGSV") || cleanedSentence.startsWith("$GLGSV") || cleanedSentence.startsWith("$GBGSV") || cleanedSentence.startsWith("$GNGSA")) { - parseGSV(cleanedSentence); - } else if (cleanedSentence.startsWith("$GNGNS")) { - parseGNS(cleanedSentence); - } else if (cleanedSentence.startsWith("$GPGSA") || cleanedSentence.startsWith("$GNGSA")) { - parseGSA(cleanedSentence); - } else if (cleanedSentence.startsWith("$GPZDA") || cleanedSentence.startsWith("$GNZDA")) { - parseZDA(cleanedSentence); - } else if (cleanedSentence.startsWith("!AIVDM")) { - parseAIS(cleanedSentence); - } else { - Log.d(TAG, "Неподдерживаемый тип NMEA сообщения: " + cleanedSentence); + // Разбираем сообщение по запятым + 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); @@ -160,6 +147,46 @@ public class NMEAParser { } } + /** + * Безопасно получает поле по индексу + */ + 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 сообщение от лишних символов */ @@ -228,583 +255,439 @@ public class NMEAParser { /** * Парсит 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 gga) { -// Log.d(TAG, "Парсим GGA: " + gga); -// Log.d(TAG, "Применяем паттерн GGA: " + GGA_PATTERN.pattern()); - Matcher matcher = GGA_PATTERN.matcher(gga); - if (matcher.matches()) { -// Log.d(TAG, "GGA совпадает с паттерном"); - - int satellites = Integer.parseInt(matcher.group(7)); - - // Обрабатываем высоту - может быть пустым полем (теперь в группе 8) - double altitude = 0.0; - String altitudeStr = matcher.group(8); - if (altitudeStr != null && !altitudeStr.trim().isEmpty()) { - try { - altitude = Double.parseDouble(altitudeStr); - } catch (NumberFormatException e) { - Log.w(TAG, "Не удалось распарсить высоту: '" + altitudeStr + "', используем 0.0"); - altitude = 0.0; - } + 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); } -// Log.d(TAG, String.format("GGA: sat=%d, alt=%.1f", satellites, altitude)); - - // В гибридном режиме не обновляем координаты - if (!hybridMode) { - // Обрабатываем координаты - могут быть пустыми полями (группы 2,3,4,5) - double latitude = 0.0; - double longitude = 0.0; - - String latStr = matcher.group(2); - String latDir = matcher.group(3); - if (latStr != null && !latStr.trim().isEmpty() && latDir != null && !latDir.trim().isEmpty()) { - latitude = parseCoordinate(latStr, latDir.equals("N")); - } - - String lonStr = matcher.group(4); - String lonDir = matcher.group(5); - if (lonStr != null && !lonStr.trim().isEmpty() && lonDir != null && !lonDir.trim().isEmpty()) { - longitude = parseCoordinate(lonStr, lonDir.equals("E")); - } - - 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); - } - } else { -// Log.w(TAG, "GGA не совпадает с паттерном"); + } + + 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 rmc) { - Log.d(TAG, "Парсим RMC: " + rmc); - Log.d(TAG, "Применяем паттерн RMC: " + RMC_PATTERN.pattern()); + private void parseRMC(String[] fields) { + Log.d(TAG, "Парсим RMC с " + fields.length + " полями"); - Matcher matcher = RMC_PATTERN.matcher(rmc); - if (matcher.matches()) { - Log.d(TAG, "RMC совпадает с паттерном"); + // Поле 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"); - // Проверяем статус валидности (группа 2) - String status = matcher.group(2); - boolean isValid = status != null && status.startsWith("A"); - Log.d(TAG, "RMC статус: " + status + " (валидный: " + isValid + ")"); - - // Обрабатываем скорость - может быть пустым полем (группа 7) - double speed = 0.0; - String speedStr = matcher.group(7); - if (speedStr != null && !speedStr.trim().isEmpty()) { - try { - speed = Double.parseDouble(speedStr); - } catch (NumberFormatException e) { - Log.w(TAG, "Не удалось распарсить скорость RMC: '" + speedStr + "', используем 0.0"); - speed = 0.0; - } - } - - // Обрабатываем курс - может быть пустым полем (группа 8) - double course = 0.0; - String courseStr = matcher.group(8); - if (courseStr != null && !courseStr.trim().isEmpty()) { - try { - course = Double.parseDouble(courseStr); - } catch (NumberFormatException e) { - Log.w(TAG, "Не удалось распарсить курс: '" + courseStr + "', используем 0.0"); - course = 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,5,6) - double latitude = 0.0; - double longitude = 0.0; - - String latStr = matcher.group(3); - String latDir = matcher.group(4); - if (latStr != null && !latStr.trim().isEmpty() && latDir != null && !latDir.trim().isEmpty()) { - latitude = parseCoordinate(latStr, latDir.equals("N")); - Log.d(TAG, "RMC широта: " + latStr + " " + latDir + " = " + latitude); - } - - String lonStr = matcher.group(5); - String lonDir = matcher.group(6); - if (lonStr != null && !lonStr.trim().isEmpty() && lonDir != null && !lonDir.trim().isEmpty()) { - longitude = parseCoordinate(lonStr, lonDir.equals("E")); - Log.d(TAG, "RMC долгота: " + lonStr + " " + lonDir + " = " + longitude); - } - - Log.d(TAG, "RMC устанавливаем координаты: lat=" + latitude + ", lon=" + longitude); + // Поля 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); } + } else if (hybridMode) { + Log.d(TAG, "Гибридный режим - координаты из RMC игнорируются"); } else { - Log.w(TAG, "RMC не совпадает с паттерном"); - Log.w(TAG, "Сообщение: '" + rmc + "'"); - Log.w(TAG, "Паттерн: " + RMC_PATTERN.pattern()); + 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 vtg) { - Matcher matcher = VTG_PATTERN.matcher(vtg); - if (matcher.matches()) { - // Обрабатываем курс - может быть пустым полем - double course = 0.0; - String courseStr = matcher.group(2); - if (courseStr != null && !courseStr.trim().isEmpty()) { - try { - course = Double.parseDouble(courseStr); - } catch (NumberFormatException e) { -// Log.w(TAG, "Не удалось распарсить курс VTG: '" + courseStr + "', используем 0.0"); - course = 0.0; - } - } - - // Обрабатываем скорость - может быть пустым полем - double speed = 0.0; - String speedStr = matcher.group(4); - if (speedStr != null && !speedStr.trim().isEmpty()) { - try { - speed = Double.parseDouble(speedStr); - } catch (NumberFormatException e) { -// Log.w(TAG, "Не удалось распарсить скорость VTG: '" + speedStr + "', используем 0.0"); - speed = 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); - } + 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 gll) { + private void parseGLL(String[] fields) { if (hybridMode) { Log.d(TAG, "GLL игнорируется в гибридном режиме"); return; } -// Log.d(TAG, "Парсим GLL: " + gll); - Matcher matcher = GLL_PATTERN.matcher(gll); - if (matcher.matches()) { -// Log.d(TAG, "GLL совпадает с паттерном"); - // Обрабатываем координаты - могут быть пустыми полями - double latitude = 0.0; - double longitude = 0.0; - - String latStr = matcher.group(1); - String latDir = matcher.group(2); - if (latStr != null && !latStr.trim().isEmpty() && latDir != null && !latDir.trim().isEmpty()) { - latitude = parseCoordinate(latStr, latDir.equals("N")); - } - - String lonStr = matcher.group(3); - String lonDir = matcher.group(4); - if (lonStr != null && !lonStr.trim().isEmpty() && lonDir != null && !lonDir.trim().isEmpty()) { - longitude = parseCoordinate(lonStr, lonDir.equals("E")); - } - -// Log.d(TAG, String.format("GLL: lat=%.6f, lon=%.6f", latitude, longitude)); - + 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); - - if (listener != null) { - listener.onVesselUpdated(ownVessel); - } - } else { -// Log.w(TAG, "GLL не совпадает с паттерном"); + } + + 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 gsv) { -// Log.d(TAG, "Парсим GSV: " + gsv); -// Log.d(TAG, "Применяем паттерн GSV: " + GSV_PATTERN.pattern()); - Matcher matcher = GSV_PATTERN.matcher(gsv); - if (matcher.matches()) { -// Log.d(TAG, "GSV совпадает с паттерном"); - int totalMessages = Integer.parseInt(matcher.group(1)); - int messageNumber = Integer.parseInt(matcher.group(2)); - int satellitesInView = Integer.parseInt(matcher.group(3)); - - // Определяем тип системы спутников - String systemType = "Unknown"; - if (gsv.startsWith("$GPGSV")) { - systemType = "GPS"; - } else if (gsv.startsWith("$GLGSV")) { - systemType = "GLONASS"; - } else if (gsv.startsWith("$GAGSV")) { - systemType = "Galileo"; - } else if (gsv.startsWith("$GBGSV")) { - systemType = "BeiDou"; - } else if (gsv.startsWith("$GNGSA")) { - systemType = "GNSS"; - } - -// Log.d(TAG, String.format("GSV [%s]: %d/%d, спутников в поле зрения: %d", -// systemType, messageNumber, totalMessages, satellitesInView)); - - // Парсим данные о спутниках из группы 4 - String satelliteData = matcher.group(4); - if (satelliteData != null && !satelliteData.trim().isEmpty()) { - String[] satFields = satelliteData.split(","); -// Log.d(TAG, String.format("Найдено %d полей данных о спутниках", satFields.length)); - - // Логируем информацию о спутниках (каждые 4 поля = 1 спутник) - for (int i = 0; i < satFields.length; i += 4) { - if (i + 3 < satFields.length) { - String satId = satFields[i]; - String elevation = satFields[i + 1]; - String azimuth = satFields[i + 2]; - String snr = satFields[i + 3]; - - if (!satId.trim().isEmpty()) { -// Log.d(TAG, String.format("Спутник %s: elev=%s, azim=%s, SNR=%s", -// satId, elevation, azimuth, snr)); - } - } - } - } - - // GSV содержит информацию о спутниках, но не обновляет позицию - 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); - } - } - } else { -// Log.w(TAG, "GSV не совпадает с паттерном"); -// Log.d(TAG, "Сообщение: '" + gsv + "'"); -// Log.d(TAG, "Паттерн: " + GSV_PATTERN.pattern()); + 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"; } - } - - /** - * Парсит GNS сообщение (GNSS Fix Data) - * В гибридном режиме используем только количество спутников и высоту - */ - private void parseGNS(String gns) { - Log.d(TAG, "Парсим GNS: " + gns); - Matcher matcher = GNS_PATTERN.matcher(gns); - if (matcher.matches()) { -// Log.d(TAG, "GNS совпадает с паттерном"); - - int satellites = Integer.parseInt(matcher.group(7)); - - // Обрабатываем высоту - может быть пустым полем - double altitude = 0.0; - String altitudeStr = matcher.group(8); - if (altitudeStr != null && !altitudeStr.trim().isEmpty()) { - try { - altitude = Double.parseDouble(altitudeStr); - } catch (NumberFormatException e) { -// Log.w(TAG, "Не удалось распарсить высоту GNS: '" + altitudeStr + "', используем 0.0"); - altitude = 0.0; + + 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)); } } - -// Log.d(TAG, String.format("GNS: sat=%d, alt=%.1f", satellites, altitude)); - - // В гибридном режиме не обновляем координаты - if (!hybridMode) { - // Обрабатываем координаты - могут быть пустыми полями - double latitude = 0.0; - double longitude = 0.0; - - String latStr = matcher.group(2); - String latDir = matcher.group(3); - if (latStr != null && !latStr.trim().isEmpty() && latDir != null && !latDir.trim().isEmpty()) { - latitude = parseCoordinate(latStr, latDir.equals("N")); - } - - String lonStr = matcher.group(4); - String lonDir = matcher.group(5); - if (lonStr != null && !lonStr.trim().isEmpty() && lonDir != null && !lonDir.trim().isEmpty()) { - longitude = parseCoordinate(lonStr, lonDir.equals("E")); - } - - ownVessel.setLatitude(latitude); - ownVessel.setLongitude(longitude); + } + + // Обновляем количество спутников только для последнего сообщения в серии + 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; } - ownVessel.setSatellites(satellites); - ownVessel.setAltitude(altitude); + // Обновляем общее количество спутников + 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); } - } else { -// Log.w(TAG, "GNS не совпадает с паттерном"); + } + } + + /** + * Парсит 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 zda) { - Log.d(TAG, "Парсим ZDA: " + zda); - Matcher matcher = ZDA_PATTERN.matcher(zda); - if (matcher.matches()) { - try { - // Время (HHMMSS.SS) - String timeStr = matcher.group(1); - // День (DD) - int day = Integer.parseInt(matcher.group(2)); - // Месяц (MM) - int month = Integer.parseInt(matcher.group(3)); - // Год (YYYY) - int year = Integer.parseInt(matcher.group(4)); - // Часовой пояс (часы) - int timezoneHours = Integer.parseInt(matcher.group(5)); - // Часовой пояс (минуты) - int timezoneMinutes = Integer.parseInt(matcher.group(6)); - - 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 (NumberFormatException e) { - Log.w(TAG, "Ошибка парсинга ZDA: " + e.getMessage()); + 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); } - } else { - Log.w(TAG, "ZDA не совпадает с паттерном: " + zda); + + } 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 gsa) { - Log.d(TAG, "Парсим GSA: " + gsa); - Matcher matcher = GSA_PATTERN.matcher(gsa); - Matcher truncatedMatcher = GSA_TRUNCATED_PATTERN.matcher(gsa); + private void parseGSA(String[] fields) { + Log.d(TAG, "Парсим GSA с " + fields.length + " полями"); - if (matcher.matches()) { - Log.d(TAG, "GSA совпадает с паттерном"); - - // Подсчитываем активные спутники (непустые поля) - int activeSatellites = 0; - for (int i = 3; i <= 14; i++) { // Группы 3-14 содержат ID спутников - String satId = matcher.group(i); - if (satId != null && !satId.trim().isEmpty() && !satId.equals("0")) { - activeSatellites++; - Log.d(TAG, "Активный спутник: " + satId); - } + // Подсчитываем активные спутники (поля 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; - - String pdopStr = matcher.group(15); // PDOP в группе 15 - if (pdopStr != null && !pdopStr.trim().isEmpty()) { - try { - pdop = Double.parseDouble(pdopStr); - } catch (NumberFormatException e) { - Log.w(TAG, "Не удалось распарсить PDOP: '" + pdopStr + "', используем 0.0"); - } + } + + // Получаем 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); } - - String hdopStr = matcher.group(16); // HDOP в группе 16 - if (hdopStr != null && !hdopStr.trim().isEmpty()) { - try { - hdop = Double.parseDouble(hdopStr); - } catch (NumberFormatException e) { - Log.w(TAG, "Не удалось распарсить HDOP: '" + hdopStr + "', используем 0.0"); - } - } - - String vdopStr = matcher.group(17); // VDOP в группе 17 - if (vdopStr != null && !vdopStr.trim().isEmpty()) { - try { - vdop = Double.parseDouble(vdopStr); - } catch (NumberFormatException e) { - Log.w(TAG, "Не удалось распарсить VDOP: '" + vdopStr + "', используем 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); - } - } else if (truncatedMatcher.matches()) { - Log.d(TAG, "GSA совпадает с обрезанным паттерном"); - - // Обрабатываем обрезанное GSA сообщение - String pdopStr = truncatedMatcher.group(1); - String hdopStr = truncatedMatcher.group(2); - String vdopStr = truncatedMatcher.group(3); - - double pdop = 0.0; - double hdop = 0.0; - double vdop = 0.0; - - try { - if (pdopStr != null && !pdopStr.trim().isEmpty()) { - pdop = Double.parseDouble(pdopStr); - } - if (hdopStr != null && !hdopStr.trim().isEmpty()) { - hdop = Double.parseDouble(hdopStr); - } - if (vdopStr != null && !vdopStr.trim().isEmpty()) { - vdop = Double.parseDouble(vdopStr); - } - } catch (NumberFormatException e) { - Log.w(TAG, "Ошибка парсинга DOP в обрезанном GSA: " + e.getMessage()); - } - - Log.d(TAG, String.format("GSA (обрезанное): PDOP=%.2f, HDOP=%.2f, VDOP=%.2f", pdop, hdop, vdop)); - - // Обновляем DOP значения - ownVessel.setPdop(pdop); - ownVessel.setHdop(hdop); - ownVessel.setVdop(vdop); - - // Отправляем DOP значения в GPS Location Listener - if (gpsLocationListener != null) { - gpsLocationListener.setDOPValues(pdop, hdop, vdop); - } - - // Уведомляем слушателя о DOP - if (listener != null) { - listener.onDOPUpdated(pdop, hdop, vdop); - listener.onVesselUpdated(ownVessel); - } - } else { - Log.w(TAG, "GSA не совпадает ни с одним паттерном"); - Log.w(TAG, "Сообщение: '" + gsa + "'"); - Log.w(TAG, "Паттерн: " + GSA_PATTERN.pattern()); - Log.w(TAG, "Обрезанный паттерн: " + GSA_TRUNCATED_PATTERN.pattern()); + } + + 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) { - Matcher matcher = AIS_PATTERN.matcher(ais); - if (matcher.matches()) { - try { - int totalFragments = Integer.parseInt(matcher.group(1)); - int fragmentNumber = Integer.parseInt(matcher.group(2)); - String sequenceId = matcher.group(3); - String channel = matcher.group(4); - String payload = matcher.group(5); - int fillBits = Integer.parseInt(matcher.group(6)); - String checksum = matcher.group(7); + Log.d(TAG, "Парсим AIS: " + ais); + + // Разбираем AIS сообщение по запятым + String[] fields = ais.split(","); + 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: количество бит заполнения + int fillBits = parseIntField(fields, 6, 0); + + // Контрольная сумма находится в последнем поле после * + 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)); @@ -819,26 +702,22 @@ public class NMEAParser { if (payload != null && !payload.trim().isEmpty()) { if (totalFragments == 1) { // Одноканальное сообщение - декодируем сразу - decodeAISPayload(payload, channel.equals("A") ? 0 : 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.equals("A") ? 0 : 1); + collectAISFragments(actualSequenceId, fragmentNumber, totalFragments, payload, channel != null && channel.equals("A") ? 0 : 1); } } else { Log.w(TAG, "AIS payload пустой, пропускаем сообщение"); } - } catch (NumberFormatException e) { - Log.e(TAG, "Ошибка парсинга AIS сообщения: " + e.getMessage() + " для сообщения: " + ais); - if (listener != null) { - listener.onParseError("Ошибка парсинга AIS: " + e.getMessage()); - } + } catch (Exception e) { + Log.e(TAG, "Ошибка парсинга AIS сообщения: " + e.getMessage() + " для сообщения: " + ais); + if (listener != null) { + listener.onParseError("Ошибка парсинга AIS: " + e.getMessage()); } - } else { - Log.w(TAG, "AIS сообщение не соответствует паттерну: " + ais); - Log.d(TAG, "Паттерн: " + AIS_PATTERN.pattern()); } }