Update vcs.xml, AndroidManifest.xml, BleScanner.kt, and 4 more files
Sensor Fusion
This commit is contained in:
1 parent
4426388063
commit
4303d34471
7 files changed
+499
-58
No files matched your search
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
<!-- Für BLE-Positionierung benötigt-->
|
<!-- Für BLE-Positionierung benötigt-->
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,15 +14,18 @@ import com.erik.bleindoorpositioning.model.PositionResult
|
|||||||
import com.erik.bleindoorpositioning.model.ScannedBeacon
|
import com.erik.bleindoorpositioning.model.ScannedBeacon
|
||||||
import com.erik.bleindoorpositioning.positioning.BeaconFilterManager
|
import com.erik.bleindoorpositioning.positioning.BeaconFilterManager
|
||||||
import com.erik.bleindoorpositioning.positioning.KalmanFilter
|
import com.erik.bleindoorpositioning.positioning.KalmanFilter
|
||||||
|
import com.erik.bleindoorpositioning.positioning.MovingAverageFilter
|
||||||
import com.erik.bleindoorpositioning.positioning.PositionCalculator
|
import com.erik.bleindoorpositioning.positioning.PositionCalculator
|
||||||
|
import com.erik.bleindoorpositioning.positioning.PositionKalmanFilter
|
||||||
import com.erik.bleindoorpositioning.positioning.PositionSmoother
|
import com.erik.bleindoorpositioning.positioning.PositionSmoother
|
||||||
|
import com.erik.bleindoorpositioning.positioning.SensorFusionManager
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class BleScanner(context: Context, private val configRepository: ConfigRepository, private val coroutineScope: CoroutineScope) {
|
class BleScanner(private val context: Context, private val configRepository: ConfigRepository, private val coroutineScope: CoroutineScope) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "BleScanner"
|
private const val TAG = "BleScanner"
|
||||||
@@ -52,18 +55,34 @@ class BleScanner(context: Context, private val configRepository: ConfigRepositor
|
|||||||
private var retryAttempt = 0
|
private var retryAttempt = 0
|
||||||
|
|
||||||
// RSSI-Filterung
|
// RSSI-Filterung
|
||||||
private val filterManager = BeaconFilterManager {
|
private val filterManager = BeaconFilterManager { // Filter für einzelne Beacon-RSSI-Werte
|
||||||
KalmanFilter(
|
MovingAverageFilter(windowSize = 20) // 20 Messpunkte für stabile Mittelwertbildung
|
||||||
processNoise = 0.5, // TODO: Kalibrieren
|
|
||||||
measurementNoise = 10.0 // TODO: Kalibrieren
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Positionsberechnung (wird bei jedem Scan-Start neu initialisiert)
|
// Positionsberechnung (wird bei jedem Scan-Start neu initialisiert)
|
||||||
private var positionCalculator: PositionCalculator? = null
|
private var positionCalculator: PositionCalculator? = null
|
||||||
|
|
||||||
// Positions-Glättung
|
// Positions-Glättung
|
||||||
private val positionSmoother = PositionSmoother(alpha = 0.15)
|
private val positionSmoother = PositionSmoother(alpha = 0.25) // Reaktiver, da EKF schon glättet
|
||||||
|
|
||||||
|
// Sensor-Fusion (EKF + PDR)
|
||||||
|
private val positionKalmanFilter = PositionKalmanFilter(processNoise = 0.5) // Standard: Balance zwischen PDR und BLE
|
||||||
|
private var isEkfInitialized = false
|
||||||
|
private var sensorFusionManager: SensorFusionManager? = null
|
||||||
|
private val lock = Any()
|
||||||
|
|
||||||
|
init {
|
||||||
|
sensorFusionManager = SensorFusionManager(context) { heading, stepLength ->
|
||||||
|
synchronized(lock) {
|
||||||
|
positionKalmanFilter.predict(heading, stepLength)
|
||||||
|
val fusedPos = positionKalmanFilter.getPosition()
|
||||||
|
val smoothed = positionSmoother.update(fusedPos)
|
||||||
|
_position.value?.let { prev ->
|
||||||
|
_position.value = prev.copy(position = smoothed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Öffentliche State Flows
|
// Öffentliche State Flows
|
||||||
private val _beacons = MutableStateFlow<Map<Int, ScannedBeacon>>(emptyMap())
|
private val _beacons = MutableStateFlow<Map<Int, ScannedBeacon>>(emptyMap())
|
||||||
@@ -109,10 +128,18 @@ class BleScanner(context: Context, private val configRepository: ConfigRepositor
|
|||||||
val rssiMap = freshBeacons.mapValues { it.value.filteredRssi }
|
val rssiMap = freshBeacons.mapValues { it.value.filteredRssi }
|
||||||
val rawResult = positionCalculator?.calculate(rssiMap)
|
val rawResult = positionCalculator?.calculate(rssiMap)
|
||||||
|
|
||||||
// Position glätten
|
// Sensor-Fusion: BLE dominiert, PDR ergänzt zwischen Updates
|
||||||
_position.value = rawResult?.let { raw ->
|
rawResult?.let { raw ->
|
||||||
val smoothed = positionSmoother.update(raw.position)
|
synchronized(lock) {
|
||||||
raw.copy(position = smoothed)
|
if (!isEkfInitialized) {
|
||||||
|
positionKalmanFilter.initialize(raw.position)
|
||||||
|
isEkfInitialized = true
|
||||||
|
}
|
||||||
|
positionKalmanFilter.update(raw.position, measurementNoise = 2.0)
|
||||||
|
val fusedPosition = positionKalmanFilter.getPosition()
|
||||||
|
val smoothed = positionSmoother.update(fusedPosition)
|
||||||
|
_position.value = raw.copy(position = smoothed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "Beacon ${beacon.minor}: " +
|
Log.d(TAG, "Beacon ${beacon.minor}: " +
|
||||||
@@ -211,6 +238,7 @@ class BleScanner(context: Context, private val configRepository: ConfigRepositor
|
|||||||
|
|
||||||
scanner?.startScan(null, settings, scanCallback)
|
scanner?.startScan(null, settings, scanCallback)
|
||||||
_isScanning.value = true
|
_isScanning.value = true
|
||||||
|
sensorFusionManager?.start()
|
||||||
Log.d(TAG, "Scanning started")
|
Log.d(TAG, "Scanning started")
|
||||||
|
|
||||||
// Start proactive scan restart job
|
// Start proactive scan restart job
|
||||||
@@ -238,6 +266,8 @@ class BleScanner(context: Context, private val configRepository: ConfigRepositor
|
|||||||
val config = configRepository.loadConfig()
|
val config = configRepository.loadConfig()
|
||||||
if (config.beacons.size < 3) {
|
if (config.beacons.size < 3) {
|
||||||
Log.w(TAG, "Weniger als 3 Beacons konfiguriert, Positionsberechnung nicht möglich")
|
Log.w(TAG, "Weniger als 3 Beacons konfiguriert, Positionsberechnung nicht möglich")
|
||||||
|
positionCalculator = null
|
||||||
|
return
|
||||||
}
|
}
|
||||||
positionCalculator = PositionCalculator(
|
positionCalculator = PositionCalculator(
|
||||||
rssiAtOneMeter = BeaconData.RSSI_AT_ONE_METER,
|
rssiAtOneMeter = BeaconData.RSSI_AT_ONE_METER,
|
||||||
@@ -262,7 +292,12 @@ class BleScanner(context: Context, private val configRepository: ConfigRepositor
|
|||||||
retryAttempt = 0
|
retryAttempt = 0
|
||||||
_isScanning.value = false
|
_isScanning.value = false
|
||||||
filterManager.reset()
|
filterManager.reset()
|
||||||
|
synchronized(lock) {
|
||||||
positionSmoother.reset()
|
positionSmoother.reset()
|
||||||
|
positionKalmanFilter.reset()
|
||||||
|
isEkfInitialized = false
|
||||||
|
}
|
||||||
|
sensorFusionManager?.stop()
|
||||||
Log.d(TAG, "Scanning stopped")
|
Log.d(TAG, "Scanning stopped")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+179
@@ -0,0 +1,179 @@
|
|||||||
|
package com.erik.bleindoorpositioning.positioning
|
||||||
|
|
||||||
|
import com.erik.bleindoorpositioning.model.Position
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.sin
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ein einfacher Erweiterter Kalman-Filter (EKF) zur Schätzung der 2D-Position.
|
||||||
|
* Dieser Filter fusioniert relative Positionsänderungen aus PDR (Pedestrian Dead Reckoning)
|
||||||
|
* mit absoluten Positionen aus BLE-Messungen.
|
||||||
|
*
|
||||||
|
* Der Zustandsvektor ist [x, y]. Die Geschwindigkeit ist nicht Teil des Zustands.
|
||||||
|
*
|
||||||
|
* @param processNoise Die Varianz des Prozessrauschens für die Vorhersage.
|
||||||
|
* Dies repräsentiert die Unsicherheit des Schrittmodells.
|
||||||
|
*/
|
||||||
|
class PositionKalmanFilter(processNoise: Double = 0.5) {
|
||||||
|
|
||||||
|
// Zustandsvektor: [x, y]
|
||||||
|
private var state = doubleArrayOf(0.0, 0.0)
|
||||||
|
|
||||||
|
// Kovarianzmatrix P (2x2)
|
||||||
|
private var covariance = arrayOf(
|
||||||
|
doubleArrayOf(1.0, 0.0),
|
||||||
|
doubleArrayOf(0.0, 1.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Prozessrauschkovarianz Q (2x2)
|
||||||
|
private val processNoiseQ = arrayOf(
|
||||||
|
doubleArrayOf(processNoise, 0.0),
|
||||||
|
doubleArrayOf(0.0, processNoise)
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialisiert den Filterzustand mit einer Startposition.
|
||||||
|
* Setzt auch die Kovarianz auf einen Anfangswert zurück, der eine hohe Unsicherheit darstellt.
|
||||||
|
*
|
||||||
|
* @param startPosition Die anfängliche Position.
|
||||||
|
*/
|
||||||
|
fun initialize(startPosition: Position) {
|
||||||
|
state[0] = startPosition.x
|
||||||
|
state[1] = startPosition.y
|
||||||
|
// Setze die Kovarianz auf einen Zustand hoher anfänglicher Unsicherheit zurück
|
||||||
|
covariance = arrayOf(
|
||||||
|
doubleArrayOf(1.0, 0.0),
|
||||||
|
doubleArrayOf(0.0, 1.0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setzt den Filterzustand und die Kovarianz auf die Standardwerte zurück.
|
||||||
|
*/
|
||||||
|
fun reset() {
|
||||||
|
state = doubleArrayOf(0.0, 0.0)
|
||||||
|
covariance = arrayOf(
|
||||||
|
doubleArrayOf(1.0, 0.0),
|
||||||
|
doubleArrayOf(0.0, 1.0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Der Vorhersageschritt (Prediction).
|
||||||
|
* Aktualisiert den Zustand basierend auf einem Bewegungsschritt (z. B. von PDR).
|
||||||
|
*
|
||||||
|
* @param heading Die Bewegungsrichtung im Bogenmaß.
|
||||||
|
* @param stepLength Die Länge des Schrittes in Metern.
|
||||||
|
*/
|
||||||
|
fun predict(heading: Double, stepLength: Double) {
|
||||||
|
// --- Zustand vorhersagen ---
|
||||||
|
// x_new = x + stepLength * sin(heading)
|
||||||
|
// y_new = y + stepLength * cos(heading)
|
||||||
|
state[0] += stepLength * sin(heading)
|
||||||
|
state[1] += stepLength * cos(heading)
|
||||||
|
|
||||||
|
// --- Kovarianz vorhersagen ---
|
||||||
|
// P_pred = F * P * F^T + Q
|
||||||
|
// Die Jacobi-Matrix F des Prozessmodells ist hier die Einheitsmatrix,
|
||||||
|
// da die neue Position linear von der alten abhängt (x_new = x + delta_x).
|
||||||
|
// Daher vereinfacht sich die Formel zu: P_pred = P + Q.
|
||||||
|
covariance[0][0] += processNoiseQ[0][0]
|
||||||
|
covariance[1][1] += processNoiseQ[1][1]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Der Korrekturschritt (Update).
|
||||||
|
* Korrigiert den Zustand basierend auf einer absoluten Positionsmessung (z. B. von BLE).
|
||||||
|
*
|
||||||
|
* @param blePosition Die gemessene Position.
|
||||||
|
* @param measurementNoise Die Varianz des Messrauschens. Repräsentiert die
|
||||||
|
* Unsicherheit der BLE-Positionsmessung.
|
||||||
|
*/
|
||||||
|
fun update(blePosition: Position, measurementNoise: Double = 2.0) {
|
||||||
|
// --- Messrauschkovarianz R ---
|
||||||
|
val r = arrayOf(
|
||||||
|
doubleArrayOf(measurementNoise, 0.0),
|
||||||
|
doubleArrayOf(0.0, measurementNoise)
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- Messmodell H ---
|
||||||
|
// Das Messmodell ist linear und direkt, da die Messung [x_ble, y_ble]
|
||||||
|
// direkt dem Zustand [x, y] entspricht.
|
||||||
|
// H ist daher die 2x2-Einheitsmatrix.
|
||||||
|
|
||||||
|
// --- Innovations-Kovarianz S ---
|
||||||
|
// S = H * P * H^T + R
|
||||||
|
// Da H die Einheitsmatrix ist: S = P + R
|
||||||
|
val s = arrayOf(
|
||||||
|
doubleArrayOf(covariance[0][0] + r[0][0], covariance[0][1]),
|
||||||
|
doubleArrayOf(covariance[1][0], covariance[1][1] + r[1][1])
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- Inverse von S berechnen ---
|
||||||
|
val detS = s[0][0] * s[1][1] - s[0][1] * s[1][0]
|
||||||
|
if (detS == 0.0) return // Singularität vermeiden
|
||||||
|
val invDetS = 1.0 / detS
|
||||||
|
val sInverse = arrayOf(
|
||||||
|
doubleArrayOf(s[1][1] * invDetS, -s[0][1] * invDetS),
|
||||||
|
doubleArrayOf(-s[1][0] * invDetS, s[0][0] * invDetS)
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- Kalman-Gewinn K ---
|
||||||
|
// K = P * H^T * S^-1
|
||||||
|
// Da H die Einheitsmatrix ist: K = P * S^-1
|
||||||
|
val k = arrayOf(
|
||||||
|
doubleArrayOf(
|
||||||
|
covariance[0][0] * sInverse[0][0] + covariance[0][1] * sInverse[1][0],
|
||||||
|
covariance[0][0] * sInverse[0][1] + covariance[0][1] * sInverse[1][1]
|
||||||
|
),
|
||||||
|
doubleArrayOf(
|
||||||
|
covariance[1][0] * sInverse[0][0] + covariance[1][1] * sInverse[1][0],
|
||||||
|
covariance[1][0] * sInverse[0][1] + covariance[1][1] * sInverse[1][1]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- Zustand aktualisieren ---
|
||||||
|
// state_new = state + K * (z - H * state)
|
||||||
|
// Da H die Einheitsmatrix ist: state_new = state + K * (z - state)
|
||||||
|
val measurement = doubleArrayOf(blePosition.x, blePosition.y)
|
||||||
|
val innovation = doubleArrayOf(measurement[0] - state[0], measurement[1] - state[1])
|
||||||
|
|
||||||
|
state[0] += k[0][0] * innovation[0] + k[0][1] * innovation[1]
|
||||||
|
state[1] += k[1][0] * innovation[0] + k[1][1] * innovation[1]
|
||||||
|
|
||||||
|
// --- Kovarianz aktualisieren ---
|
||||||
|
// P_new = (I - K * H) * P
|
||||||
|
// Da H die Einheitsmatrix ist: P_new = (I - K) * P
|
||||||
|
val iMinusK = arrayOf(
|
||||||
|
doubleArrayOf(1.0 - k[0][0], -k[0][1]),
|
||||||
|
doubleArrayOf(-k[1][0], 1.0 - k[1][1])
|
||||||
|
)
|
||||||
|
|
||||||
|
val newCovariance = arrayOf(
|
||||||
|
doubleArrayOf(
|
||||||
|
iMinusK[0][0] * covariance[0][0] + iMinusK[0][1] * covariance[1][0],
|
||||||
|
iMinusK[0][0] * covariance[0][1] + iMinusK[0][1] * covariance[1][1]
|
||||||
|
),
|
||||||
|
doubleArrayOf(
|
||||||
|
iMinusK[1][0] * covariance[0][0] + iMinusK[1][1] * covariance[1][0],
|
||||||
|
iMinusK[1][0] * covariance[0][1] + iMinusK[1][1] * covariance[1][1]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
// Minimale Kovarianz erzwingen, damit der Filter nie zu "sicher" wird
|
||||||
|
// und immer auf neue Messungen reagiert
|
||||||
|
val minCovariance = 0.1
|
||||||
|
newCovariance[0][0] = newCovariance[0][0].coerceAtLeast(minCovariance)
|
||||||
|
newCovariance[1][1] = newCovariance[1][1].coerceAtLeast(minCovariance)
|
||||||
|
|
||||||
|
covariance = newCovariance
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt die aktuell geschätzte Position zurück.
|
||||||
|
*
|
||||||
|
* @return Die fusionierte Position als [Position]-Objekt.
|
||||||
|
*/
|
||||||
|
fun getPosition(): Position {
|
||||||
|
return Position(state[0], state[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
+189
@@ -0,0 +1,189 @@
|
|||||||
|
package com.erik.bleindoorpositioning.positioning
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.hardware.Sensor
|
||||||
|
import android.hardware.SensorEvent
|
||||||
|
import android.hardware.SensorEventListener
|
||||||
|
import android.hardware.SensorManager
|
||||||
|
import android.util.Log
|
||||||
|
import kotlin.math.atan2
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback-Interface für Schritt-Events
|
||||||
|
* @param heading Bewegungsrichtung in Radiant (0 = Nord, π/2 = Ost)
|
||||||
|
* @param stepLength Schrittlänge in Metern
|
||||||
|
*/
|
||||||
|
fun interface StepListener {
|
||||||
|
fun onStep(heading: Double, stepLength: Double)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SensorFusionManager für Pedestrian Dead Reckoning (PDR)
|
||||||
|
*
|
||||||
|
* Nutzt Android-Sensoren:
|
||||||
|
* - TYPE_STEP_DETECTOR: Schritterkennung
|
||||||
|
* - TYPE_ROTATION_VECTOR: Geräteausrichtung (Azimuth/Heading)
|
||||||
|
* - TYPE_LINEAR_ACCELERATION: Stillstandserkennung + dynamische Schrittlänge
|
||||||
|
*
|
||||||
|
* @param context Android Context für SensorManager-Zugriff
|
||||||
|
* @param listener Callback für Schritt-Events
|
||||||
|
*/
|
||||||
|
class SensorFusionManager(
|
||||||
|
context: Context,
|
||||||
|
private val listener: StepListener
|
||||||
|
) : SensorEventListener {
|
||||||
|
|
||||||
|
private val sensorManager: SensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
||||||
|
|
||||||
|
// Sensoren
|
||||||
|
private val stepDetector: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_DETECTOR)
|
||||||
|
private val rotationVector: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)
|
||||||
|
private val linearAcceleration: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_LINEAR_ACCELERATION)
|
||||||
|
|
||||||
|
val isAvailable: Boolean = (stepDetector != null && rotationVector != null && linearAcceleration != null)
|
||||||
|
|
||||||
|
// State
|
||||||
|
private var currentHeading: Double = 0.0 // Azimuth in Radiant
|
||||||
|
private val accelerationWindow = ArrayDeque<Float>(ACCELERATION_WINDOW_SIZE)
|
||||||
|
private var lastAccelerationMagnitude: Float = 0f
|
||||||
|
|
||||||
|
// Konfiguration
|
||||||
|
private val baseStepLength: Double = 0.7 // Meter
|
||||||
|
private val minStepLength: Double = 0.5 // Meter
|
||||||
|
private val maxStepLength: Double = 1.0 // Meter
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (!isAvailable) {
|
||||||
|
val missingSensors = mutableListOf<String>()
|
||||||
|
if (stepDetector == null) missingSensors.add("TYPE_STEP_DETECTOR")
|
||||||
|
if (rotationVector == null) missingSensors.add("TYPE_ROTATION_VECTOR")
|
||||||
|
if (linearAcceleration == null) missingSensors.add("TYPE_LINEAR_ACCELERATION")
|
||||||
|
Log.w(TAG, "SensorFusionManager is not available. Missing sensors: ${missingSensors.joinToString()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Startet Sensor-Listeners
|
||||||
|
*/
|
||||||
|
fun start() {
|
||||||
|
if (!isAvailable) return
|
||||||
|
stepDetector?.let {
|
||||||
|
sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_FASTEST)
|
||||||
|
}
|
||||||
|
rotationVector?.let {
|
||||||
|
sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_GAME)
|
||||||
|
}
|
||||||
|
linearAcceleration?.let {
|
||||||
|
sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_FASTEST)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stoppt Sensor-Listeners
|
||||||
|
*/
|
||||||
|
fun stop() {
|
||||||
|
if (!isAvailable) return
|
||||||
|
sensorManager.unregisterListener(this)
|
||||||
|
accelerationWindow.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSensorChanged(event: SensorEvent) {
|
||||||
|
when (event.sensor.type) {
|
||||||
|
Sensor.TYPE_STEP_DETECTOR -> handleStepDetected()
|
||||||
|
Sensor.TYPE_ROTATION_VECTOR -> handleRotationVector(event.values)
|
||||||
|
Sensor.TYPE_LINEAR_ACCELERATION -> handleLinearAcceleration(event.values)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
|
||||||
|
// Nicht benötigt für PDR
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verarbeitet Schritt-Event
|
||||||
|
* Prüft Stillstand und berechnet dynamische Schrittlänge
|
||||||
|
*/
|
||||||
|
private fun handleStepDetected() {
|
||||||
|
// Stillstandserkennung: Varianz der Acceleration prüfen
|
||||||
|
if (isStationary()) {
|
||||||
|
return // Kein Step wenn Nutzer steht
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamische Schrittlänge basierend auf Acceleration-Magnitude
|
||||||
|
val stepLength = calculateStepLength(lastAccelerationMagnitude)
|
||||||
|
|
||||||
|
listener.onStep(currentHeading, stepLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extrahiert Azimuth (Heading) aus Rotation Vector
|
||||||
|
* @param values Rotation Vector [x, y, z, optional scalar]
|
||||||
|
*/
|
||||||
|
private fun handleRotationVector(values: FloatArray) {
|
||||||
|
val rotationMatrix = FloatArray(9)
|
||||||
|
SensorManager.getRotationMatrixFromVector(rotationMatrix, values)
|
||||||
|
|
||||||
|
val orientation = FloatArray(3)
|
||||||
|
SensorManager.getOrientation(rotationMatrix, orientation)
|
||||||
|
|
||||||
|
// orientation[0] = Azimuth (um Z-Achse, -π bis π)
|
||||||
|
// 0 = Nord, π/2 = Ost, -π/2 = West, ±π = Süd
|
||||||
|
currentHeading = orientation[0].toDouble()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verarbeitet Linear Acceleration für Stillstandserkennung
|
||||||
|
* @param values [x, y, z] Acceleration in m/s² (ohne Gravitation)
|
||||||
|
*/
|
||||||
|
private fun handleLinearAcceleration(values: FloatArray) {
|
||||||
|
val magnitude = sqrt(
|
||||||
|
values[0] * values[0] +
|
||||||
|
values[1] * values[1] +
|
||||||
|
values[2] * values[2]
|
||||||
|
)
|
||||||
|
|
||||||
|
lastAccelerationMagnitude = magnitude
|
||||||
|
|
||||||
|
// Sliding Window für Varianz-Berechnung
|
||||||
|
if (accelerationWindow.size >= ACCELERATION_WINDOW_SIZE) {
|
||||||
|
accelerationWindow.removeFirst()
|
||||||
|
}
|
||||||
|
accelerationWindow.addLast(magnitude)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft ob Nutzer stillsteht (geringe Varianz der Acceleration)
|
||||||
|
* @return true wenn stillstehend
|
||||||
|
*/
|
||||||
|
private fun isStationary(): Boolean {
|
||||||
|
if (accelerationWindow.size < ACCELERATION_WINDOW_SIZE) {
|
||||||
|
return false // Noch nicht genug Daten
|
||||||
|
}
|
||||||
|
|
||||||
|
val mean = accelerationWindow.average()
|
||||||
|
val variance = accelerationWindow.map { (it - mean) * (it - mean) }.average()
|
||||||
|
|
||||||
|
return variance < STATIONARY_VARIANCE_THRESHOLD
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Berechnet dynamische Schrittlänge basierend auf Acceleration
|
||||||
|
* Höhere Beschleunigung = längerer Schritt (Laufen vs. Gehen)
|
||||||
|
* @param accelerationMagnitude Betrag der Linear Acceleration in m/s²
|
||||||
|
* @return Schrittlänge in Metern (zwischen minStepLength und maxStepLength)
|
||||||
|
*/
|
||||||
|
private fun calculateStepLength(accelerationMagnitude: Float): Double {
|
||||||
|
// Mapping: 0-2 m/s² (Gehen) -> 0.5-0.7m, 2-5 m/s² (schnelles Gehen/Laufen) -> 0.7-1.0m
|
||||||
|
val normalized = (accelerationMagnitude / 5.0).coerceIn(0.0, 1.0)
|
||||||
|
val stepLength = baseStepLength + normalized * (maxStepLength - baseStepLength)
|
||||||
|
|
||||||
|
return stepLength.coerceIn(minStepLength, maxStepLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "SensorFusionManager"
|
||||||
|
private const val ACCELERATION_WINDOW_SIZE = 10 // Anzahl Samples für Varianz
|
||||||
|
private const val STATIONARY_VARIANCE_THRESHOLD = 0.05 // m²/s⁴ (experimentell anpassen)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,6 +41,9 @@ fun PositioningScreen(
|
|||||||
) {
|
) {
|
||||||
val permissions = buildList {
|
val permissions = buildList {
|
||||||
add(Manifest.permission.ACCESS_FINE_LOCATION)
|
add(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
add(Manifest.permission.ACTIVITY_RECOGNITION)
|
||||||
|
}
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
add(Manifest.permission.BLUETOOTH_SCAN)
|
add(Manifest.permission.BLUETOOTH_SCAN)
|
||||||
add(Manifest.permission.BLUETOOTH_CONNECT)
|
add(Manifest.permission.BLUETOOTH_CONNECT)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ Minimiere Claude-Tokens:
|
|||||||
- eigenständig große Implementierungen schreiben
|
- eigenständig große Implementierungen schreiben
|
||||||
- ganze Dateien neu ausgeben (außer Gemini liefert es als Diff)
|
- ganze Dateien neu ausgeben (außer Gemini liefert es als Diff)
|
||||||
- lange Analysen/Prosatexte erzeugen
|
- lange Analysen/Prosatexte erzeugen
|
||||||
- “Best guess” ohne Gemini machen
|
- "Best guess" ohne Gemini machen
|
||||||
|
|
||||||
4) Kontext-Minimierung:
|
4) Kontext-Minimierung:
|
||||||
- Öffne nur Dateien, die wirklich benötigt werden.
|
- Öffne nur Dateien, die wirklich benötigt werden.
|
||||||
@@ -79,35 +79,37 @@ D) Ausführen:
|
|||||||
|
|
||||||
## #############################################################################################
|
## #############################################################################################
|
||||||
|
|
||||||
## Project Overview
|
## Projektübersicht
|
||||||
|
|
||||||
BLE Indoor Positioning system consisting of two components:
|
BLE-basiertes Indoor-Positionierungssystem, bestehend aus zwei Komponenten:
|
||||||
1. **Android App** (Kotlin/Compose) — estimates user position via RSSI-based trilateration. Active version in `App v2/`.
|
1. **Android App** (Kotlin/Compose) — schätzt die Nutzerposition mittels RSSI-basierter Trilateration. Die aktive Version befindet sich in `App v2/`. **Die Version in `App v1_2/` ist eine Legacy-Version und soll NICHT bearbeitet werden.**
|
||||||
2. **ESP32 Firmware** (Arduino) — iBeacon sender on Seeed XIAO ESP32-C3. Active version in `ESP32 iBeacon Code/ESP32_iBeacon_v2/`.
|
2. **ESP32 Firmware** (Arduino) — iBeacon-Sender auf einem Seeed XIAO ESP32-C3. Die aktive Version befindet sich in `ESP32 iBeacon Code/ESP32_iBeacon_v2/`.
|
||||||
|
|
||||||
## Build & Run
|
## Build & Run
|
||||||
|
|
||||||
### Android App (from `App v2/`)
|
### Android App
|
||||||
|
|
||||||
|
Alle Gradle-Kommandos müssen aus dem `App v2/`-Verzeichnis ausgeführt werden (`cd App v2/`).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./gradlew assembleDebug # Debug build
|
./gradlew assembleDebug # Debug-Build
|
||||||
./gradlew test # Unit tests
|
./gradlew test # Unit-Tests
|
||||||
./gradlew connectedAndroidTest # Instrumented tests (device/emulator required)
|
./gradlew connectedAndroidTest # Instrumentierte Tests (Gerät/Emulator erforderlich)
|
||||||
./gradlew clean assembleDebug # Clean build
|
./gradlew clean assembleDebug # Clean-Build
|
||||||
|
|
||||||
# Single test class
|
# Einzelne Testklasse
|
||||||
./gradlew test --tests "com.erik.bleindoorpositioning.ExampleUnitTest"
|
./gradlew test --tests "com.erik.bleindoorpositioning.ExampleUnitTest"
|
||||||
```
|
```
|
||||||
|
|
||||||
- Compile/Target SDK: 36, Min SDK: 26
|
- Compile/Target SDK: 36, Min SDK: 26
|
||||||
- Java 11 compatibility, Kotlin 2.0.21 with Compose compiler plugin
|
- Java 11-kompatibel, Kotlin 2.0.21 mit Compose Compiler-Plugin
|
||||||
- AGP 9.0.0, Compose BOM 2024.09.00
|
- AGP 9.0.0, Compose BOM 2024.09.00
|
||||||
- Dependencies managed via `gradle/libs.versions.toml`
|
- Abhängigkeiten werden über `gradle/libs.versions.toml` verwaltet.
|
||||||
- Additional: kotlinx-serialization (JSON config persistence), material-icons-extended (Sonne/Mond Icons)
|
- Zusätzlich: kotlinx-serialization (Persistenz der JSON-Konfiguration), material-icons-extended (Sonne/Mond-Icons)
|
||||||
|
|
||||||
### ESP32 Firmware
|
### ESP32 Firmware
|
||||||
|
|
||||||
Flash via Arduino IDE (Board: XIAO_ESP32C3). Before flashing each beacon, set `BEACON_MINOR` in `ESP32_iBeacon_v2.ino` to a unique ID (1, 2, 3, ...).
|
Flashen über die Arduino IDE (Board: XIAO_ESP32C3). Vor dem Flashen jedes Beacons muss `BEACON_MINOR` in `ESP32_iBeacon_v2.ino` auf eine eindeutige ID (1, 2, 3, ...) gesetzt werden.
|
||||||
|
|
||||||
**ESP32-C3 Besonderheiten:**
|
**ESP32-C3 Besonderheiten:**
|
||||||
- Nutzt native USB-CDC → `delay(2000)` am Anfang von `setup()` nötig, damit Serial-Ausgaben nicht verloren gehen
|
- Nutzt native USB-CDC → `delay(2000)` am Anfang von `setup()` nötig, damit Serial-Ausgaben nicht verloren gehen
|
||||||
@@ -115,56 +117,82 @@ Flash via Arduino IDE (Board: XIAO_ESP32C3). Before flashing each beacon, set `B
|
|||||||
- Header: `esp_mac.h` statt `esp_system.h` (ESP-IDF v4.0+), `esp_bt_device.h` existiert nicht auf ESP32-C3
|
- Header: `esp_mac.h` statt `esp_system.h` (ESP-IDF v4.0+), `esp_bt_device.h` existiert nicht auf ESP32-C3
|
||||||
- Optional: `USE_CUSTOM_MAC` aktivieren falls ein Android-Gerät den Beacon nicht findet (MAC-Blacklisting-Workaround)
|
- Optional: `USE_CUSTOM_MAC` aktivieren falls ein Android-Gerät den Beacon nicht findet (MAC-Blacklisting-Workaround)
|
||||||
|
|
||||||
## Architecture
|
## Architektur
|
||||||
|
|
||||||
### Android App
|
### Android App
|
||||||
|
|
||||||
**Data flow:** BLE Hardware → `BleScanner` → `BeaconFilterManager` (RSSI filtering + Ausreißer-Erkennung) → `PositioningCalculator` (Trilateration + Raumbegrenzung) → `PositionSmoother` (2D-EMA Glättung) → UI via `StateFlow`
|
**Datenfluss:**
|
||||||
|
```
|
||||||
|
BLE Hardware → BleScanner → BeaconFilterManager (RSSI-Filterung + Ausreißer-Erkennung)
|
||||||
|
→ PositionCalculator (Trilateration + Raumbegrenzung) → EKF update() ─┐
|
||||||
|
├→ EKF.getPosition() → PositionSmoother (2D-EMA) → UI via StateFlow
|
||||||
|
Step Detector → SensorFusionManager (PDR: Heading + Schrittlänge) → EKF predict() ─┘
|
||||||
|
```
|
||||||
|
|
||||||
Key layers (`app/src/main/java/com/erik/bleindoorpositioning/`):
|
Wichtige Layer (`app/src/main/java/com/erik/bleindoorpositioning/`):
|
||||||
|
|
||||||
- **`ble/`**: `BleScanner` parses iBeacon packets (Apple 0x4C00 manufacturer ID), validates UUID, extracts Minor/Major/RSSI. Filtert veraltete Beacons (>3s) vor Positionsberechnung aus. Auto-Restart alle 25 Min (Android BLE Scan-Limit). Exponential Backoff bei Scan-Fehlern. `reloadConfig()` lädt Config dynamisch neu. `BeaconData` holds default calibration constants (RSSI_AT_ONE_METER, PATH_LOSS_EXPONENT, BEACON_UUID).
|
- **`ble/`**: `BleScanner` parst iBeacon-Pakete (Apple 0x4C00 Manufacturer-ID), validiert die UUID und extrahiert Minor/Major/RSSI. Veraltete Beacons (>3s) werden vor der Positionsberechnung ausgefiltert. Auto-Restart alle 25 Min (Android BLE Scan-Limit). Exponentielles Backoff bei Scan-Fehlern. `reloadConfig()` lädt die Konfiguration dynamisch neu. `BeaconData` enthält die `BEACON_UUID` und Standard-Kalibrierungskonstanten.
|
||||||
- **`positioning/`**: `RssiFilter` interface with `KalmanFilter`/`ExponentialMovingAverageFilter`/`MovingAverageFilter` implementations. `BeaconFilterManager` manages one filter per beacon (keyed by Minor ID) with outlier rejection (Schwellwert: 20 dBm). `PositioningCalculator` runs a 3-stage pipeline: IDW weighted centroid (exponent=4.0) → Gauss-Newton trilateration (weight=1/d³) → adaptive blending based on minimum distance (0.5m=100% IDW, 1.5m=100% Gauss-Newton), mit Raumbegrenzung (Clamping auf dynamische Stockwerk-Grenzen aus Räumen+Beacons). Beacon-spezifische `rssiAtOneMeter` und `pathLossExponent` Werte. `PositionSmoother` applies 2D EMA (alpha=0.15) on the final position to reduce jumping.
|
- **`positioning/`**: `RssiFilter`-Interface mit `KalmanFilter`-, `ExponentialMovingAverageFilter`- und `MovingAverageFilter`-Implementierungen. Aktuell wird `MovingAverageFilter(windowSize=20)` verwendet. Der `BeaconFilterManager` verwaltet einen Filter pro Beacon (Schlüssel: Minor ID) mit Ausreißer-Erkennung (Schwellwert: 20 dBm). `PositionCalculator` führt eine 3-stufige Pipeline aus: IDW Weighted Centroid (Exponent=4.0) → Gauss-Newton-Trilateration (Gewicht=1/d³) → adaptives Blending basierend auf der minimalen Distanz (0.5m=100% IDW, 1.5m=100% Gauss-Newton), mit Raumbegrenzung (Clamping auf dynamische Stockwerk-Grenzen). Er verwendet Beacon-spezifische Kalibrierungswerte (siehe Sektion "Kalibrierungsparameter"). `PositionSmoother` wendet einen 2D-EMA (alpha=0.25) auf die finale Position an, um Sprünge zu reduzieren. `PositionKalmanFilter` implementiert einen Extended Kalman Filter (EKF) zur Sensorfusion: `predict()` mit PDR-Schritten, `update()` mit BLE-Positionen (Zustand=[x,y], Prozessrauschen=0.5, Messrauschen=2.0, minimale Kovarianz=0.1 um Einfrieren über Zeit zu verhindern). `SensorFusionManager` ist **optional** und stellt PDR (Pedestrian Dead Reckoning) über Android-Sensoren (`TYPE_STEP_DETECTOR`, `TYPE_ROTATION_VECTOR`, `TYPE_LINEAR_ACCELERATION`) bereit. Er verfügt über Stillstandserkennung (Varianz-Schwellwert=0.05, Fenster=10 Samples) und eine dynamische Schrittlängenschätzung (0.5–1.0m). Sensor-Delays: `SENSOR_DELAY_FASTEST` für Step Detector und Linear Acceleration, `SENSOR_DELAY_GAME` für Rotation Vector. **Graceful Degradation:** Wenn die benötigten Sensoren auf dem Gerät nicht verfügbar sind, läuft die App in einem reinen BLE-Modus ohne Schrittverfolgung. Die Verfügbarkeit der Sensoren wird über die `isAvailable`-Property geprüft.
|
||||||
- **`map/`**: `CoordinateMapper` transforms meters↔pixels with dynamic bounding box (supports negative coordinates). `FloorplanView` draws floor with multiple rooms, beacons, distance circles, and position on a Compose Canvas. Interactive: Pinch-to-Zoom (0.5x–5x), Pan, Double-Tap reset. Zoom-kompensierte Schrift/Borders. Dark Mode kompatible Farben via MaterialTheme.
|
- **`map/`**: `CoordinateMapper` transformiert Meter↔Pixel mit einer dynamischen Bounding Box (unterstützt negative Koordinaten). `FloorplanView` zeichnet den Grundriss mit mehreren Räumen, Beacons, Distanzkreisen und der Position auf einer Compose Canvas. Interaktiv: Pinch-to-Zoom (0.5x–5x), Schwenken, Doppeltipp zum Zurücksetzen. Zoom-kompensierte Schriftgrößen und Strichstärken. Dark-Mode-kompatible Farben über MaterialTheme.
|
||||||
- **`ui/`**: Tab-Navigation mit `MainScreen` (Positioning + Config Tabs). Dark/Light Mode Toggle (Sonne/Mond-Icon, persistent). `PositioningScreen`: Karte mit Vollbild-Modus, einklappbare Beacon-Liste, Scan-Kontrolle. `ConfigScreen`: Scrollbar mit einklappbaren Sektionen (Stockwerk, Räume, Beacons, Anlernen). Auto-Save bei Änderungen. Raum-Verwaltung (Add/Edit/Delete), Beacon-Verwaltung mit Detail-Dialog (Position, RSSI@1m, Path Loss Exponent), Beacon-Anlern-Funktion via BLE-Scan.
|
- **`ui/`**: Tab-Navigation mit `MainScreen` (Positionierungs- und Konfigurations-Tabs). Dark/Light-Mode-Umschalter (Sonne/Mond-Icon, persistent). `PositioningScreen`: Karte mit Vollbild-Modus, einklappbare Beacon-Liste, Scan-Kontrolle. `ConfigScreen`: Scrollbar mit einklappbaren Sektionen (Stockwerk, Räume, Beacons, Anlernen). Auto-Save bei Änderungen. Raum-Verwaltung (Hinzufügen/Bearbeiten/Löschen), Beacon-Verwaltung mit Detail-Dialog (Position, RSSI @1m, Path Loss Exponent), Beacon-Anlern-Funktion via BLE-Scan.
|
||||||
- **`data/`**: `AppConfig` (floorWidth/Height, rooms, beacons). `ConfigRepository` persistiert Config in SharedPreferences (JSON/kotlinx.serialization). Legacy-Migration für alte Configs.
|
- **`data/`**: `AppConfig` (floorWidth/Height, rooms, beacons). `ConfigRepository` persistiert die Konfiguration in den SharedPreferences (JSON/kotlinx.serialization). Inklusive Legacy-Migration für alte Konfigurationsformate.
|
||||||
- **`model/`**: `BeaconConfig` (position, rssiAtOneMeter, pathLossExponent), `RoomConfig` (name, position, size), `ScannedBeacon` (live scan data with `lastSeen` timestamp, uuid), `Position`/`PositionResult`.
|
- **`model/`**: `BeaconConfig` (Position, rssiAtOneMeter, pathLossExponent), `RoomConfig` (Name, Position, Größe), `ScannedBeacon` (Live-Scan-Daten mit `lastSeen`-Zeitstempel, UUID), `Position`/`PositionResult`.
|
||||||
|
|
||||||
`MainActivity` creates `BleScanner` + `ConfigRepository` and passes them to `MainScreen`. Lifecycle-Handling: onPause stoppt Scan, onResume startet wieder. No DI framework — direct instantiation. All state is reactive via Kotlin `StateFlow`, observed in Compose with `collectAsState()`. Material 3 Theme mit Schwarz/Lila Farbschema.
|
`MainActivity` erzeugt `BleScanner` und `ConfigRepository` und übergibt sie an `MainScreen`. Lifecycle-Handling: `onPause` stoppt den Scan, `onResume` startet ihn wieder. Kein DI-Framework — direkte Instanziierung. Der gesamte Zustand ist reaktiv über Kotlin `StateFlow` und wird in Compose mit `collectAsState()` beobachtet. Material 3 Theme mit einem Schwarz/Lila-Farbschema.
|
||||||
|
|
||||||
### ESP32 Firmware
|
### ESP32 Firmware
|
||||||
|
|
||||||
Pure iBeacon advertiser (no GATT service, no connections). Key configuration:
|
Reiner iBeacon-Advertiser (kein GATT-Service, keine Verbindungen). Wichtige Konfiguration:
|
||||||
- Advertising interval: 100ms (fixed)
|
- Advertising-Intervall: 100ms (fest)
|
||||||
- TX Power: +9 dBm (maximum for ESP32-C3), set via `esp_ble_tx_power_set()` for both ADV and DEFAULT
|
- TX Power: +9 dBm (Maximum für ESP32-C3), gesetzt über `esp_ble_tx_power_set()` für ADV und DEFAULT
|
||||||
- Signal Power: -65 dBm @ 1m (must match `RSSI_AT_ONE_METER` in Android app)
|
- Signal Power: -65 dBm @ 1m (muss mit `rssiAtOneMeter` in der Android-App übereinstimmen)
|
||||||
- Advertising Flags: 0x06 (LE General Discoverable + BR/EDR Not Supported)
|
- Advertising-Flags: 0x06 (LE General Discoverable + BR/EDR Not Supported)
|
||||||
- BLE Sleep enabled between advertising events for battery operation
|
- BLE Sleep zwischen den Advertising-Events für Batteriebetrieb aktiviert
|
||||||
- `BEACON_MINOR` identifies each physical beacon — must match beacon Minor ID in the app's Config tab
|
- `BEACON_MINOR` identifiziert jeden physischen Beacon — muss mit der Beacon Minor ID in der App-Konfiguration übereinstimmen
|
||||||
- Optional MAC-Override (`USE_CUSTOM_MAC`) for Android BLE-Blacklisting workaround
|
- Optionaler MAC-Override (`USE_CUSTOM_MAC`) als Workaround für Android BLE-Blacklisting
|
||||||
|
|
||||||
## Calibration Parameters
|
## Kalibrierungsparameter
|
||||||
|
|
||||||
### Android App
|
### Android App
|
||||||
Beacon-Kalibrierung ist jetzt **pro Beacon konfigurierbar** im Config-Tab der App:
|
Die Beacon-Kalibrierung ist **pro Beacon konfigurierbar** im Config-Tab der App:
|
||||||
- `rssiAtOneMeter`: Measured RSSI at 1m distance (default: -65.0 dBm)
|
- `rssiAtOneMeter`: Gemessener RSSI bei 1m Abstand (Standard: -65.0 dBm)
|
||||||
- `pathLossExponent`: Environment-dependent (default: 5.5; free-space=2.0, indoor=2.5-5.5)
|
- `pathLossExponent`: Umgebungsabhängig (Standard: 5.5; Freifeld=2.0, Innenraum=2.5-5.5)
|
||||||
- Position (x, y) in meters — supports negative coordinates
|
- Position (x, y) in Metern — unterstützt negative Koordinaten
|
||||||
|
|
||||||
Globale Defaults in `BeaconData.kt`:
|
Globale Standardwerte in `BeaconData.kt`:
|
||||||
- `RSSI_AT_ONE_METER`: Default -65.0 dBm
|
- `RSSI_AT_ONE_METER`: Standardwert -65.0 dBm
|
||||||
- `PATH_LOSS_EXPONENT`: Default 5.5
|
- `PATH_LOSS_EXPONENT`: Standardwert 5.5
|
||||||
- `BEACON_UUID`: Must match UUID on ESP32 beacons (`87b99b2c-9080-4d04-8f00-a5d9a3f9a0e7`)
|
- `BEACON_UUID`: Muss mit der UUID auf den ESP32-Beacons übereinstimmen (`87b99b2c-9080-4d04-8f00-a5d9a3f9a0e7`)
|
||||||
|
|
||||||
Stockwerk-Konfiguration (Config-Tab):
|
Stockwerk-Konfiguration (Config-Tab):
|
||||||
- `floorWidth` / `floorHeight`: Stockwerk-Dimensionen in Metern
|
- `floorWidth` / `floorHeight`: Stockwerk-Dimensionen in Metern
|
||||||
- `rooms`: Liste von Räumen (Name, Position, Größe) — beliebig viele, auch an negativen Koordinaten
|
- `rooms`: Liste von Räumen (Name, Position, Größe) — beliebig viele, auch an negativen Koordinaten
|
||||||
|
|
||||||
### ESP32 (ESP32_iBeacon_v2.ino)
|
### ESP32 (ESP32_iBeacon_v2.ino)
|
||||||
- `SIGNAL_POWER_1M`: Must match `RSSI_AT_ONE_METER` in the Android app (currently 0xBF = -65 dBm)
|
- `SIGNAL_POWER_1M`: Muss mit `rssiAtOneMeter` in der Android-App übereinstimmen (aktuell 0xBF = -65 dBm)
|
||||||
- `BEACON_MINOR`: Unique per beacon, must match beacon Minor ID configured in the app's Config tab
|
- `BEACON_MINOR`: Eindeutig pro Beacon, muss mit der Beacon Minor ID übereinstimmen, die im Config-Tab der App konfiguriert ist.
|
||||||
|
|
||||||
## Known Issues / Troubleshooting
|
## Tuning-Parameter (Positionierungspipeline)
|
||||||
|
|
||||||
|
Die Positionierung durchläuft mehrere Glättungsstufen. Beim Tuning beachten:
|
||||||
|
|
||||||
|
**Pipeline:** RSSI → MovingAverageFilter → PositionCalculator → EKF → PositionSmoother → UI
|
||||||
|
|
||||||
|
| Parameter | Datei | Aktuell | Effekt |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `MovingAverageFilter(windowSize)` | BleScanner.kt | 20 | Mehr = stabilere RSSI, aber trägere Reaktion |
|
||||||
|
| `BeaconFilterManager(outlierThreshold)` | BleScanner.kt | 20.0 dBm | Höher = mehr RSSI-Sprünge erlaubt |
|
||||||
|
| `PositionKalmanFilter(processNoise)` | BleScanner.kt | 0.5 | Höher = EKF folgt BLE schneller, PDR weniger Gewicht |
|
||||||
|
| `positionKalmanFilter.update(measurementNoise)` | BleScanner.kt | 2.0 | Niedriger = mehr Vertrauen in BLE-Position |
|
||||||
|
| `PositionSmoother(alpha)` | BleScanner.kt | 0.25 | Höher = reaktiver, niedriger = glatter |
|
||||||
|
| `BEACON_MAX_AGE_MS` | BleScanner.kt | 3000 | Kürzer = weniger Einfluss veralteter Beacons |
|
||||||
|
| `minCovariance` | PositionKalmanFilter.kt | 0.1 | Verhindert EKF-Einfrieren über Zeit |
|
||||||
|
| `STATIONARY_VARIANCE_THRESHOLD` | SensorFusionManager.kt | 0.05 | Niedriger = weniger Schritte als Stillstand gefiltert |
|
||||||
|
| `ACCELERATION_WINDOW_SIZE` | SensorFusionManager.kt | 10 | Weniger = schnellere Stillstandserkennung |
|
||||||
|
|
||||||
|
**Bekanntes Problem:** Zu viele Glättungsstufen hintereinander können die Position extrem träge machen. Beim Debugging einzelne Stufen testweise deaktivieren (EKF bypass / Smoother bypass), um die Ursache zu isolieren. Der EKF kann über Zeit einfrieren wenn die Kovarianz zu klein wird — daher `minCovariance`-Clamp.
|
||||||
|
|
||||||
|
## Bekannte Probleme / Fehlerbehebung
|
||||||
|
|
||||||
### ESP32 Beacon nicht von Android erkannt
|
### ESP32 Beacon nicht von Android erkannt
|
||||||
Wenn ein ESP32-C3 Beacon auf einem bestimmten Android-Gerät nicht erkannt wird (auch nicht in nRF Connect), aber auf anderen Geräten sichtbar ist:
|
Wenn ein ESP32-C3 Beacon auf einem bestimmten Android-Gerät nicht erkannt wird (auch nicht in nRF Connect), aber auf anderen Geräten sichtbar ist:
|
||||||
@@ -176,6 +204,6 @@ Wenn ein ESP32-C3 Beacon auf einem bestimmten Android-Gerät nicht erkannt wird
|
|||||||
### ESP32-C3 Serial Monitor leer
|
### ESP32-C3 Serial Monitor leer
|
||||||
Der XIAO ESP32-C3 nutzt native USB-CDC. Frühe `Serial.print()`-Aufrufe gehen verloren. Fix: `delay(2000)` nach `Serial.begin()`.
|
Der XIAO ESP32-C3 nutzt native USB-CDC. Frühe `Serial.print()`-Aufrufe gehen verloren. Fix: `delay(2000)` nach `Serial.begin()`.
|
||||||
|
|
||||||
## Language
|
## Sprache
|
||||||
|
|
||||||
The codebase, comments, and documentation are in German. Respond in German when working with this project.
|
Codebasis, Kommentare und Dokumentation sind auf Deutsch. Antworte auf Deutsch, wenn du mit diesem Projekt arbeitest.
|
||||||
Reference in new issue
Block a user