1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
|
package com.example.androidapp.nmea
import com.example.androidapp.gps.GpsPosition
import java.util.Calendar
import java.util.TimeZone
class NmeaParser {
/**
* Parses an NMEA RMC sentence and returns a [GpsPosition], or null if the
* sentence is void (status=V), malformed, or cannot be parsed.
*
* Supported talker IDs: GP, GN, and any other standard prefix.
* SOG and COG default to 0.0 when the fields are absent.
*/
fun parseRmc(sentence: String): GpsPosition? {
if (sentence.isBlank()) return null
val body = if ('*' in sentence) sentence.substringBefore('*') else sentence
val fields = body.split(',')
if (fields.size < 10) return null
if (!fields[0].endsWith("RMC")) return null
if (fields[2] != "A") return null
val latStr = fields.getOrNull(3) ?: return null
val latDir = fields.getOrNull(4) ?: return null
val lonStr = fields.getOrNull(5) ?: return null
val lonDir = fields.getOrNull(6) ?: return null
val latitude = parseNmeaDegrees(latStr) * if (latDir == "S") -1.0 else 1.0
val longitude = parseNmeaDegrees(lonStr) * if (lonDir == "W") -1.0 else 1.0
val sog = fields.getOrNull(7)?.toDoubleOrNull() ?: 0.0
val cog = fields.getOrNull(8)?.toDoubleOrNull() ?: 0.0
val timestampMs = parseTimestamp(
timeStr = fields.getOrNull(1) ?: "",
dateStr = fields.getOrNull(9) ?: ""
)
if (timestampMs == 0L) return null
return GpsPosition(latitude, longitude, sog, cog, timestampMs)
}
/**
* Converts NMEA degree-minutes format (DDDMM.MMMM) to decimal degrees.
*/
private fun parseNmeaDegrees(value: String): Double {
val raw = value.toDoubleOrNull() ?: return 0.0
val degrees = (raw / 100.0).toInt()
val minutes = raw - degrees * 100.0
return degrees + minutes / 60.0
}
/**
* Combines NMEA time (HHMMSS.ss) and date (DDMMYY) into Unix epoch millis UTC.
* Returns 0 on any parse failure.
*/
private fun parseTimestamp(timeStr: String, dateStr: String): Long {
return try {
val cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
cal.isLenient = false
if (dateStr.length >= 6) {
val day = dateStr.substring(0, 2).toInt()
val month = dateStr.substring(2, 4).toInt() - 1
val yy = dateStr.substring(4, 6).toInt()
val year = if (yy < 70) 2000 + yy else 1900 + yy
cal.set(Calendar.YEAR, year)
cal.set(Calendar.MONTH, month)
cal.set(Calendar.DAY_OF_MONTH, day)
}
if (timeStr.length >= 6) {
val hours = timeStr.substring(0, 2).toInt()
val minutes = timeStr.substring(2, 4).toInt()
val seconds = timeStr.substring(4, 6).toInt()
val millis = if (timeStr.length > 7) {
val fracStr = timeStr.substring(7)
(("0.$fracStr").toDoubleOrNull()?.times(1000.0))?.toInt() ?: 0
} else 0
cal.set(Calendar.HOUR_OF_DAY, hours)
cal.set(Calendar.MINUTE, minutes)
cal.set(Calendar.SECOND, seconds)
cal.set(Calendar.MILLISECOND, millis)
}
cal.timeInMillis
} catch (e: Exception) {
0L
}
}
}
|