Created ship vectors (not added yet)

Created menu
Created udp support
Created DockWidgets for compass and SOG/COG
This commit is contained in:
2025-09-03 15:40:02 +03:00
parent 2734560160
commit 25b1dabf73
70 changed files with 3145 additions and 293 deletions
@@ -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());
}
}