Lucky attempt to accomplish

This commit is contained in:
2025-09-03 16:36:09 +03:00
parent a9db35543b
commit 332ed3ac0a
@@ -4,8 +4,6 @@ import android.util.Log;
import com.grigowashere.aismap.models.Vessel; import com.grigowashere.aismap.models.Vessel;
import com.grigowashere.aismap.models.AISVessel; import com.grigowashere.aismap.models.AISVessel;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.util.List; import java.util.List;
import java.util.ArrayList; import java.util.ArrayList;
@@ -14,55 +12,14 @@ import java.util.ArrayList;
* Работает в гибридном режиме: координаты через Location API, остальное через NMEA * Работает в гибридном режиме: координаты через Location API, остальное через NMEA
*/ */
//TODO: Вместо регулярных выражений использовать дерево разбора /**
* Контроллер для парсинга NMEA сообщений
* Использует простой разбор по запятым вместо регулярных выражений
*/
public class NMEAParser { public class NMEAParser {
private static final String TAG = "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 Vessel ownVessel;
private List<AISVessel> aisVessels; private List<AISVessel> aisVessels;
private NMEAParserListener listener; private NMEAParserListener listener;
@@ -131,26 +88,56 @@ public class NMEAParser {
Log.d(TAG, "Парсим NMEA: " + cleanedSentence); Log.d(TAG, "Парсим NMEA: " + cleanedSentence);
try { try {
if (cleanedSentence.startsWith("$GPGGA") || cleanedSentence.startsWith("$GNGGA")) { // Разбираем сообщение по запятым
parseGGA(cleanedSentence); String[] fields = cleanedSentence.split(",");
} else if (cleanedSentence.startsWith("$GPRMC") || cleanedSentence.startsWith("$GNRMC")) { if (fields.length < 2) {
parseRMC(cleanedSentence); Log.w(TAG, "NMEA сообщение слишком короткое: " + cleanedSentence);
} else if (cleanedSentence.startsWith("$GPVTG") || cleanedSentence.startsWith("$GNVTG")) { return;
parseVTG(cleanedSentence); }
} else if (cleanedSentence.startsWith("$GPGLL") || cleanedSentence.startsWith("$GNGLL")) {
parseGLL(cleanedSentence); // Извлекаем приамбуду (первые 6 символов после $)
} else if (cleanedSentence.startsWith("$GPGSV") || cleanedSentence.startsWith("$GAGSV") || cleanedSentence.startsWith("$GLGSV") || cleanedSentence.startsWith("$GBGSV") || cleanedSentence.startsWith("$GNGSA")) { String preamble = fields[0];
parseGSV(cleanedSentence); if (preamble.length() < 6) {
} else if (cleanedSentence.startsWith("$GNGNS")) { Log.w(TAG, "Некорректная приамбула: " + preamble);
parseGNS(cleanedSentence); return;
} else if (cleanedSentence.startsWith("$GPGSA") || cleanedSentence.startsWith("$GNGSA")) { }
parseGSA(cleanedSentence);
} else if (cleanedSentence.startsWith("$GPZDA") || cleanedSentence.startsWith("$GNZDA")) { // Определяем тип сообщения по последним трем символам приамбуды
parseZDA(cleanedSentence); String messageType = preamble.substring(preamble.length() - 3);
} else if (cleanedSentence.startsWith("!AIVDM")) {
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); parseAIS(cleanedSentence);
} else { } else {
Log.d(TAG, "Неподдерживаемый тип NMEA сообщения: " + cleanedSentence); Log.d(TAG, "Неподдерживаемый тип NMEA сообщения: " + messageType);
}
break;
} }
} catch (Exception e) { } catch (Exception e) {
Log.e(TAG, "Ошибка парсинга NMEA: " + e.getMessage(), 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 сообщение от лишних символов * Очищает NMEA сообщение от лишних символов
*/ */
@@ -228,51 +255,37 @@ public class NMEAParser {
/** /**
* Парсит GGA сообщение (Global Positioning System Fix Data) * Парсит 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) { private void parseGGA(String[] fields) {
// Log.d(TAG, "Парсим GGA: " + gga); Log.d(TAG, "Парсим GGA с " + fields.length + " полями");
// 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)); // Поле 7: количество спутников
int satellites = parseIntField(fields, 7, 0);
// Обрабатываем высоту - может быть пустым полем (теперь в группе 8) // Поле 9: высота над эллипсоидом
double altitude = 0.0; double altitude = parseDoubleField(fields, 9, 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;
}
}
// Log.d(TAG, String.format("GGA: sat=%d, alt=%.1f", satellites, altitude)); Log.d(TAG, String.format("GGA: sat=%d, alt=%.1f", satellites, altitude));
// В гибридном режиме не обновляем координаты // В гибридном режиме не обновляем координаты
if (!hybridMode) { if (!hybridMode) {
// Обрабатываем координаты - могут быть пустыми полями (группы 2,3,4,5) // Поля 2,3: широта и направление
double latitude = 0.0; String latStr = getField(fields, 2);
double longitude = 0.0; String latDir = getField(fields, 3);
if (latStr != null && latDir != null) {
String latStr = matcher.group(2); double latitude = parseCoordinate(latStr, latDir.equals("N"));
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.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.setLongitude(longitude);
} }
}
ownVessel.setSatellites(satellites); ownVessel.setSatellites(satellites);
ownVessel.setAltitude(altitude); ownVessel.setAltitude(altitude);
@@ -285,78 +298,50 @@ public class NMEAParser {
if (listener != null) { if (listener != null) {
listener.onVesselUpdated(ownVessel); listener.onVesselUpdated(ownVessel);
} }
} else {
// Log.w(TAG, "GGA не совпадает с паттерном");
}
} }
/** /**
* Парсит RMC сообщение (Recommended Minimum Navigation Information) * Парсит 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) { private void parseRMC(String[] fields) {
Log.d(TAG, "Парсим RMC: " + rmc); Log.d(TAG, "Парсим RMC с " + fields.length + " полями");
Log.d(TAG, "Применяем паттерн RMC: " + RMC_PATTERN.pattern());
Matcher matcher = RMC_PATTERN.matcher(rmc); // Поле 2: статус валидности (A = валидный, V = невалидный)
if (matcher.matches()) { String status = getField(fields, 2);
Log.d(TAG, "RMC совпадает с паттерном");
// Проверяем статус валидности (группа 2)
String status = matcher.group(2);
boolean isValid = status != null && status.startsWith("A"); boolean isValid = status != null && status.startsWith("A");
Log.d(TAG, "RMC статус: " + status + " (валидный: " + isValid + ")"); Log.d(TAG, "RMC статус: " + status + " (валидный: " + isValid + ")");
// Обрабатываем скорость - может быть пустым полем (группа 7) // Поле 7: скорость в узлах
double speed = 0.0; double speed = parseDoubleField(fields, 7, 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) // Поле 8: курс в градусах
double course = 0.0; double course = parseDoubleField(fields, 8, 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)); Log.d(TAG, String.format("RMC: speed=%.1f, course=%.1f, valid=%s", speed, course, isValid));
// В гибридном режиме не обновляем координаты // В гибридном режиме не обновляем координаты
if (!hybridMode && isValid) { if (!hybridMode && isValid) {
Log.d(TAG, "Режим НЕ гибридный - обрабатываем координаты из RMC"); Log.d(TAG, "Режим НЕ гибридный - обрабатываем координаты из RMC");
// Обрабатываем координаты - могут быть пустыми полями (группы 3,4,5,6)
double latitude = 0.0;
double longitude = 0.0;
String latStr = matcher.group(3); // Поля 3,4: широта и направление
String latDir = matcher.group(4); String latStr = getField(fields, 3);
if (latStr != null && !latStr.trim().isEmpty() && latDir != null && !latDir.trim().isEmpty()) { String latDir = getField(fields, 4);
latitude = parseCoordinate(latStr, latDir.equals("N")); if (latStr != null && latDir != null) {
double latitude = parseCoordinate(latStr, latDir.equals("N"));
Log.d(TAG, "RMC широта: " + latStr + " " + latDir + " = " + latitude); 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);
ownVessel.setLatitude(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); ownVessel.setLongitude(longitude);
}
} else if (hybridMode) { } else if (hybridMode) {
Log.d(TAG, "Гибридный режим - координаты из RMC игнорируются"); Log.d(TAG, "Гибридный режим - координаты из RMC игнорируются");
} else { } else {
@@ -377,44 +362,22 @@ public class NMEAParser {
if (listener != null) { if (listener != null) {
listener.onVesselUpdated(ownVessel); listener.onVesselUpdated(ownVessel);
} }
} else {
Log.w(TAG, "RMC не совпадает с паттерном");
Log.w(TAG, "Сообщение: '" + rmc + "'");
Log.w(TAG, "Паттерн: " + RMC_PATTERN.pattern());
}
} }
/** /**
* Парсит VTG сообщение (Course Over Ground and Ground Speed) * Парсит VTG сообщение (Course Over Ground and Ground Speed)
* Формат: $GPVTG,course,T,course,M,speed,N,speed,K,mode*checksum
*/ */
private void parseVTG(String vtg) { private void parseVTG(String[] fields) {
Matcher matcher = VTG_PATTERN.matcher(vtg); Log.d(TAG, "Парсим VTG с " + fields.length + " полями");
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;
}
}
// Обрабатываем скорость - может быть пустым полем // Поле 1: курс в градусах (True)
double speed = 0.0; double course = parseDoubleField(fields, 1, 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)); // Поле 5: скорость в узлах
double speed = parseDoubleField(fields, 5, 0.0);
Log.d(TAG, String.format("VTG: course=%.1f, speed=%.1f", course, speed));
ownVessel.setCourse(course); ownVessel.setCourse(course);
ownVessel.setSpeed(speed); ownVessel.setSpeed(speed);
@@ -423,104 +386,87 @@ public class NMEAParser {
listener.onVesselUpdated(ownVessel); listener.onVesselUpdated(ownVessel);
} }
} }
}
/** /**
* Парсит GLL сообщение (Geographic Position - Latitude/Longitude) * Парсит 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) { if (hybridMode) {
Log.d(TAG, "GLL игнорируется в гибридном режиме"); Log.d(TAG, "GLL игнорируется в гибридном режиме");
return; return;
} }
// Log.d(TAG, "Парсим GLL: " + gll); Log.d(TAG, "Парсим GLL с " + fields.length + " полями");
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));
// Поля 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); 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); ownVessel.setLongitude(longitude);
}
Log.d(TAG, String.format("GLL: lat=%.6f, lon=%.6f", ownVessel.getLatitude(), ownVessel.getLongitude()));
if (listener != null) { if (listener != null) {
listener.onVesselUpdated(ownVessel); listener.onVesselUpdated(ownVessel);
} }
} else {
// Log.w(TAG, "GLL не совпадает с паттерном");
}
} }
/** /**
* Парсит GSV сообщение (GPS Satellites in View) * Парсит GSV сообщение (GPS Satellites in View)
* Формат: $GPGSV,totalMsgs,msgNum,totalSats,satId1,elev1,azim1,snr1,satId2,elev2,azim2,snr2,...*checksum
*/ */
private void parseGSV(String gsv) { private void parseGSV(String[] fields) {
// Log.d(TAG, "Парсим GSV: " + gsv); Log.d(TAG, "Парсим GSV с " + fields.length + " полями");
// 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));
// Определяем тип системы спутников // Поля 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 systemType = "Unknown";
if (gsv.startsWith("$GPGSV")) { String preamble = fields[0];
if (preamble.startsWith("$GPGSV")) {
systemType = "GPS"; systemType = "GPS";
} else if (gsv.startsWith("$GLGSV")) { } else if (preamble.startsWith("$GLGSV")) {
systemType = "GLONASS"; systemType = "GLONASS";
} else if (gsv.startsWith("$GAGSV")) { } else if (preamble.startsWith("$GAGSV")) {
systemType = "Galileo"; systemType = "Galileo";
} else if (gsv.startsWith("$GBGSV")) { } else if (preamble.startsWith("$GBGSV")) {
systemType = "BeiDou"; systemType = "BeiDou";
} else if (gsv.startsWith("$GNGSA")) {
systemType = "GNSS";
} }
// Log.d(TAG, String.format("GSV [%s]: %d/%d, спутников в поле зрения: %d", Log.d(TAG, String.format("GSV [%s]: %d/%d, спутников в поле зрения: %d",
// systemType, messageNumber, totalMessages, satellitesInView)); systemType, messageNumber, totalMessages, satellitesInView));
// Парсим данные о спутниках из группы 4 // Парсим данные о спутниках (начиная с поля 4, каждые 4 поля = 1 спутник)
String satelliteData = matcher.group(4); for (int i = 4; i < fields.length - 1; i += 4) { // -1 чтобы исключить контрольную сумму
if (satelliteData != null && !satelliteData.trim().isEmpty()) { if (i + 3 < fields.length) {
String[] satFields = satelliteData.split(","); String satId = getField(fields, i);
// Log.d(TAG, String.format("Найдено %d полей данных о спутниках", satFields.length)); String elevation = getField(fields, i + 1);
String azimuth = getField(fields, i + 2);
String snr = getField(fields, i + 3);
// Логируем информацию о спутниках (каждые 4 поля = 1 спутник) if (satId != null) {
for (int i = 0; i < satFields.length; i += 4) { Log.d(TAG, String.format("Спутник %s: elev=%s, azim=%s, SNR=%s",
if (i + 3 < satFields.length) { satId, elevation, azimuth, snr));
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) { if (messageNumber == totalMessages) {
// Обновляем количество спутников для соответствующей системы // Обновляем количество спутников для соответствующей системы
switch (systemType) { switch (systemType) {
@@ -548,67 +494,49 @@ public class NMEAParser {
gpsLocationListener.setSatellitesInVessel(ownVessel); gpsLocationListener.setSatellitesInVessel(ownVessel);
} }
// Log.d(TAG, String.format("GSV [%s] завершен: %d спутников. Общий счет: GPS=%d, GLONASS=%d, Galileo=%d, Всего=%d", Log.d(TAG, String.format("GSV [%s] завершен: %d спутников. Общий счет: GPS=%d, GLONASS=%d, Galileo=%d, Всего=%d",
// systemType, satellitesInView, gpsSatellites, glonassSatellites, galileoSatellites, totalSatellites)); systemType, satellitesInView, gpsSatellites, glonassSatellites, galileoSatellites, totalSatellites));
if (listener != null) { if (listener != null) {
listener.onVesselUpdated(ownVessel); listener.onVesselUpdated(ownVessel);
} }
} }
} else {
// Log.w(TAG, "GSV не совпадает с паттерном");
// Log.d(TAG, "Сообщение: '" + gsv + "'");
// Log.d(TAG, "Паттерн: " + GSV_PATTERN.pattern());
}
} }
/** /**
* Парсит GNS сообщение (GNSS Fix Data) * Парсит 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 gns) { private void parseGNS(String[] fields) {
Log.d(TAG, "Парсим GNS: " + gns); Log.d(TAG, "Парсим GNS с " + fields.length + " полями");
Matcher matcher = GNS_PATTERN.matcher(gns);
if (matcher.matches()) {
// Log.d(TAG, "GNS совпадает с паттерном");
int satellites = Integer.parseInt(matcher.group(7)); // Поле 7: количество спутников
int satellites = parseIntField(fields, 7, 0);
// Обрабатываем высоту - может быть пустым полем // Поле 9: высота над эллипсоидом
double altitude = 0.0; double altitude = parseDoubleField(fields, 9, 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("GNS: sat=%d, alt=%.1f", satellites, altitude)); Log.d(TAG, String.format("GNS: sat=%d, alt=%.1f", satellites, altitude));
// В гибридном режиме не обновляем координаты // В гибридном режиме не обновляем координаты
if (!hybridMode) { if (!hybridMode) {
// Обрабатываем координаты - могут быть пустыми полями // Поля 2,3: широта и направление
double latitude = 0.0; String latStr = getField(fields, 2);
double longitude = 0.0; String latDir = getField(fields, 3);
if (latStr != null && latDir != null) {
String latStr = matcher.group(2); double latitude = parseCoordinate(latStr, latDir.equals("N"));
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.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.setLongitude(longitude);
} }
}
ownVessel.setSatellites(satellites); ownVessel.setSatellites(satellites);
ownVessel.setAltitude(altitude); ownVessel.setAltitude(altitude);
@@ -621,31 +549,27 @@ public class NMEAParser {
if (listener != null) { if (listener != null) {
listener.onVesselUpdated(ownVessel); listener.onVesselUpdated(ownVessel);
} }
} else {
// Log.w(TAG, "GNS не совпадает с паттерном");
}
} }
/** /**
* Парсит ZDA сообщение (Date and Time) * Парсит ZDA сообщение (Date and Time)
* Формат: $GPZDA,time,day,month,year,timezoneHours,timezoneMinutes*checksum
*/ */
private void parseZDA(String zda) { private void parseZDA(String[] fields) {
Log.d(TAG, "Парсим ZDA: " + zda); Log.d(TAG, "Парсим ZDA с " + fields.length + " полями");
Matcher matcher = ZDA_PATTERN.matcher(zda);
if (matcher.matches()) {
try { try {
// Время (HHMMSS.SS) // Поле 1: время (HHMMSS.SS)
String timeStr = matcher.group(1); String timeStr = getField(fields, 1);
// День (DD)
int day = Integer.parseInt(matcher.group(2)); // Поля 2,3,4: день, месяц, год
// Месяц (MM) int day = parseIntField(fields, 2, 0);
int month = Integer.parseInt(matcher.group(3)); int month = parseIntField(fields, 3, 0);
// Год (YYYY) int year = parseIntField(fields, 4, 0);
int year = Integer.parseInt(matcher.group(4));
// Часовой пояс (часы) // Поля 5,6: часовой пояс (часы и минуты)
int timezoneHours = Integer.parseInt(matcher.group(5)); int timezoneHours = parseIntField(fields, 5, 0);
// Часовой пояс (минуты) int timezoneMinutes = parseIntField(fields, 6, 0);
int timezoneMinutes = Integer.parseInt(matcher.group(6));
Log.d(TAG, String.format("ZDA: %04d-%02d-%02d %s, TZ: %+03d:%02d", Log.d(TAG, String.format("ZDA: %04d-%02d-%02d %s, TZ: %+03d:%02d",
year, month, day, timeStr, timezoneHours, timezoneMinutes)); year, month, day, timeStr, timezoneHours, timezoneMinutes));
@@ -657,65 +581,47 @@ public class NMEAParser {
listener.onVesselUpdated(ownVessel); listener.onVesselUpdated(ownVessel);
} }
} catch (NumberFormatException e) { } catch (Exception e) {
Log.w(TAG, "Ошибка парсинга ZDA: " + e.getMessage()); Log.w(TAG, "Ошибка парсинга ZDA: " + e.getMessage());
} }
} else {
Log.w(TAG, "ZDA не совпадает с паттерном: " + zda);
}
} }
/** /**
* Парсит GSA сообщение (GPS DOP and Active Satellites) * Парсит GSA сообщение (GPS DOP and Active Satellites)
* КЛЮЧЕВОЕ сообщение для получения DOP и активных спутников * КЛЮЧЕВОЕ сообщение для получения DOP и активных спутников
* Формат: $GPGSA,mode,fixType,sat1,sat2,...,sat12,PDOP,HDOP,VDOP*checksum
*/ */
private void parseGSA(String gsa) { private void parseGSA(String[] fields) {
Log.d(TAG, "Парсим GSA: " + gsa); Log.d(TAG, "Парсим GSA с " + fields.length + " полями");
Matcher matcher = GSA_PATTERN.matcher(gsa);
Matcher truncatedMatcher = GSA_TRUNCATED_PATTERN.matcher(gsa);
if (matcher.matches()) { // Подсчитываем активные спутники (поля 3-14 содержат ID спутников)
Log.d(TAG, "GSA совпадает с паттерном");
// Подсчитываем активные спутники (непустые поля)
int activeSatellites = 0; int activeSatellites = 0;
for (int i = 3; i <= 14; i++) { // Группы 3-14 содержат ID спутников for (int i = 3; i <= 14 && i < fields.length; i++) {
String satId = matcher.group(i); String satId = getField(fields, i);
if (satId != null && !satId.trim().isEmpty() && !satId.equals("0")) { if (satId != null && !satId.equals("0")) {
activeSatellites++; activeSatellites++;
Log.d(TAG, "Активный спутник: " + satId); Log.d(TAG, "Активный спутник: " + satId);
} }
} }
// Получаем DOP значения - могут быть пустыми полями // Получаем DOP значения - могут быть в разных позициях в зависимости от количества полей
double pdop = 0.0; double pdop = 0.0;
double hdop = 0.0; double hdop = 0.0;
double vdop = 0.0; double vdop = 0.0;
String pdopStr = matcher.group(15); // PDOP в группе 15 // DOP значения обычно в последних полях перед контрольной суммой
if (pdopStr != null && !pdopStr.trim().isEmpty()) { if (fields.length >= 17) {
try { // Полное GSA сообщение
pdop = Double.parseDouble(pdopStr); pdop = parseDoubleField(fields, 15, 0.0); // PDOP
} catch (NumberFormatException e) { hdop = parseDoubleField(fields, 16, 0.0); // HDOP
Log.w(TAG, "Не удалось распарсить PDOP: '" + pdopStr + "', используем 0.0"); vdop = parseDoubleField(fields, 17, 0.0); // VDOP
} } else if (fields.length >= 6) {
} // Обрезанное GSA сообщение - DOP в последних полях
int dopStartIndex = fields.length - 4; // -4 чтобы исключить контрольную сумму
String hdopStr = matcher.group(16); // HDOP в группе 16 if (dopStartIndex >= 3) {
if (hdopStr != null && !hdopStr.trim().isEmpty()) { pdop = parseDoubleField(fields, dopStartIndex, 0.0);
try { hdop = parseDoubleField(fields, dopStartIndex + 1, 0.0);
hdop = Double.parseDouble(hdopStr); vdop = parseDoubleField(fields, dopStartIndex + 2, 0.0);
} 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");
} }
} }
@@ -740,71 +646,48 @@ public class NMEAParser {
listener.onDOPUpdated(pdop, hdop, vdop); listener.onDOPUpdated(pdop, hdop, vdop);
listener.onVesselUpdated(ownVessel); 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());
}
} }
/** /**
* Парсит AIS сообщение (Automatic Identification System) * Парсит AIS сообщение (Automatic Identification System)
* Формат: !AIVDM,totalFragments,fragmentNumber,sequenceId,channel,payload,fillBits*checksum
*/ */
private void parseAIS(String ais) { private void parseAIS(String ais) {
Matcher matcher = AIS_PATTERN.matcher(ais); Log.d(TAG, "Парсим AIS: " + ais);
if (matcher.matches()) {
// Разбираем AIS сообщение по запятым
String[] fields = ais.split(",");
if (fields.length < 7) {
Log.w(TAG, "AIS сообщение слишком короткое: " + ais);
return;
}
try { try {
int totalFragments = Integer.parseInt(matcher.group(1)); // Поля 1,2: общее количество фрагментов, номер фрагмента
int fragmentNumber = Integer.parseInt(matcher.group(2)); int totalFragments = parseIntField(fields, 1, 1);
String sequenceId = matcher.group(3); int fragmentNumber = parseIntField(fields, 2, 1);
String channel = matcher.group(4);
String payload = matcher.group(5); // Поле 3: ID последовательности
int fillBits = Integer.parseInt(matcher.group(6)); String sequenceId = getField(fields, 3);
String checksum = matcher.group(7);
// Поле 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'", 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)); fragmentNumber, totalFragments, sequenceId, channel, payload, fillBits, checksum));
@@ -819,27 +702,23 @@ public class NMEAParser {
if (payload != null && !payload.trim().isEmpty()) { if (payload != null && !payload.trim().isEmpty()) {
if (totalFragments == 1) { if (totalFragments == 1) {
// Одноканальное сообщение - декодируем сразу // Одноканальное сообщение - декодируем сразу
decodeAISPayload(payload, channel.equals("A") ? 0 : 1); decodeAISPayload(payload, channel != null && channel.equals("A") ? 0 : 1);
} else { } else {
// Многочастное сообщение - собираем фрагменты // Многочастное сообщение - собираем фрагменты
// Используем номер фрагмента как sequenceId если поле пустое // Используем номер фрагмента как sequenceId если поле пустое
String actualSequenceId = (sequenceId != null && !sequenceId.trim().isEmpty()) ? String actualSequenceId = (sequenceId != null && !sequenceId.trim().isEmpty()) ?
sequenceId : String.valueOf(fragmentNumber); 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 { } else {
Log.w(TAG, "AIS payload пустой, пропускаем сообщение"); Log.w(TAG, "AIS payload пустой, пропускаем сообщение");
} }
} catch (NumberFormatException e) { } catch (Exception e) {
Log.e(TAG, "Ошибка парсинга AIS сообщения: " + e.getMessage() + " для сообщения: " + ais); Log.e(TAG, "Ошибка парсинга AIS сообщения: " + e.getMessage() + " для сообщения: " + ais);
if (listener != null) { if (listener != null) {
listener.onParseError("Ошибка парсинга AIS: " + e.getMessage()); listener.onParseError("Ошибка парсинга AIS: " + e.getMessage());
} }
} }
} else {
Log.w(TAG, "AIS сообщение не соответствует паттерну: " + ais);
Log.d(TAG, "Паттерн: " + AIS_PATTERN.pattern());
}
} }
/** /**