updated 151025-01

This commit is contained in:
wengki81 2025-10-15 09:49:07 +08:00
parent 438fb644cd
commit c306b59b9e
15 changed files with 127 additions and 13 deletions

View File

@ -169,6 +169,11 @@ interface PositioningData {
isMocked: boolean; isMocked: boolean;
mode: 'normal' | 'driving'; mode: 'normal' | 'driving';
predicted?: boolean; predicted?: boolean;
// opsional: detail asal speed
speedSource?: 'GNSS' | 'IMU' | 'DELTA' | 'NONE';
speedGnss?: number; // m/s dari Location.getSpeed bila tersedia & segar
speedImu?: number; // m/s dari IMU (heuristik internal)
speedDerived?: number; // m/s hasil Δpos/Δt (kasar)
} }
``` ```
@ -189,6 +194,14 @@ interface PermissionStatus {
- Prediksi maju (deadreckoning) bersifat opsional dan nonaktif secara default. Aktifkan dengan `setOptions({ enableForwardPrediction: true, maxPredictionSeconds?: number })`. - Prediksi maju (deadreckoning) bersifat opsional dan nonaktif secara default. Aktifkan dengan `setOptions({ enableForwardPrediction: true, maxPredictionSeconds?: number })`.
- Saat prediksi aktif, posisi dapat diproyeksikan pendek (<= `maxPredictionSeconds`) berdasarkan `speed` dan `directionRad` dari IMU, serta ditandai `predicted: true`. Nilai `source` tidak berubah. - Saat prediksi aktif, posisi dapat diproyeksikan pendek (<= `maxPredictionSeconds`) berdasarkan `speed` dan `directionRad` dari IMU, serta ditandai `predicted: true`. Nilai `source` tidak berubah.
### Kebijakan `speed`
- Field `speed` kini dipilih dengan prioritas: GNSS > IMU > Δpos/Δt > 0.
- GNSS: dipakai jika `Location.hasSpeed()` dan usia fix ≤ ~3000 ms.
- IMU: fallback dari integrasi percepatan IMU (dibatasi ke 0..30 m/s, dengan smoothing & idle handling).
- Δpos/Δt: fallback terakhir, hanya bila selang waktu antar fix ≥ 3 s.
- Field tambahan opsional: `speedSource`, `speedGnss`, `speedImu`, `speedDerived` untuk transparansi asal nilai.
--- ---
## Detail Sensor (untuk pengembangan lanjutan) ## Detail Sensor (untuk pengembangan lanjutan)

View File

@ -60,6 +60,7 @@ class DumonGeolocation : Plugin() {
private var latestAccuracy = 999.0 private var latestAccuracy = 999.0
private var latestSource = "GNSS" private var latestSource = "GNSS"
private var latestTimestamp: Long = 0L private var latestTimestamp: Long = 0L
private var latestGnssSpeed: Float? = null
private var latestImu: ImuData? = null private var latestImu: ImuData? = null
private var satelliteStatus: SatelliteStatus? = null private var satelliteStatus: SatelliteStatus? = null
@ -72,6 +73,7 @@ class DumonGeolocation : Plugin() {
private var prevLongitude = 0.0 private var prevLongitude = 0.0
private var prevSpeed = 0f private var prevSpeed = 0f
private var prevDirection = 0f private var prevDirection = 0f
private var prevTimestamp: Long = 0L
// private val significantChangeThreshold = 0.00007 // ~7 meters // private val significantChangeThreshold = 0.00007 // ~7 meters
private var significantChangeThreshold = 7.0 // ~7 meters private var significantChangeThreshold = 7.0 // ~7 meters
private var speedChangeThreshold = 0.5f // m/s private var speedChangeThreshold = 0.5f // m/s
@ -121,6 +123,7 @@ class DumonGeolocation : Plugin() {
latestSource = if (isMocked) "MOCK" else "GNSS" latestSource = if (isMocked) "MOCK" else "GNSS"
isMockedLocation = isMocked isMockedLocation = isMocked
latestTimestamp = location.time latestTimestamp = location.time
latestGnssSpeed = if (location.hasSpeed()) location.speed else null
if (currentTrackingMode == GpsTrackingMode.DRIVING) { if (currentTrackingMode == GpsTrackingMode.DRIVING) {
bufferedDrivingLocation = location bufferedDrivingLocation = location
@ -160,6 +163,7 @@ class DumonGeolocation : Plugin() {
latestAccuracy = location.accuracy.toDouble() latestAccuracy = location.accuracy.toDouble()
latestTimestamp = location.time latestTimestamp = location.time
latestSource = if (isMockedLocation) "MOCK" else "GNSS" latestSource = if (isMockedLocation) "MOCK" else "GNSS"
latestGnssSpeed = if (location.hasSpeed()) location.speed else null
emitPositionUpdate(forceEmit = true) // force emit in driving emitPositionUpdate(forceEmit = true) // force emit in driving
} }
drivingEmitHandler?.postDelayed(this, drivingEmitIntervalMs) drivingEmitHandler?.postDelayed(this, drivingEmitIntervalMs)
@ -229,6 +233,7 @@ class DumonGeolocation : Plugin() {
latestSource = if (isMocked) "MOCK" else "GNSS" latestSource = if (isMocked) "MOCK" else "GNSS"
isMockedLocation = isMocked isMockedLocation = isMocked
latestTimestamp = location.time latestTimestamp = location.time
latestGnssSpeed = if (location.hasSpeed()) location.speed else null
call.resolve(buildPositionData()) call.resolve(buildPositionData())
} }
} else { } else {
@ -653,6 +658,7 @@ class DumonGeolocation : Plugin() {
prevLongitude = latestLongitude prevLongitude = latestLongitude
prevSpeed = speedNow prevSpeed = speedNow
prevDirection = directionNow prevDirection = directionNow
prevTimestamp = if (latestTimestamp > 0) latestTimestamp else now
lastEmitTimestamp = now lastEmitTimestamp = now
// Ensure listener notifications run on the main thread for consistency // Ensure listener notifications run on the main thread for consistency
@ -740,13 +746,42 @@ class DumonGeolocation : Plugin() {
obj.put("isMocked", isMockedLocation) obj.put("isMocked", isMockedLocation)
obj.put("mode", if (currentTrackingMode == GpsTrackingMode.DRIVING) "driving" else "normal") obj.put("mode", if (currentTrackingMode == GpsTrackingMode.DRIVING) "driving" else "normal")
// Always provide IMU-related fields to match TS definitions // Gather speed candidates
val speedVal = latestImu?.speed ?: 0f val imuSpeed: Float? = latestImu?.speed
val accelVal = latestImu?.acceleration ?: 0f val imuAccel: Float = latestImu?.acceleration ?: 0f
val dirVal = latestImu?.directionRad ?: 0f val imuDir: Float = latestImu?.directionRad ?: 0f
obj.put("speed", speedVal)
obj.put("acceleration", accelVal) // GNSS speed candidate is valid only if recent enough
obj.put("directionRad", dirVal) val fixAgeMs = if (latestTimestamp > 0) (now - latestTimestamp) else Long.MAX_VALUE
val gnssSpeed: Float? = latestGnssSpeed?.takeIf { fixAgeMs <= 3000L }
// Derived speed from delta position and timestamps (use only on sufficient dt)
val derivedSpeed: Float? = run {
val dtMs = if (prevTimestamp > 0 && latestTimestamp > 0) (latestTimestamp - prevTimestamp) else 0L
val dtSec = dtMs.toDouble() / 1000.0
if (dtSec >= 3.0 && dtSec <= 30.0) {
val dMeters = calculateDistance(latestLatitude, latestLongitude, prevLatitude, prevLongitude)
(dMeters / dtSec).toFloat()
} else null
}
// Choose final speed: GNSS > IMU > Derived > 0
val (finalSpeed, speedSource) = when {
gnssSpeed != null -> Pair(gnssSpeed, "GNSS")
imuSpeed != null -> Pair(imuSpeed, "IMU")
derivedSpeed != null -> Pair(derivedSpeed, "DELTA")
else -> Pair(0f, "NONE")
}
obj.put("speed", finalSpeed)
obj.put("acceleration", imuAccel)
obj.put("directionRad", imuDir)
obj.put("speedSource", speedSource)
// Optional additional fields when available
gnssSpeed?.let { obj.put("speedGnss", it.toDouble()) }
imuSpeed?.let { obj.put("speedImu", it.toDouble()) }
derivedSpeed?.let { obj.put("speedDerived", it.toDouble()) }
obj.put("predicted", predicted) obj.put("predicted", predicted)

28
dist/docs.json vendored
View File

@ -386,6 +386,34 @@
"docs": "", "docs": "",
"complexTypes": [], "complexTypes": [],
"type": "boolean | undefined" "type": "boolean | undefined"
},
{
"name": "speedSource",
"tags": [],
"docs": "",
"complexTypes": [],
"type": "'GNSS' | 'IMU' | 'DELTA' | 'NONE' | undefined"
},
{
"name": "speedGnss",
"tags": [],
"docs": "",
"complexTypes": [],
"type": "number | undefined"
},
{
"name": "speedImu",
"tags": [],
"docs": "",
"complexTypes": [],
"type": "number | undefined"
},
{
"name": "speedDerived",
"tags": [],
"docs": "",
"complexTypes": [],
"type": "number | undefined"
} }
] ]
}, },

View File

@ -11,6 +11,10 @@ export interface PositioningData {
isMocked: boolean; isMocked: boolean;
mode: 'normal' | 'driving'; mode: 'normal' | 'driving';
predicted?: boolean; predicted?: boolean;
speedSource?: 'GNSS' | 'IMU' | 'DELTA' | 'NONE';
speedGnss?: number;
speedImu?: number;
speedDerived?: number;
} }
export interface SatelliteStatus { export interface SatelliteStatus {
satellitesInView: number; satellitesInView: number;

View File

@ -1 +1 @@
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PluginListenerHandle } from '@capacitor/core';\n\n// export interface SatelliteStatus {\n// satellitesInView: number;\n// usedInFix: number;\n// constellationCounts: { [key: string]: number };\n// }\n\n// export interface WifiAp {\n// ssid: string;\n// bssid: string;\n// rssi: number;\n// distance?: number;\n// }\n\n// export interface WifiScanResult {\n// apCount: number;\n// aps: WifiAp[];\n// }\n\n// export interface ImuData {\n// accelX: number;\n// accelY: number;\n// accelZ: number;\n// gyroX: number;\n// gyroY: number;\n// gyroZ: number;\n// speed?: number;\n// acceleration?: number;\n// directionRad?: number;\n// }\n\n// export interface GpsData {\n// latitude: number;\n// longitude: number;\n// accuracy: number;\n// satellitesInView?: number;\n// usedInFix?: number;\n// constellationCounts?: { [key: string]: number };\n// }\n\n// export interface PositioningData {\n// source: 'GNSS' | 'WIFI' | 'FUSED' | 'MOCK';\n// timestamp: number;\n// latitude: number;\n// longitude: number;\n// accuracy: number;\n\n// gnssData?: SatelliteStatus;\n// wifiData?: WifiAp[];\n// imuData?: ImuData;\n// }\n\nexport interface PositioningData {\n source: 'GNSS' | 'WIFI' | 'FUSED' | 'MOCK';\n timestamp: number;\n latitude: number;\n longitude: number;\n accuracy: number;\n speed: number;\n acceleration: number;\n directionRad: number;\n isMocked: boolean;\n mode: 'normal' | 'driving';\n predicted?: boolean;\n}\n\nexport interface SatelliteStatus {\n satellitesInView: number;\n usedInFix: number;\n constellationCounts: { [key: string]: number };\n}\n\nexport interface DumonGeoOptions {\n distanceThresholdMeters?: number;\n speedChangeThreshold?: number;\n directionChangeThreshold?: number;\n emitDebounceMs?: number;\n drivingEmitIntervalMs?: number;\n wifiScanIntervalMs?: number;\n enableWifiRtt?: boolean;\n enableLogging?: boolean;\n enableForwardPrediction?: boolean;\n maxPredictionSeconds?: number;\n emitGnssStatus?: boolean;\n suppressMockedUpdates?: boolean;\n keepScreenOn?: boolean;\n backgroundPollingIntervalMs?: number; // Android background polling interval\n backgroundPostMinDistanceMeters?: number; // Android background min distance to post\n backgroundPostMinAccuracyMeters?: number; // Android background min acceptable accuracy for POST (meters)\n backgroundMinPostIntervalMs?: number; // Android background minimum interval between POST attempts\n}\n\nexport interface PermissionStatus {\n location: 'granted' | 'denied';\n wifi: 'granted' | 'denied';\n}\n\nexport interface DumonGeolocationPlugin {\n startPositioning(): Promise<void>;\n stopPositioning(): Promise<void>;\n getLatestPosition(): Promise<PositioningData>;\n checkAndRequestPermissions(): Promise<PermissionStatus>;\n setOptions(options: DumonGeoOptions): Promise<void>;\n getGnssStatus(): Promise<SatelliteStatus | null>;\n getLocationServicesStatus(): Promise<{ gpsEnabled: boolean; networkEnabled: boolean }>;\n // Background tracking (Android)\n startBackgroundTracking(options?: {\n title?: string;\n text?: string;\n channelId?: string;\n channelName?: string;\n postUrl?: string; // optional: service will POST latest fixes here as JSON\n }): Promise<void>;\n stopBackgroundTracking(): Promise<void>;\n isBackgroundTrackingActive(): Promise<{ active: boolean }>;\n getBackgroundLatestPosition(): Promise<PositioningData | null>;\n openBackgroundPermissionSettings(): Promise<void>;\n openNotificationPermissionSettings(): Promise<void>;\n // Auth token management for background posting\n setAuthTokens(tokens: { accessToken: string; refreshToken: string }): Promise<void>;\n clearAuthTokens(): Promise<void>;\n getAuthState(): Promise<{ present: boolean }>;\n setBackgroundPostUrl(options: { url?: string }): Promise<void>;\n getBackgroundPostUrl(): Promise<{ url: string | null }>;\n\n configureEdgeToEdge(options: {\n bgColor: string;\n style: 'DARK' | 'LIGHT';\n overlay?: boolean;\n }): Promise<void>;\n\n setGpsMode(options: { mode: 'normal' | 'driving' }): Promise<void>;\n\n addListener(\n eventName: 'onPositionUpdate',\n listenerFunc: (data: PositioningData) => void\n ): PluginListenerHandle;\n\n addListener(\n eventName: 'onGnssStatus',\n listenerFunc: (data: SatelliteStatus) => void\n ): PluginListenerHandle;\n}\n"]} {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PluginListenerHandle } from '@capacitor/core';\n\n// export interface SatelliteStatus {\n// satellitesInView: number;\n// usedInFix: number;\n// constellationCounts: { [key: string]: number };\n// }\n\n// export interface WifiAp {\n// ssid: string;\n// bssid: string;\n// rssi: number;\n// distance?: number;\n// }\n\n// export interface WifiScanResult {\n// apCount: number;\n// aps: WifiAp[];\n// }\n\n// export interface ImuData {\n// accelX: number;\n// accelY: number;\n// accelZ: number;\n// gyroX: number;\n// gyroY: number;\n// gyroZ: number;\n// speed?: number;\n// acceleration?: number;\n// directionRad?: number;\n// }\n\n// export interface GpsData {\n// latitude: number;\n// longitude: number;\n// accuracy: number;\n// satellitesInView?: number;\n// usedInFix?: number;\n// constellationCounts?: { [key: string]: number };\n// }\n\n// export interface PositioningData {\n// source: 'GNSS' | 'WIFI' | 'FUSED' | 'MOCK';\n// timestamp: number;\n// latitude: number;\n// longitude: number;\n// accuracy: number;\n\n// gnssData?: SatelliteStatus;\n// wifiData?: WifiAp[];\n// imuData?: ImuData;\n// }\n\nexport interface PositioningData {\n source: 'GNSS' | 'WIFI' | 'FUSED' | 'MOCK';\n timestamp: number;\n latitude: number;\n longitude: number;\n accuracy: number;\n speed: number;\n acceleration: number;\n directionRad: number;\n isMocked: boolean;\n mode: 'normal' | 'driving';\n predicted?: boolean;\n // Optional detailed speed fields and provenance\n speedSource?: 'GNSS' | 'IMU' | 'DELTA' | 'NONE';\n speedGnss?: number; // m/s from Location.getSpeed when available and fresh\n speedImu?: number; // m/s from IMU fusion (internal heuristic)\n speedDerived?: number; // m/s from delta-position / delta-time (coarse)\n}\n\nexport interface SatelliteStatus {\n satellitesInView: number;\n usedInFix: number;\n constellationCounts: { [key: string]: number };\n}\n\nexport interface DumonGeoOptions {\n distanceThresholdMeters?: number;\n speedChangeThreshold?: number;\n directionChangeThreshold?: number;\n emitDebounceMs?: number;\n drivingEmitIntervalMs?: number;\n wifiScanIntervalMs?: number;\n enableWifiRtt?: boolean;\n enableLogging?: boolean;\n enableForwardPrediction?: boolean;\n maxPredictionSeconds?: number;\n emitGnssStatus?: boolean;\n suppressMockedUpdates?: boolean;\n keepScreenOn?: boolean;\n backgroundPollingIntervalMs?: number; // Android background polling interval\n backgroundPostMinDistanceMeters?: number; // Android background min distance to post\n backgroundPostMinAccuracyMeters?: number; // Android background min acceptable accuracy for POST (meters)\n backgroundMinPostIntervalMs?: number; // Android background minimum interval between POST attempts\n}\n\nexport interface PermissionStatus {\n location: 'granted' | 'denied';\n wifi: 'granted' | 'denied';\n}\n\nexport interface DumonGeolocationPlugin {\n startPositioning(): Promise<void>;\n stopPositioning(): Promise<void>;\n getLatestPosition(): Promise<PositioningData>;\n checkAndRequestPermissions(): Promise<PermissionStatus>;\n setOptions(options: DumonGeoOptions): Promise<void>;\n getGnssStatus(): Promise<SatelliteStatus | null>;\n getLocationServicesStatus(): Promise<{ gpsEnabled: boolean; networkEnabled: boolean }>;\n // Background tracking (Android)\n startBackgroundTracking(options?: {\n title?: string;\n text?: string;\n channelId?: string;\n channelName?: string;\n postUrl?: string; // optional: service will POST latest fixes here as JSON\n }): Promise<void>;\n stopBackgroundTracking(): Promise<void>;\n isBackgroundTrackingActive(): Promise<{ active: boolean }>;\n getBackgroundLatestPosition(): Promise<PositioningData | null>;\n openBackgroundPermissionSettings(): Promise<void>;\n openNotificationPermissionSettings(): Promise<void>;\n // Auth token management for background posting\n setAuthTokens(tokens: { accessToken: string; refreshToken: string }): Promise<void>;\n clearAuthTokens(): Promise<void>;\n getAuthState(): Promise<{ present: boolean }>;\n setBackgroundPostUrl(options: { url?: string }): Promise<void>;\n getBackgroundPostUrl(): Promise<{ url: string | null }>;\n\n configureEdgeToEdge(options: {\n bgColor: string;\n style: 'DARK' | 'LIGHT';\n overlay?: boolean;\n }): Promise<void>;\n\n setGpsMode(options: { mode: 'normal' | 'driving' }): Promise<void>;\n\n addListener(\n eventName: 'onPositionUpdate',\n listenerFunc: (data: PositioningData) => void\n ): PluginListenerHandle;\n\n addListener(\n eventName: 'onGnssStatus',\n listenerFunc: (data: SatelliteStatus) => void\n ): PluginListenerHandle;\n}\n"]}

4
dist/esm/web.js vendored
View File

@ -23,6 +23,10 @@ export class DumonGeolocationWeb extends WebPlugin {
directionRad: 0, directionRad: 0,
isMocked: false, isMocked: false,
mode: this._mode, mode: this._mode,
speedSource: 'NONE',
speedImu: 0,
speedGnss: 0,
speedDerived: 0,
}; };
} }
async checkAndRequestPermissions() { async checkAndRequestPermissions() {

2
dist/esm/web.js.map vendored

File diff suppressed because one or more lines are too long

4
dist/plugin.cjs.js vendored
View File

@ -30,6 +30,10 @@ class DumonGeolocationWeb extends core.WebPlugin {
directionRad: 0, directionRad: 0,
isMocked: false, isMocked: false,
mode: this._mode, mode: this._mode,
speedSource: 'NONE',
speedImu: 0,
speedGnss: 0,
speedDerived: 0,
}; };
} }
async checkAndRequestPermissions() { async checkAndRequestPermissions() {

File diff suppressed because one or more lines are too long

4
dist/plugin.js vendored
View File

@ -29,6 +29,10 @@ var capacitorDumonGeolocation = (function (exports, core) {
directionRad: 0, directionRad: 0,
isMocked: false, isMocked: false,
mode: this._mode, mode: this._mode,
speedSource: 'NONE',
speedImu: 0,
speedGnss: 0,
speedDerived: 0,
}; };
} }
async checkAndRequestPermissions() { async checkAndRequestPermissions() {

2
dist/plugin.js.map vendored

File diff suppressed because one or more lines are too long

View File

@ -25,12 +25,17 @@ export interface PositioningData {
latitude: number; latitude: number;
longitude: number; longitude: number;
accuracy: number; // meter accuracy: number; // meter
speed: number; // m/s (dari IMU heuristik) speed: number; // m/s (lihat kebijakan pemilihan di bawah)
acceleration: number; // m/s^2 (magnitude IMU) acceleration: number; // m/s^2 (magnitude IMU)
directionRad: number; // radian, relatif utara directionRad: number; // radian, relatif utara
isMocked: boolean; // lokasi terdeteksi palsu isMocked: boolean; // lokasi terdeteksi palsu
mode: 'normal' | 'driving'; // mode tracking aktif saat emisi mode: 'normal' | 'driving'; // mode tracking aktif saat emisi
predicted?: boolean; // true jika posisi diproyeksikan ke depan predicted?: boolean; // true jika posisi diproyeksikan ke depan
// opsional: detail asal speed
speedSource?: 'GNSS' | 'IMU' | 'DELTA' | 'NONE';
speedGnss?: number; // m/s dari Location.getSpeed bila tersedia & segar
speedImu?: number; // m/s dari IMU (heuristik internal)
speedDerived?: number; // m/s hasil Δpos/Δt (kasar)
} }
export interface SatelliteStatus { export interface SatelliteStatus {
@ -184,6 +189,14 @@ Implementasi utama: `android/src/main/java/com/dumon/plugin/geolocation/DumonGeo
- Jika `enableForwardPrediction` aktif dan ada IMU, posisi dapat diproyeksikan pendek (<= `maxPredictionSeconds`) memakai `speed` dan `directionRad` → flag `predicted: true`. - Jika `enableForwardPrediction` aktif dan ada IMU, posisi dapat diproyeksikan pendek (<= `maxPredictionSeconds`) memakai `speed` dan `directionRad` → flag `predicted: true`.
- `source` tetap diisi nilai asli (mis. `GNSS`), bukan `FUSED`. - `source` tetap diisi nilai asli (mis. `GNSS`), bukan `FUSED`.
### Kebijakan pemilihan `speed`
- Urutan prioritas: GNSS > IMU > Δpos/Δt > 0.
- GNSS: dipakai bila `Location.hasSpeed()` dan usia fix ≤ ~3000 ms.
- IMU: fallback dari integrasi percepatan IMU (0..30 m/s; smoothing dan idle handling diterapkan).
- Δpos/Δt: dipakai hanya bila selang waktu antar fix memadai (≥ 3 s). Cocok sebagai estimasi kasar.
- Untuk transparansi, payload dapat menyertakan `speedSource`, `speedGnss`, `speedImu`, `speedDerived` (opsional).
### getLatestPosition (perilaku terbaru) ### getLatestPosition (perilaku terbaru)
- Memicu `GpsStatusManager.requestSingleFix()` (GPS + opsional Network) dengan timeout default ~3000 ms. - Memicu `GpsStatusManager.requestSingleFix()` (GPS + opsional Network) dengan timeout default ~3000 ms.

View File

@ -1,6 +1,6 @@
{ {
"name": "dumon-geolocation", "name": "dumon-geolocation",
"version": "1.0.3", "version": "1.0.4",
"description": "Implement manager GNSS, WiFi RTT, IMU, Kalman fusion, event emitter", "description": "Implement manager GNSS, WiFi RTT, IMU, Kalman fusion, event emitter",
"main": "dist/plugin.cjs.js", "main": "dist/plugin.cjs.js",
"module": "dist/esm/index.js", "module": "dist/esm/index.js",

View File

@ -63,6 +63,11 @@ export interface PositioningData {
isMocked: boolean; isMocked: boolean;
mode: 'normal' | 'driving'; mode: 'normal' | 'driving';
predicted?: boolean; predicted?: boolean;
// Optional detailed speed fields and provenance
speedSource?: 'GNSS' | 'IMU' | 'DELTA' | 'NONE';
speedGnss?: number; // m/s from Location.getSpeed when available and fresh
speedImu?: number; // m/s from IMU fusion (internal heuristic)
speedDerived?: number; // m/s from delta-position / delta-time (coarse)
} }
export interface SatelliteStatus { export interface SatelliteStatus {

View File

@ -24,6 +24,10 @@ export class DumonGeolocationWeb extends WebPlugin {
directionRad: 0, directionRad: 0,
isMocked: false, isMocked: false,
mode: this._mode, mode: this._mode,
speedSource: 'NONE',
speedImu: 0,
speedGnss: 0,
speedDerived: 0,
}; };
} }