generated from Grigo/AndroidTemplate
Created ship vectors (not added yet)
Created menu Created udp support Created DockWidgets for compass and SOG/COG
This commit is contained in:
@@ -23,7 +23,7 @@ public class NMEAParser {
|
||||
);
|
||||
|
||||
private static final Pattern RMC_PATTERN = Pattern.compile(
|
||||
"\\$G[PN]RMC,(\\d{6}\\.\\d{2}),([AV]),(\\d{4}\\.\\d+),([NS]),(\\d{5}\\.\\d+),([EW]),([^,]*),([^,]*),(\\d{6}),([^,]*),([^,]*),([^,]*),([^,]*)\\*([0-9A-F]{2})"
|
||||
"\\$G[PN]RMC,(\\d{6}\\.\\d{2}),([AV][^,]*),([^,]*),([^,]*),([^,]*),([^,]*),([^,]*),([^,]*),(\\d{6}),([^,]*),([^,]*),([^,]*),?([^,]*)?\\*([0-9A-F]{2})"
|
||||
);
|
||||
|
||||
private static final Pattern VTG_PATTERN = Pattern.compile(
|
||||
@@ -44,7 +44,17 @@ public class NMEAParser {
|
||||
|
||||
// Паттерн для GSA сообщения (DOP и активные спутники)
|
||||
private static final Pattern GSA_PATTERN = Pattern.compile(
|
||||
"\\$G[PN]GSA,([AM]),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+),([^,]*),([^,]*),([^,]*)\\*([0-9A-F]{2})"
|
||||
"\\$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(
|
||||
@@ -97,7 +107,9 @@ public class NMEAParser {
|
||||
*/
|
||||
public void setHybridMode(boolean enabled) {
|
||||
this.hybridMode = enabled;
|
||||
Log.i(TAG, "Гибридный режим: " + (enabled ? "включен" : "отключен"));
|
||||
Log.i(TAG, "🔄 Гибридный режим: " + (enabled ? "включен" : "отключен"));
|
||||
Log.i(TAG, "📍 В режиме " + (enabled ? "гибридном" : "только NMEA") + " координаты будут " +
|
||||
(enabled ? "браться из Android GPS API" : "браться из NMEA сообщений"));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,6 +122,10 @@ public class NMEAParser {
|
||||
|
||||
// Очищаем сообщение от лишних символов
|
||||
String cleanedSentence = cleanNMEASentence(nmeaSentence);
|
||||
if (cleanedSentence == null) {
|
||||
Log.w(TAG, "NMEA сообщение не удалось очистить или слишком короткое: " + nmeaSentence);
|
||||
return;
|
||||
}
|
||||
Log.d(TAG, "Парсим NMEA: " + cleanedSentence);
|
||||
|
||||
try {
|
||||
@@ -121,12 +137,14 @@ public class NMEAParser {
|
||||
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("$GNGSA")) {
|
||||
} 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 {
|
||||
@@ -144,17 +162,57 @@ public class NMEAParser {
|
||||
* Очищает NMEA сообщение от лишних символов
|
||||
*/
|
||||
private String cleanNMEASentence(String sentence) {
|
||||
if (sentence == null) {
|
||||
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) {
|
||||
cleaned = cleaned.substring(0, asteriskIndex + 3); // включаем * и 2 символа контрольной суммы
|
||||
// Проверяем, что после * есть достаточно символов для контрольной суммы
|
||||
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); // включаем только *
|
||||
}
|
||||
}
|
||||
|
||||
// Убираем все непечатаемые символы
|
||||
@@ -235,41 +293,47 @@ public class NMEAParser {
|
||||
* В гибридном режиме используем только курс и скорость
|
||||
*/
|
||||
private void parseRMC(String rmc) {
|
||||
// Log.d(TAG, "Парсим RMC: " + rmc);
|
||||
// Log.d(TAG, "Применяем паттерн RMC: " + RMC_PATTERN.pattern());
|
||||
Log.d(TAG, "Парсим RMC: " + rmc);
|
||||
Log.d(TAG, "Применяем паттерн RMC: " + RMC_PATTERN.pattern());
|
||||
|
||||
Matcher matcher = RMC_PATTERN.matcher(rmc);
|
||||
if (matcher.matches()) {
|
||||
// Log.d(TAG, "RMC совпадает с паттерном");
|
||||
Log.d(TAG, "RMC совпадает с паттерном");
|
||||
|
||||
// Обрабатываем скорость - может быть пустым полем (теперь в группе 7)
|
||||
// Проверяем статус валидности (группа 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");
|
||||
Log.w(TAG, "Не удалось распарсить скорость RMC: '" + speedStr + "', используем 0.0");
|
||||
speed = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
// Обрабатываем курс - может быть пустым полем (теперь в группе 8)
|
||||
// Обрабатываем курс - может быть пустым полем (группа 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");
|
||||
Log.w(TAG, "Не удалось распарсить курс: '" + courseStr + "', используем 0.0");
|
||||
course = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
// Log.d(TAG, String.format("RMC: speed=%.1f, course=%.1f", speed, course));
|
||||
Log.d(TAG, String.format("RMC: speed=%.1f, course=%.1f, valid=%s", speed, course, isValid));
|
||||
|
||||
// В гибридном режиме не обновляем координаты
|
||||
if (!hybridMode) {
|
||||
if (!hybridMode && isValid) {
|
||||
Log.d(TAG, "Режим НЕ гибридный - обрабатываем координаты из RMC");
|
||||
// Обрабатываем координаты - могут быть пустыми полями (группы 3,4,5,6)
|
||||
double latitude = 0.0;
|
||||
double longitude = 0.0;
|
||||
@@ -278,26 +342,43 @@ public class NMEAParser {
|
||||
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);
|
||||
ownVessel.setLatitude(latitude);
|
||||
ownVessel.setLongitude(longitude);
|
||||
} else if (hybridMode) {
|
||||
Log.d(TAG, "Гибридный режим - координаты из RMC игнорируются");
|
||||
} else {
|
||||
Log.d(TAG, "RMC данные невалидны (статус V) - координаты не обновляем");
|
||||
}
|
||||
|
||||
ownVessel.setSpeed(speed);
|
||||
ownVessel.setCourse(course);
|
||||
// Обновляем скорость и курс только если данные валидны
|
||||
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 {
|
||||
// Log.w(TAG, "RMC не совпадает с паттерном");
|
||||
Log.w(TAG, "RMC не совпадает с паттерном");
|
||||
Log.w(TAG, "Сообщение: '" + rmc + "'");
|
||||
Log.w(TAG, "Паттерн: " + RMC_PATTERN.pattern());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,6 +487,8 @@ public class NMEAParser {
|
||||
systemType = "GLONASS";
|
||||
} else if (gsv.startsWith("$GAGSV")) {
|
||||
systemType = "Galileo";
|
||||
} else if (gsv.startsWith("$GBGSV")) {
|
||||
systemType = "BeiDou";
|
||||
} else if (gsv.startsWith("$GNGSA")) {
|
||||
systemType = "GNSS";
|
||||
}
|
||||
@@ -448,6 +531,10 @@ public class NMEAParser {
|
||||
case "Galileo":
|
||||
galileoSatellites = satellitesInView;
|
||||
break;
|
||||
case "BeiDou":
|
||||
// Пока не добавляем отдельный счетчик для BeiDou, считаем как GPS
|
||||
gpsSatellites = Math.max(gpsSatellites, satellitesInView);
|
||||
break;
|
||||
}
|
||||
|
||||
// Обновляем общее количество спутников
|
||||
@@ -537,6 +624,45 @@ public class NMEAParser {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Парсит ZDA сообщение (Date and Time)
|
||||
*/
|
||||
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());
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "ZDA не совпадает с паттерном: " + zda);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Парсит GSA сообщение (GPS DOP and Active Satellites)
|
||||
* КЛЮЧЕВОЕ сообщение для получения DOP и активных спутников
|
||||
@@ -544,15 +670,18 @@ public class NMEAParser {
|
||||
private void parseGSA(String gsa) {
|
||||
Log.d(TAG, "Парсим GSA: " + gsa);
|
||||
Matcher matcher = GSA_PATTERN.matcher(gsa);
|
||||
Matcher truncatedMatcher = GSA_TRUNCATED_PATTERN.matcher(gsa);
|
||||
|
||||
if (matcher.matches()) {
|
||||
// Log.d(TAG, "GSA совпадает с паттерном");
|
||||
Log.d(TAG, "GSA совпадает с паттерном");
|
||||
|
||||
// Подсчитываем активные спутники (непустые поля)
|
||||
int activeSatellites = 0;
|
||||
for (int i = 2; i <= 13; i++) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -561,35 +690,35 @@ public class NMEAParser {
|
||||
double hdop = 0.0;
|
||||
double vdop = 0.0;
|
||||
|
||||
String pdopStr = matcher.group(14);
|
||||
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");
|
||||
Log.w(TAG, "Не удалось распарсить PDOP: '" + pdopStr + "', используем 0.0");
|
||||
}
|
||||
}
|
||||
|
||||
String hdopStr = matcher.group(15);
|
||||
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");
|
||||
Log.w(TAG, "Не удалось распарсить HDOP: '" + hdopStr + "', используем 0.0");
|
||||
}
|
||||
}
|
||||
|
||||
String vdopStr = matcher.group(16);
|
||||
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.w(TAG, "Не удалось распарсить VDOP: '" + vdopStr + "', используем 0.0");
|
||||
}
|
||||
}
|
||||
|
||||
// Log.d(TAG, String.format("GSA: активных спутников=%d, PDOP=%.2f, HDOP=%.2f, VDOP=%.2f",
|
||||
// activeSatellites, pdop, hdop, vdop));
|
||||
Log.d(TAG, String.format("GSA: активных спутников=%d, PDOP=%.2f, HDOP=%.2f, VDOP=%.2f",
|
||||
activeSatellites, pdop, hdop, vdop));
|
||||
|
||||
// Обновляем информацию о спутниках
|
||||
ownVessel.setActiveSatellites(activeSatellites);
|
||||
@@ -604,13 +733,59 @@ public class NMEAParser {
|
||||
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 + "'");
|
||||
Log.w(TAG, "Паттерн: " + GSA_PATTERN.pattern());
|
||||
Log.w(TAG, "Обрезанный паттерн: " + GSA_TRUNCATED_PATTERN.pattern());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user