Integrate asynchronous GPS IMU fusion

This commit is contained in:
2026-07-16 23:49:43 +03:00
parent 38a96dd88f
commit 7cc3eb2a54
80 changed files with 3901 additions and 553 deletions
+135 -13
View File
@@ -12,6 +12,55 @@ Bu bölüm yeni bir geliştirme oturumunun nereden devam edeceğini tek yerde
özetler. Aşağıdaki Python model ve bare-metal C uygulaması aynı problemi çözer,
fakat aynı program değildir:
### Asenkron IMU/GPS düzeltmesi ve sentetik oran matrisi
Önceki prototipte 52 Hz ST akışından yalnız yaklaşık 250 ms'de bir ham örnek
seçiliyordu. Bu inertial downsampling değildir: kısa gyro hareketi iki seçilen
örneğin arasında kalınca değişen gravity vektörü görülüyor, dönüş görülmüyor ve
yanlış yatay ivme entegre ediliyordu. Linux C hızlı-eğim testinde bu yol 8.169 m
tepe yatay hata ve 1.554 m/s kalıcı hız üretti.
Bu seçim katmanı kaldırıldı. Artık her raw sample kendi timestamp'iyle işlenir;
ardışık accel/gyro örnekleri midpoint mechanization ile entegre edilir. GPS,
IMU'dan bağımsız bir position observation'dır. WLS yalnız geçerli olduğunda
ölçüm epoch'unda uygulanır; epoch iki IMU sample'ı arasındaysa input lineer
interpolate edilir, update sonrası kalan IMU geçmişi current-time horizon'a
replay edilir. Ana döngü IMU kuyruğunu WLS'den önce ve sonra tamamen boşaltır.
`async_rate_matrix_test.c` aynı üretim C kaynaklarına 52 Hz timestamp'li IMU,
100/200/1000 ms GPS, 73/310 ms delivery delay, farklı eksenlerde ivmelenme ve
fren, yaw dönüşü ve 10--18 s GPS outage verir. Sonuçlar sırasıyla 0.172/0.180/
0.389 m yatay RMSE ve outage sırasında 0.634/0.671/0.822 m tepe hatadır; üçünde
de GPS recovery kabul edilmiş, false ZUPT ve NIS reject sıfırdır. Tam GPS'siz
koşu da kendine konum feed etmeden yerel dead-reckoning yapar: global origin
false kalır, kovaryans büyür ve 25 s sonunda 3.674 m drift oluşur. Bu drift
gizlenmez; mutlak konumun IMU-only durumda gözlenememesinin beklenen sonucudur.
`gps_reacquisition_test.c`, canlı UART'ta görülen kötü ilk GPS fix'i senaryosunu
ENU'da 80/40/-170 m başlangıç ofsetiyle tekrarlar. Hard NIS kapısı tek başına
sonraki doğru fix'leri sürekli reddettiği için fusion engine zaman tabanlı bir
position reacquisition uygular: son başarılı position fusion üzerinden yedi
saniye timeout geçtikten ve en az üç rejected fix kendi içinde devamlı olduktan
sonra yalnız position state resetlenir. Hız, attitude ve IMU bias'ları korunur.
Test 100/200/1000 ms GPS periyotlarında sıfır son hata üretir; ayrı
150/-120/+80 m tek-fix testi aykırı ölçümün reddedildiğini ve state reset
oluşturmadığını doğrular.
Testin `--trace` seçeneği UART'a benzer `IMU ts=...`, `GPS ts=... arrival=...`
satırları üretir. Standart regression komutu:
```sh
make -C soc/software/fusion/tests test
```
`platform_scenario_matrix_test.c` ayrıca stationary/tilt/vibration, yaya,
trafik ışığında duran otomobil ve ani dönüşlü/yüksek ivmeli uçak profillerini;
200/500/1000 ms GNSS ile 19/20/200/300 ms IMU'nun 48 kombinasyonunda çalıştırır.
GNSS dört uydu kullanır, ilk bölümde 5--10 m, sonra 3--5 m sınıfı yatay gürültü,
3 saniye outage ve 2 saniye tutarlı büyük hata içerir. Kısa hata yedi saniyelik
recovery timeout'unu açamaz. 200/300 ms IMU yüksek dinamikteki doğruluk kaybıyla
birlikte açıkça raporlanır; test bunu daha sık IMU varmış gibi gizlemez.
```text
Offline model:
UART GPS + IMU logları -> Python parser -> fixed/adaptive ESKF -> CSV/grafik/harita
@@ -213,16 +262,16 @@ yoktur.
Temel parçalar:
- `gps_system/src/eskf.c`: ENU konum/hız, body-to-ENU quaternion, accel bias,
- `gps_system/src/fusion/eskf.c`: ENU konum/hız, body-to-ENU quaternion, accel bias,
gyro bias ve 15x15 covariance içeren 15-hata-durumlu ESKF;
- `gps_system/src/fusion_motion.c`: bootstrap/stationary/moving/candidate
- `gps_system/src/fusion/fusion_motion.c`: bootstrap/stationary/moving/candidate
durum makinesi, filtrelenmiş jerk ve ZUPT izni;
- `gps_system/src/fusion_engine.c`: başlangıç kalibrasyonu, asenkron IMU/GPS,
- `gps_system/src/fusion/fusion_engine.c`: başlangıç kalibrasyonu, asenkron IMU/GPS,
NIS kapısı ve gecikmiş GPS için fixed-lag IMU replay;
- `gps_system/src/fusion_gps_adapter.c`: WGS-84 LLH/ECEF/ENU dönüşümü ve
- `gps_system/src/fusion/fusion_gps_adapter.c`: WGS-84 LLH/ECEF/ENU dönüşümü ve
covariance tabanı;
- `gps_system/src/tdk_icm42688.c`: ICM-42688-P SPI/INT1 sürücüsü;
- `gps_system/src/navigation_fusion.c`: GPS ve IMU kuyruklarını algoritmaya
- `gps_system/src/drivers/imu/tdk_icm42688.c`: ICM-42688-P SPI/INT1 sürücüsü;
- `gps_system/src/fusion/navigation_fusion.c`: GPS ve IMU kuyruklarını algoritmaya
bağlayan üst koordinatör.
### ESKF state ve matematik sınırı
@@ -270,6 +319,64 @@ Default ESKF process/başlangıç değerleri:
| Maksimum propagation alt adımı | 20 ms |
| Konum NIS kapısı | 16.266236, 3 DOF %99.9 |
### Innovation rejection, recovery ve katman siniri (2026-07-16)
Standart ESKF position update'i tek basina buyuk ve kalici bir baslangic
ofsetini duzeltemez. GPS--INS residual'i NIS kapisini astiginda olcum state'e
hic uygulanmaz; state GPS'e yaklasmadigi icin sonraki dogru GPS fix'leri de
ayni nedenle sonsuza kadar reddedilebilir. NIS yalniz iki bilgi kaynaginin
tutarsiz oldugunu soyler. GPS'in mi hatali, yoksa ataletsel cozumun mu yanlis
ankrajlandigini matematiksel update tek basina belirleyemez.
Bu nedenle sorumluluk iki katmana ayrilir:
- `eskf.c` innovation, innovation covariance, NIS gate, kabul edilmis Kalman
correction ve covariance bakimindan dogru position-reset primitive'ini
icerir. Rejected olcumu gizlice correction'a sokmaz ve NIS'i bypass etmez.
- `fusion_engine.c` sensor gecmisi ve zaman bilgisine sahip supervisor'dur.
Ardisik rejected GNSS fix'lerinin kendi aralarinda zamansal olarak tutarli
olup olmadigini izler ve gerekli recovery/reset kararini burada verir.
Mevcut yalın recovery, son başarılı position fusion'dan sonra yedi saniyelik
timeout dolmuşsa ve en az üç rejected position fix'i kendi arasında zamansal
tutarlıysa yalnız position state ve position covariance'ini GNSS'e yeniden
ankrajlar. Position cross-covariance'lari gecersiz korelasyon tasimamasi icin
sifirlanir; velocity, attitude ve IMU bias state'leri korunur. Hard NIS gate
aktif kalir ve tek aykiri olcum reset uretemez. Bu bir innovation-bypass
workaround'u degil, kontrollu state recovery mekanizmasidir.
PX4 EKF2 de ayni temel ayrimi yapar. Generic `fuseHorizontalPosition()` yalniz
innovation testinden gecen olcumu fuse eder. GNSS'e baslama, quality check,
`time_last_fuse` timeout'u, state reset, fusion stop ve sensor-fault kararlari
ayri GNSS control akisindadir. Otomatik modda baska yatay konum kaynagi yoksa
uzun fusion timeout'undan sonra GNSS'e reset yapilabilir; alternatif aiding
varsa veya dead-reckoning politikasinda GNSS durdurulabilir/fault kabul
edilebilir. Kaynaklar:
- <https://github.com/PX4/PX4-Autopilot/blob/main/src/modules/ekf2/EKF/aid_sources/gnss/gps_control.cpp>
- <https://github.com/PX4/PX4-Autopilot/blob/main/src/modules/ekf2/EKF/position_fusion.cpp>
- <https://docs.px4.io/main/en/advanced_config/tuning_the_ecl_ekf>
Mevcut recovery PX4'teki aiding yoneticisinin sade bare-metal karsiligidir;
nihai mimari hedefi asagidaki acik GNSS aiding durum makinesidir:
```text
DISABLED -> STARTING -> FUSING -> FAULTED/RECOVERING
```
Karar yalniz rejected-candidate suresine degil son basarili position fusion
zamanina baglanmalidir. Uydu sayisi, finite covariance, DOP, WLS integrity ve
alternatif aiding kaynaklari birlikte degerlendirilmelidir. Tam dort uyduda
WLS residual redundancy/chi-square DOF bulunmadigi icin temporal consistency
daha onemlidir. Ileride Doppler velocity ayri innovation, fusion timeout ve
velocity-reset politikasi ile fuse edilmelidir. Yatay position ve height da
barometre gibi ek kaynaklar geldiginde birbirinden bagimsiz yonetilmelidir.
Baslangic GNSS'i icin mutlak bir dogruluk referansi yoksa tutarli fakat yanlis
bir GNSS kumesini otomatik olarak gercek konumdan ayirmak temel olarak mumkun
degildir. Kisa bir stable-fix/startup-quality penceresi riski azaltir, fakat
spoofing veya tutarli multipath'e karsi tek basina kanit degildir.
### Asenkron veri akışı ve timestamp
IMU ve GPS aynı local monotonic CPU/CLINT tick alanında timestamp edilir.
@@ -313,6 +420,13 @@ verir. Yaw gravity ile gözlenemediği için başlangıçta sıfır referanslı
Benzer şekilde stationary IMU ile yatay accel bias ve küçük tilt tamamen
ayrılamaz; sonraki GPS/kinematik update'ler bu error-state'leri düzeltir.
IMU motion-state eşikleri tanılama için tutulur, fakat ZUPT yetkisini tek başına
vermez. PX4 ayrımına uygun olarak `fusion_rest_detector` titreşim metriklerini
izler ve dışarıdan gelen `platform_landed` kapısıyla birleşir. Prototipin unarmed
bench modu landed=true başlar; gerçek araç kontrolü bu değeri kendi state'inden
güncellemelidir. Sessiz IMU, landed=false iken sabit hızlı hareketi stationary
ilan edemez.
Motion-state default eşikleri:
| Karar parametresi | Değer |
@@ -328,12 +442,20 @@ Motion-state default eşikleri:
| Stationary'e dönüş quiet dwell | 1.0 s |
| Zayıf hareket doğrulaması | 3 ardışık IMU örneği |
Güçlü accel/gyro/jerk veya yüksek ESKF hızı stationary state'ten tek örnekte
çıkabilir. Geri dönüş daha konservatiftir: quiet IMU, düşük ESKF hızı, aktif
GNSS-moving veto olmaması ve 1 s dwell birlikte gerekir. ZUPT ile zero-gyro
update yalnız gerçek `STATIONARY` state'inde, her IMU örneğinde değil 20 Hz'de
uygulanır. ZUPT velocity sigma 0.03 m/s, zero-gyro sigma eksen başına
0.01 deg/s'dir.
Güçlü accel/gyro/jerk veya yüksek ESKF hızı motion tanılamasını tek örnekte
moving'e çıkarabilir. ZUPT ve zero-gyro ise yalnız ardışık iki timestamp'li
örnekte dış supervisor `vehicle_at_rest=true` verdiğinde 200 ms periyotla
uygulanır. ZUPT velocity sigma 0.20 m/s'dir. Zero-gyro aynı 200 ms penceredeki
delta-angle toplamından ortalama açısal hız üretir; tek bir örneği bias diye
kabul etmez. Bu politika PX4 EKF2 `ZeroVelocityUpdate`/`ZeroGyroUpdate` ve land
detector katman ayrımının bare-metal karşılığıdır.
Kaynaklar:
- <https://github.com/PX4/PX4-Autopilot/blob/main/src/modules/ekf2/EKF/aid_sources/ZeroVelocityUpdate.cpp>
- <https://github.com/PX4/PX4-Autopilot/blob/main/src/modules/ekf2/EKF/aid_sources/ZeroGyroUpdate.cpp>
- <https://github.com/PX4/PX4-Autopilot/blob/main/src/modules/land_detector/LandDetector.cpp>
- <https://github.com/PX4/PX4-Autopilot/blob/main/src/modules/sensors/vehicle_imu/VehicleIMU.cpp>
GPS motion evidence, ardışık kabul edilmiş ENU konumları arasındaki yatay
displacement için 2 DOF `9.21034` (%99) NIS ve en az 0.50 m/s hız ister. Moving
@@ -836,7 +958,7 @@ azınlık TOW'unu çoğunluğa kopyalayan geçici recovery yolu da kaldırıldı
veya problemli kanal reject edilir. TOW değeri sessizce başka kanalın
değeriyle değiştirilmez.
Üretim kodu `header/gps_time_anchor.h` ve `src/gps_time_anchor.c` içindedir.
Üretim kodu `header/gps/gps_time_anchor.h` ve `src/gps/gps_time_anchor.c` içindedir.
`parse_subframe()` artık TOW state'ini değiştirmez; yalnız HOW TOW değerini
dışarı verir. `gps_time_from_sample_anchor()` her nav-solve için GPS zamanını
son HOW ve iki receiver-sample latch'i arasındaki farktan baştan üretir.
+70 -7
View File
@@ -3,13 +3,13 @@ CFLAGS ?= -std=c11 -O2 -Wall -Wextra -Wpedantic
CFLAGS += -I../../gps_system/header
LDLIBS ?= -lm
GPS_TIME_SOURCE := ../../gps_system/src/gps_time_anchor.c
ESKF_SOURCES := ../../gps_system/src/eskf.c ../../gps_system/src/fusion_motion.c ../../gps_system/src/fusion_engine.c ../../gps_system/src/fusion_gps_adapter.c ../../gps_system/src/fusion_bus.c
IMU_PROFILE_SOURCE := ../../gps_system/src/imu_sensor_profile.c
GPS_TIME_SOURCE := ../../gps_system/src/gps/gps_time_anchor.c
ESKF_SOURCES := ../../gps_system/src/fusion/eskf.c ../../gps_system/src/fusion/fusion_motion.c ../../gps_system/src/fusion/fusion_rest_detector.c ../../gps_system/src/fusion/fusion_engine.c ../../gps_system/src/fusion/fusion_gps_adapter.c ../../gps_system/src/fusion/fusion_bus.c
IMU_PROFILE_SOURCE := ../../gps_system/src/adapters/imu_fusion_profile.c
.PHONY: all test sanitize profile benchmark clean
.PHONY: all test scenario-logs sanitize profile benchmark clean
all: standalone_gps_integrity_test eskf_motion_test fusion_synthetic_test imu_quality_synthetic_test
all: standalone_gps_integrity_test eskf_motion_test fusion_rest_detector_test fusion_synthetic_test imu_quality_synthetic_test async_tilt_recovery_test async_rate_matrix_test gps_reacquisition_test platform_scenario_matrix_test
standalone_gps_integrity_test: standalone_gps_integrity_test.c $(GPS_TIME_SOURCE)
$(CC) $(CFLAGS) $^ -o $@ $(LDLIBS)
@@ -17,19 +17,58 @@ standalone_gps_integrity_test: standalone_gps_integrity_test.c $(GPS_TIME_SOURCE
eskf_motion_test: eskf_motion_test.c $(ESKF_SOURCES)
$(CC) $(CFLAGS) $^ -o $@ $(LDLIBS)
fusion_rest_detector_test: fusion_rest_detector_test.c ../../gps_system/src/fusion/fusion_rest_detector.c
$(CC) $(CFLAGS) $^ -o $@ $(LDLIBS)
fusion_synthetic_test: fusion_synthetic_test.c $(ESKF_SOURCES)
$(CC) $(CFLAGS) $^ -o $@ $(LDLIBS)
imu_quality_synthetic_test: imu_quality_synthetic_test.c $(ESKF_SOURCES) $(IMU_PROFILE_SOURCE)
$(CC) $(CFLAGS) $^ -o $@ $(LDLIBS)
test: standalone_gps_integrity_test eskf_motion_test fusion_synthetic_test imu_quality_synthetic_test
async_tilt_recovery_test: async_tilt_recovery_test.c $(ESKF_SOURCES)
$(CC) $(CFLAGS) $^ -o $@ $(LDLIBS)
async_rate_matrix_test: async_rate_matrix_test.c $(ESKF_SOURCES)
$(CC) $(CFLAGS) $^ -o $@ $(LDLIBS)
gps_reacquisition_test: gps_reacquisition_test.c $(ESKF_SOURCES)
$(CC) $(CFLAGS) $^ -o $@ $(LDLIBS)
platform_scenario_matrix_test: platform_scenario_matrix_test.c $(ESKF_SOURCES)
$(CC) $(CFLAGS) $^ -o $@ $(LDLIBS)
test: standalone_gps_integrity_test eskf_motion_test fusion_rest_detector_test fusion_synthetic_test imu_quality_synthetic_test async_tilt_recovery_test async_rate_matrix_test gps_reacquisition_test platform_scenario_matrix_test
./standalone_gps_integrity_test
./eskf_motion_test
./fusion_rest_detector_test
./fusion_synthetic_test
./imu_quality_synthetic_test
./async_tilt_recovery_test
./async_rate_matrix_test
./gps_reacquisition_test
./platform_scenario_matrix_test
scenario-logs: platform_scenario_matrix_test
mkdir -p output/scenarios
@for scenario in stationary pedestrian car aircraft; do \
for gps in 200 500 1000; do \
for imu in 19 20 200 300; do \
./platform_scenario_matrix_test --log $$scenario $$gps $$imu \
output/scenarios/$${scenario}_gps$${gps}_imu$${imu}.log >/dev/null || exit $$?; \
done; \
done; \
done
@echo "Wrote 48 replay logs under output/scenarios/"
sanitize:
$(CC) -std=c11 -O1 -g -Wall -Wextra -Wpedantic -Werror \
-fsanitize=address,undefined -fno-omit-frame-pointer \
-I../../gps_system/header fusion_rest_detector_test.c \
../../gps_system/src/fusion/fusion_rest_detector.c \
-o fusion_rest_detector_test_san $(LDLIBS)
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
UBSAN_OPTIONS=halt_on_error=1 ./fusion_rest_detector_test_san
$(CC) -std=c11 -O1 -g -Wall -Wextra -Wpedantic -Werror \
-fsanitize=address,undefined -fno-omit-frame-pointer \
-I../../gps_system/header eskf_motion_test.c $(ESKF_SOURCES) \
@@ -48,6 +87,30 @@ sanitize:
$(IMU_PROFILE_SOURCE) -o imu_quality_synthetic_test_san $(LDLIBS)
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
UBSAN_OPTIONS=halt_on_error=1 ./imu_quality_synthetic_test_san
$(CC) -std=c11 -O1 -g -Wall -Wextra -Wpedantic -Werror \
-fsanitize=address,undefined -fno-omit-frame-pointer \
-I../../gps_system/header async_tilt_recovery_test.c $(ESKF_SOURCES) \
-o async_tilt_recovery_test_san $(LDLIBS)
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
UBSAN_OPTIONS=halt_on_error=1 ./async_tilt_recovery_test_san
$(CC) -std=c11 -O1 -g -Wall -Wextra -Wpedantic -Werror \
-fsanitize=address,undefined -fno-omit-frame-pointer \
-I../../gps_system/header async_rate_matrix_test.c $(ESKF_SOURCES) \
-o async_rate_matrix_test_san $(LDLIBS)
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
UBSAN_OPTIONS=halt_on_error=1 ./async_rate_matrix_test_san
$(CC) -std=c11 -O1 -g -Wall -Wextra -Wpedantic -Werror \
-fsanitize=address,undefined -fno-omit-frame-pointer \
-I../../gps_system/header gps_reacquisition_test.c $(ESKF_SOURCES) \
-o gps_reacquisition_test_san $(LDLIBS)
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
UBSAN_OPTIONS=halt_on_error=1 ./gps_reacquisition_test_san
$(CC) -std=c11 -O1 -g -Wall -Wextra -Wpedantic -Werror \
-fsanitize=address,undefined -fno-omit-frame-pointer \
-I../../gps_system/header platform_scenario_matrix_test.c $(ESKF_SOURCES) \
-o platform_scenario_matrix_test_san $(LDLIBS)
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
UBSAN_OPTIONS=halt_on_error=1 ./platform_scenario_matrix_test_san
profile:
$(CC) -std=c11 -O2 -g -pg -Wall -Wextra -Wpedantic \
@@ -62,4 +125,4 @@ benchmark: standalone_gps_integrity_test
./standalone_gps_integrity_test --bench-double 100000
clean:
rm -f standalone_gps_integrity_test standalone_gps_integrity_test_san standalone_gps_integrity_test_gprof eskf_motion_test eskf_motion_test_san fusion_synthetic_test fusion_synthetic_test_san fusion_synthetic_test_gprof fusion_synthetic_trace imu_quality_synthetic_test imu_quality_synthetic_test_san gmon.out
rm -f standalone_gps_integrity_test standalone_gps_integrity_test_san standalone_gps_integrity_test_gprof eskf_motion_test eskf_motion_test_san fusion_rest_detector_test fusion_rest_detector_test_san fusion_synthetic_test fusion_synthetic_test_san fusion_synthetic_test_gprof fusion_synthetic_trace imu_quality_synthetic_test imu_quality_synthetic_test_san async_tilt_recovery_test async_tilt_recovery_test_san async_rate_matrix_test async_rate_matrix_test_san gps_reacquisition_test gps_reacquisition_test_san platform_scenario_matrix_test platform_scenario_matrix_test_san gmon.out
@@ -0,0 +1,416 @@
#include "fusion/fusion_engine.h"
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#define TIMEBASE_HZ 1000000u
#define IMU_HZ 52u
#define DURATION_S 25.0
#define PI 3.14159265358979323846264338327950288
#define GPS_EVENT_CAPACITY 300u
typedef struct {
double position_m[3];
double velocity_mps[3];
double yaw_rad;
double accel_nav_mps2[3];
double yaw_rate_rps;
} truth_t;
typedef struct {
fusion_position_measurement_t measurement;
uint64_t arrival_ticks;
} gps_event_t;
typedef struct {
unsigned int gps_period_ms;
double horizontal_rmse_m;
double peak_outage_error_m;
double final_horizontal_error_m;
double final_vertical_error_m;
double final_speed_mps;
double outage_sigma_growth_m;
double final_horizontal_sigma_m;
double maximum_position_step_m;
uint64_t gps_accepted;
uint64_t gps_rejected;
uint64_t replay_count;
uint64_t imu_received;
unsigned int false_stationary_samples;
bool moving_detected;
bool recovery_gps_accepted;
bool local_state_finite;
bool covariance_finite_positive;
bool global_origin_available;
} metrics_t;
static bool trace_enabled;
static double square(double value)
{
return value * value;
}
static void command_at(double time_s, double accel[3], double* yaw_rate)
{
memset(accel, 0, 3u * sizeof(double));
*yaw_rate = 0.0;
if((time_s >= 3.0) && (time_s < 5.0))
{
accel[0] = 1.0;
}
else if((time_s >= 8.0) && (time_s < 10.0))
{
accel[0] = -1.0;
}
else if((time_s >= 12.0) && (time_s < 14.0))
{
accel[1] = 0.5;
*yaw_rate = PI / 4.0;
}
else if((time_s >= 18.0) && (time_s < 20.0))
{
accel[1] = -0.5;
}
}
static truth_t truth_at(double time_s)
{
static const double boundaries[] = {
0.0, 3.0, 5.0, 8.0, 10.0, 12.0, 14.0, 18.0, 20.0, DURATION_S
};
truth_t truth;
memset(&truth, 0, sizeof(truth));
for(unsigned int segment = 0u;
segment + 1u < sizeof(boundaries) / sizeof(boundaries[0]); segment++)
{
double start = boundaries[segment];
if(time_s <= start)
{
break;
}
double end = fmin(time_s, boundaries[segment + 1u]);
double dt = end - start;
double accel[3];
double yaw_rate;
command_at(start + 0.5 * dt, accel, &yaw_rate);
for(unsigned int axis = 0u; axis < 3u; axis++)
{
truth.position_m[axis] += truth.velocity_mps[axis] * dt +
0.5 * accel[axis] * dt * dt;
truth.velocity_mps[axis] += accel[axis] * dt;
}
truth.yaw_rad += yaw_rate * dt;
if(end >= time_s)
{
break;
}
}
command_at(time_s, truth.accel_nav_mps2, &truth.yaw_rate_rps);
return truth;
}
static fusion_imu_measurement_t make_imu(uint64_t ticks, double gravity)
{
double time_s = (double)ticks / (double)TIMEBASE_HZ;
truth_t truth = truth_at(time_s);
double cosine = cos(truth.yaw_rad);
double sine = sin(truth.yaw_rad);
double specific_force_nav[3] = {
truth.accel_nav_mps2[0], truth.accel_nav_mps2[1],
truth.accel_nav_mps2[2] + gravity
};
fusion_imu_measurement_t imu;
memset(&imu, 0, sizeof(imu));
imu.timestamp_ticks = ticks;
imu.accel_mps2[0] = cosine * specific_force_nav[0] +
sine * specific_force_nav[1];
imu.accel_mps2[1] = -sine * specific_force_nav[0] +
cosine * specific_force_nav[1];
imu.accel_mps2[2] = specific_force_nav[2];
imu.gyro_rps[2] = truth.yaw_rate_rps;
/* Constant offsets are learned during stationary startup. Small
* deterministic broadband components keep the test reproducible. */
static const double accel_bias[3] = {0.030, -0.020, 0.050};
static const double gyro_bias[3] = {0.0008, -0.0005, 0.0006};
for(unsigned int axis = 0u; axis < 3u; axis++)
{
double phase = time_s * (11.0 + 2.0 * axis) + (double)axis;
imu.accel_mps2[axis] += accel_bias[axis] + 0.008 * sin(phase);
imu.gyro_rps[axis] += gyro_bias[axis] + 0.00025 * cos(1.7 * phase);
}
imu.temperature_c = 25.0 + 0.4 * sin(0.1 * time_s);
imu.sequence = (uint32_t)((ticks * IMU_HZ + TIMEBASE_HZ / 2u) /
TIMEBASE_HZ);
imu.vehicle_at_rest =
(hypot(truth.velocity_mps[0], truth.velocity_mps[1]) < 0.05) &&
(hypot(truth.accel_nav_mps2[0], truth.accel_nav_mps2[1]) < 0.05) &&
(fabs(truth.yaw_rate_rps) < 0.02);
return imu;
}
static fusion_position_measurement_t make_gps(uint64_t ticks)
{
double time_s = (double)ticks / (double)TIMEBASE_HZ;
truth_t truth = truth_at(time_s);
fusion_position_measurement_t gps;
memset(&gps, 0, sizeof(gps));
gps.timestamp_ticks = ticks;
gps.position_enu_m[0] = truth.position_m[0] + 0.45 * sin(7.1 * time_s);
gps.position_enu_m[1] = truth.position_m[1] + 0.45 * cos(5.3 * time_s);
gps.position_enu_m[2] = truth.position_m[2] + 0.70 * sin(3.7 * time_s);
gps.covariance_enu_m2[0] = square(1.2);
gps.covariance_enu_m2[4] = square(1.2);
gps.covariance_enu_m2[8] = square(2.0);
gps.source_quality = 1.0;
gps.source_valid = true;
return gps;
}
static bool covariance_diagonal_valid(const eskf_t* filter)
{
for(unsigned int index = 0u; index < ESKF_ERROR_STATE_DIM; index++)
{
if(!isfinite(filter->covariance[index][index]) ||
!(filter->covariance[index][index] > 0.0))
{
return false;
}
}
return true;
}
static metrics_t run_scenario(unsigned int gps_period_ms)
{
static fusion_engine_t engine;
memset(&engine, 0, sizeof(engine));
fusion_engine_config_t config;
fusion_engine_default_config(&config, TIMEBASE_HZ);
config.calibration_min_samples = 100u;
metrics_t metrics;
memset(&metrics, 0, sizeof(metrics));
metrics.gps_period_ms = gps_period_ms;
if(!fusion_engine_init(&engine, &config))
{
return metrics;
}
if(gps_period_ms != 0u)
{
fusion_position_measurement_t origin = make_gps(0u);
origin.position_enu_m[0] = 0.0;
origin.position_enu_m[1] = 0.0;
origin.position_enu_m[2] = 0.0;
(void)fusion_engine_push_position(&engine, &origin, NULL, NULL);
}
gps_event_t events[GPS_EVENT_CAPACITY];
unsigned int event_head = 0u;
unsigned int event_count = 0u;
uint64_t next_gps_ticks = 3037000u;
uint64_t gps_period_ticks = (uint64_t)gps_period_ms * 1000u;
double error_square_sum = 0.0;
uint64_t error_samples = 0u;
double sigma_at_outage_start = NAN;
double sigma_before_recovery = NAN;
uint64_t accepted_before_recovery = 0u;
double previous_position[3] = {0.0, 0.0, 0.0};
bool have_previous_position = false;
uint64_t sample_count = (uint64_t)(DURATION_S * (double)IMU_HZ);
for(uint64_t sample = 0u; sample <= sample_count; sample++)
{
uint64_t ticks = (sample * TIMEBASE_HZ + IMU_HZ / 2u) / IMU_HZ;
double time_s = (double)ticks / (double)TIMEBASE_HZ;
fusion_imu_measurement_t imu = make_imu(ticks, config.eskf.gravity_mps2);
if(gps_period_ms == 0u)
{
/* Preserve the pure unaided observability case: no GNSS and no
* external platform-rest constraint. */
imu.vehicle_at_rest = false;
}
if(trace_enabled)
{
printf("IMU ts=%llu seq=%u accel=%.6f,%.6f,%.6f gyro=%.6f,%.6f,%.6f\n",
(unsigned long long)imu.timestamp_ticks, imu.sequence,
imu.accel_mps2[0], imu.accel_mps2[1], imu.accel_mps2[2],
imu.gyro_rps[0], imu.gyro_rps[1], imu.gyro_rps[2]);
}
if(!fusion_engine_push_imu(&engine, &imu))
{
break;
}
if((gps_period_ms != 0u) && (ticks >= next_gps_ticks))
{
double gps_time_s = (double)next_gps_ticks / (double)TIMEBASE_HZ;
if(!((gps_time_s >= 10.0) && (gps_time_s < 18.0)) &&
(event_count < GPS_EVENT_CAPACITY))
{
events[event_count].measurement = make_gps(next_gps_ticks);
events[event_count].arrival_ticks = next_gps_ticks +
((gps_period_ms == 1000u) ? 310000u : 73000u);
event_count++;
}
next_gps_ticks += gps_period_ticks;
}
while((event_head < event_count) &&
(events[event_head].arrival_ticks <= ticks))
{
eskf_update_result_t result = ESKF_UPDATE_INVALID;
double nis = NAN;
(void)fusion_engine_push_position(
&engine, &events[event_head].measurement, &result, &nis);
if(trace_enabled)
{
printf("GPS ts=%llu arrival=%llu result=%u nis=%.6f pos=%.3f,%.3f,%.3f\n",
(unsigned long long)events[event_head].measurement.timestamp_ticks,
(unsigned long long)ticks, (unsigned int)result, nis,
events[event_head].measurement.position_enu_m[0],
events[event_head].measurement.position_enu_m[1],
events[event_head].measurement.position_enu_m[2]);
}
event_head++;
}
if(engine.state == FUSION_ENGINE_RUNNING)
{
truth_t truth = truth_at(time_s);
double east_error = engine.current.filter.position_m[0] - truth.position_m[0];
double north_error = engine.current.filter.position_m[1] - truth.position_m[1];
double horizontal_error = hypot(east_error, north_error);
if(have_previous_position)
{
double position_step = hypot(
engine.current.filter.position_m[0] - previous_position[0],
engine.current.filter.position_m[1] - previous_position[1]);
metrics.maximum_position_step_m =
fmax(metrics.maximum_position_step_m, position_step);
}
memcpy(previous_position, engine.current.filter.position_m,
sizeof(previous_position));
have_previous_position = true;
if(time_s >= 2.5)
{
error_square_sum += square(horizontal_error);
error_samples++;
}
if((time_s >= 10.0) && (time_s < 18.0))
{
metrics.peak_outage_error_m =
fmax(metrics.peak_outage_error_m, horizontal_error);
}
if((hypot(truth.velocity_mps[0], truth.velocity_mps[1]) > 0.2) &&
(engine.current.motion.state == FUSION_MOTION_STATIONARY))
{
metrics.false_stationary_samples++;
}
if(engine.current.motion.state == FUSION_MOTION_MOVING)
{
metrics.moving_detected = true;
}
if(isnan(sigma_at_outage_start) && (time_s >= 10.0))
{
sigma_at_outage_start = sqrt(engine.current.filter.covariance[0][0]);
accepted_before_recovery = engine.diagnostics.position_accepted;
}
if(isnan(sigma_before_recovery) && (time_s >= 18.0))
{
sigma_before_recovery = sqrt(engine.current.filter.covariance[0][0]);
}
}
}
truth_t final_truth = truth_at(DURATION_S);
metrics.horizontal_rmse_m = (error_samples != 0u)
? sqrt(error_square_sum / (double)error_samples) : INFINITY;
metrics.final_horizontal_error_m = hypot(
engine.current.filter.position_m[0] - final_truth.position_m[0],
engine.current.filter.position_m[1] - final_truth.position_m[1]);
metrics.final_vertical_error_m =
fabs(engine.current.filter.position_m[2] - final_truth.position_m[2]);
metrics.final_speed_mps = eskf_speed_mps(&engine.current.filter);
metrics.outage_sigma_growth_m =
(isfinite(sigma_at_outage_start) && isfinite(sigma_before_recovery))
? sigma_before_recovery - sigma_at_outage_start : 0.0;
metrics.final_horizontal_sigma_m = sqrt(
engine.current.filter.covariance[0][0] +
engine.current.filter.covariance[1][1]);
metrics.gps_accepted = engine.diagnostics.position_accepted;
metrics.gps_rejected = engine.diagnostics.position_nis_rejected;
metrics.replay_count = engine.diagnostics.replay_count;
metrics.imu_received = engine.diagnostics.imu_received;
metrics.recovery_gps_accepted = (gps_period_ms == 0u) ||
(engine.diagnostics.position_accepted > accepted_before_recovery);
metrics.local_state_finite = (engine.state == FUSION_ENGINE_RUNNING) &&
eskf_state_is_finite(&engine.current.filter);
metrics.covariance_finite_positive =
covariance_diagonal_valid(&engine.current.filter);
metrics.global_origin_available = engine.have_initial_position;
return metrics;
}
static void print_metrics(const metrics_t* metrics)
{
printf("GPS=%4u ms IMU=%llu GPS-ok/rej=%llu/%llu replay=%llu "
"rmse-h=%.3f m outage-peak=%.3f m sigma-growth=%.3f m "
"final=%.3f/%.3f m sigma-h=%.3f m step-max=%.3f m "
"speed=%.4f m/s false-ZUPT=%u origin=%u\n",
metrics->gps_period_ms,
(unsigned long long)metrics->imu_received,
(unsigned long long)metrics->gps_accepted,
(unsigned long long)metrics->gps_rejected,
(unsigned long long)metrics->replay_count,
metrics->horizontal_rmse_m, metrics->peak_outage_error_m,
metrics->outage_sigma_growth_m,
metrics->final_horizontal_error_m, metrics->final_vertical_error_m,
metrics->final_horizontal_sigma_m, metrics->maximum_position_step_m,
metrics->final_speed_mps, metrics->false_stationary_samples,
metrics->global_origin_available ? 1u : 0u);
}
int main(int argc, char** argv)
{
trace_enabled = (argc == 2) && (strcmp(argv[1], "--trace") == 0);
const unsigned int periods_ms[] = {100u, 200u, 1000u, 0u};
bool passed = true;
printf("Timestamped 52 Hz IMU, asynchronous GPS, 8 s outage (10..18 s)\n");
for(unsigned int index = 0u;
index < sizeof(periods_ms) / sizeof(periods_ms[0]); index++)
{
metrics_t metrics = run_scenario(periods_ms[index]);
print_metrics(&metrics);
bool common_ok = metrics.local_state_finite &&
metrics.covariance_finite_positive &&
metrics.moving_detected &&
(metrics.false_stationary_samples == 0u) &&
(metrics.imu_received == 1301u) &&
(metrics.maximum_position_step_m < 1.0);
bool gps_ok = (periods_ms[index] == 0u)
? (!metrics.global_origin_available && (metrics.gps_accepted == 0u) &&
(metrics.peak_outage_error_m < 1.5) &&
(metrics.final_horizontal_error_m < 5.0) &&
(metrics.final_horizontal_error_m <
3.0 * metrics.final_horizontal_sigma_m) &&
(metrics.outage_sigma_growth_m > 0.0))
: (metrics.global_origin_available && metrics.recovery_gps_accepted &&
(metrics.gps_rejected == 0u) && (metrics.replay_count > 0u) &&
(metrics.peak_outage_error_m < 2.0) &&
(metrics.outage_sigma_growth_m > 0.0) &&
(metrics.final_speed_mps < 0.08) &&
(metrics.final_horizontal_error_m < 1.5) &&
(metrics.final_vertical_error_m < 1.5));
passed = passed && common_ok && gps_ok;
}
if(!passed)
{
fprintf(stderr, "FAIL: asynchronous rate/dead-reckoning matrix.\n");
return 1;
}
printf("PASS: all rates use the same IMU prediction path; GPS loss remains "
"local dead reckoning and recovery is an external update.\n");
return 0;
}
@@ -0,0 +1,315 @@
#include "fusion/fusion_engine.h"
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#define TEST_TIMEBASE_HZ 1000000u
#define TEST_IMU_HZ 52u
#define TEST_DURATION_S 30u
#define TEST_PI 3.14159265358979323846264338327950288
#define GPS_PERIOD_TICKS 200000u
#define GPS_DELAY_TICKS 73000u
#define MAX_GPS_EVENTS 160u
typedef struct {
fusion_position_measurement_t measurement;
uint64_t arrival_ticks;
} gps_event_t;
typedef struct {
const char* name;
double peak_horizontal_error_m;
double peak_vertical_error_m;
double final_horizontal_error_m;
double final_vertical_error_m;
double final_speed_mps;
double final_roll_error_deg;
uint64_t imu_pushed;
uint64_t gps_accepted;
uint64_t gps_rejected;
uint64_t zero_updates;
fusion_engine_state_t engine_state;
fusion_motion_state_t motion_state;
} scenario_metrics_t;
static double radians(double degrees)
{
return degrees * TEST_PI / 180.0;
}
static double degrees(double radians_value)
{
return radians_value * 180.0 / TEST_PI;
}
/* The IMU rotates about its own X axis without translating. Calibration is
* complete before the manoeuvre. This is the smallest synthetic version of
* the live hand-tilt failure: the true position and the GNSS antenna remain at
* the origin while body-frame gravity changes with roll. */
static void truth_roll(uint64_t ticks,
double final_roll_rad,
double* roll_rad,
double* roll_rate_rps)
{
double time_s = (double)ticks / (double)TEST_TIMEBASE_HZ;
double roll = 0.0;
double rate = 0.0;
const double maximum_roll = radians(20.0);
/* A short hand motion is intentionally placed between the 250 ms selected
* samples. Full-rate IMU observes the angular rate; sample dropping can
* observe only the changed gravity vector and completely miss the gyro. */
if((time_s >= 16.05) && (time_s < 16.15))
{
roll = maximum_roll * (time_s - 16.05) / 0.10;
rate = maximum_roll / 0.10;
}
else if((time_s >= 16.15) && (time_s < 17.05))
{
roll = maximum_roll;
}
else if((time_s >= 17.05) && (time_s < 17.15))
{
double fraction = (time_s - 17.05) / 0.10;
roll = maximum_roll + fraction * (final_roll_rad - maximum_roll);
rate = (final_roll_rad - maximum_roll) / 0.10;
}
else if(time_s >= 17.15)
{
roll = final_roll_rad;
}
*roll_rad = roll;
*roll_rate_rps = rate;
}
static fusion_imu_measurement_t make_imu(uint64_t ticks,
double gravity_mps2,
double final_roll_rad,
double gyro_scale)
{
double roll;
double roll_rate;
truth_roll(ticks, final_roll_rad, &roll, &roll_rate);
fusion_imu_measurement_t imu;
memset(&imu, 0, sizeof(imu));
imu.timestamp_ticks = ticks;
/* R_nb is a positive X rotation. A stationary accelerometer measures
* R_nb^T [0, 0, g], hence [0, g sin(phi), g cos(phi)] in body axes. */
imu.accel_mps2[1] = gravity_mps2 * sin(roll);
imu.accel_mps2[2] = gravity_mps2 * cos(roll);
imu.gyro_rps[0] = gyro_scale * roll_rate;
imu.temperature_c = 25.0;
imu.sequence = (uint32_t)((ticks * TEST_IMU_HZ + TEST_TIMEBASE_HZ / 2u) /
TEST_TIMEBASE_HZ);
double time_s = (double)ticks / (double)TEST_TIMEBASE_HZ;
imu.vehicle_at_rest = (time_s < 16.05) || (time_s >= 18.15);
return imu;
}
static fusion_position_measurement_t make_gps(uint64_t ticks)
{
fusion_position_measurement_t gps;
memset(&gps, 0, sizeof(gps));
gps.timestamp_ticks = ticks;
gps.covariance_enu_m2[0] = 4.0;
gps.covariance_enu_m2[4] = 4.0;
gps.covariance_enu_m2[8] = 9.0;
gps.source_quality = 100.0;
gps.source_valid = true;
return gps;
}
static unsigned int build_gps_events(gps_event_t events[MAX_GPS_EVENTS])
{
unsigned int count = 0u;
/* Deliberately offset GNSS epochs from both the 52 Hz IMU samples and
* integer seconds. */
for(uint64_t ticks = 123457u;
ticks <= (uint64_t)TEST_DURATION_S * TEST_TIMEBASE_HZ;
ticks += GPS_PERIOD_TICKS)
{
events[count].measurement = make_gps(ticks);
events[count].arrival_ticks = ticks + GPS_DELAY_TICKS;
count++;
}
return count;
}
static double quaternion_roll(const double quaternion[4])
{
double w = quaternion[0];
double x = quaternion[1];
double y = quaternion[2];
double z = quaternion[3];
return atan2(2.0 * (w * x + y * z),
1.0 - 2.0 * (x * x + y * y));
}
static scenario_metrics_t run_scenario(const char* name,
bool emulate_250_ms_sample_drop,
bool provide_gps,
double final_roll_deg,
double gyro_scale)
{
static fusion_engine_t engine;
memset(&engine, 0, sizeof(engine));
fusion_engine_config_t config;
fusion_engine_default_config(&config, TEST_TIMEBASE_HZ);
if(!fusion_engine_init(&engine, &config))
{
return (scenario_metrics_t){.name = name, .engine_state = FUSION_ENGINE_FAULT};
}
/* Give calibration an absolute origin, just as the product does when GNSS
* becomes available before initial tilt/bias calibration completes. */
if(provide_gps)
{
fusion_position_measurement_t origin = make_gps(1u);
(void)fusion_engine_push_position(&engine, &origin, NULL, NULL);
}
gps_event_t events[MAX_GPS_EVENTS];
unsigned int event_count = provide_gps ? build_gps_events(events) : 0u;
unsigned int event_head = 0u;
uint64_t last_forwarded_ticks = 0u;
bool have_forwarded = false;
scenario_metrics_t metrics;
memset(&metrics, 0, sizeof(metrics));
metrics.name = name;
const uint64_t sample_count =
(uint64_t)TEST_DURATION_S * TEST_IMU_HZ;
for(uint64_t sample = 0u ; sample <= sample_count ; sample++)
{
uint64_t ticks = (sample * TEST_TIMEBASE_HZ + TEST_IMU_HZ / 2u) /
TEST_IMU_HZ;
fusion_imu_measurement_t imu = make_imu(
ticks, config.eskf.gravity_mps2, radians(final_roll_deg), gyro_scale);
bool forward = true;
if(emulate_250_ms_sample_drop && have_forwarded &&
((ticks - last_forwarded_ticks) < 250000u))
{
forward = false;
}
if(forward)
{
if(!fusion_engine_push_imu(&engine, &imu))
{
break;
}
last_forwarded_ticks = ticks;
have_forwarded = true;
metrics.imu_pushed++;
}
while((event_head < event_count) &&
(events[event_head].arrival_ticks <= ticks))
{
(void)fusion_engine_push_position(
&engine, &events[event_head].measurement, NULL, NULL);
event_head++;
}
if(engine.state == FUSION_ENGINE_RUNNING)
{
double horizontal = hypot(engine.current.filter.position_m[0],
engine.current.filter.position_m[1]);
metrics.peak_horizontal_error_m =
fmax(metrics.peak_horizontal_error_m, horizontal);
metrics.peak_vertical_error_m =
fmax(metrics.peak_vertical_error_m,
fabs(engine.current.filter.position_m[2]));
}
}
metrics.engine_state = engine.state;
if(engine.state == FUSION_ENGINE_RUNNING)
{
metrics.final_horizontal_error_m =
hypot(engine.current.filter.position_m[0],
engine.current.filter.position_m[1]);
metrics.final_vertical_error_m = fabs(engine.current.filter.position_m[2]);
metrics.final_speed_mps = eskf_speed_mps(&engine.current.filter);
metrics.final_roll_error_deg =
fabs(degrees(quaternion_roll(engine.current.filter.body_to_nav_quaternion)) -
final_roll_deg);
metrics.motion_state = engine.current.motion.state;
}
metrics.gps_accepted = engine.diagnostics.position_accepted;
metrics.gps_rejected = engine.diagnostics.position_nis_rejected;
metrics.zero_updates = engine.diagnostics.zero_velocity_update_count;
return metrics;
}
static void print_metrics(const scenario_metrics_t* metrics)
{
printf("%-20s imu=%4llu state=%u motion=%-20s peak-h=%9.3f m peak-v=%9.3f m "
"final-h=%9.3f m final-v=%9.3f m speed=%8.3f m/s "
"roll=%7.3f deg gps=%llu/%llu zupt=%llu\n",
metrics->name,
(unsigned long long)metrics->imu_pushed,
(unsigned int)metrics->engine_state,
fusion_motion_state_name(metrics->motion_state),
metrics->peak_horizontal_error_m,
metrics->peak_vertical_error_m,
metrics->final_horizontal_error_m,
metrics->final_vertical_error_m,
metrics->final_speed_mps,
metrics->final_roll_error_deg,
(unsigned long long)metrics->gps_accepted,
(unsigned long long)metrics->gps_rejected,
(unsigned long long)metrics->zero_updates);
}
int main(void)
{
scenario_metrics_t full_rate = run_scenario(
"52 Hz full IMU", false, true, 0.0, 1.0);
scenario_metrics_t imu_only = run_scenario(
"52 Hz, no GNSS", false, false, 0.0, 1.0);
scenario_metrics_t residual = run_scenario(
"1 deg residual tilt", false, true, 10.0, 0.9);
scenario_metrics_t sparse = run_scenario(
"250 ms sample-drop", true, true, 0.0, 1.0);
printf("Asynchronous stationary tilt: 52 Hz IMU, 5 Hz GNSS, 73 ms GNSS delay\n");
print_metrics(&full_rate);
print_metrics(&imu_only);
print_metrics(&residual);
print_metrics(&sparse);
bool full_rate_ok = (full_rate.engine_state == FUSION_ENGINE_RUNNING) &&
(full_rate.peak_horizontal_error_m < 1.0) &&
(full_rate.peak_vertical_error_m < 1.0) &&
(full_rate.final_speed_mps < 0.05);
bool imu_only_ok = (imu_only.engine_state == FUSION_ENGINE_RUNNING) &&
(imu_only.gps_accepted == 0u) &&
(imu_only.peak_horizontal_error_m < 1.0) &&
(imu_only.peak_vertical_error_m < 1.0) &&
(imu_only.final_speed_mps < 0.05);
bool residual_recovered =
(residual.engine_state == FUSION_ENGINE_RUNNING) &&
(residual.motion_state == FUSION_MOTION_STATIONARY) &&
(residual.final_speed_mps < 0.05) &&
(residual.final_horizontal_error_m < 1.0) &&
(residual.final_roll_error_deg < 0.5);
bool sparse_aliasing_detected_and_recovered =
(sparse.engine_state == FUSION_ENGINE_RUNNING) &&
(sparse.peak_horizontal_error_m > 3.0) &&
(sparse.final_horizontal_error_m < 1.0) &&
(sparse.final_speed_mps < 0.10) &&
(sparse.motion_state == FUSION_MOTION_STATIONARY);
if(!full_rate_ok || !imu_only_ok || !residual_recovered ||
!sparse_aliasing_detected_and_recovered)
{
fprintf(stderr,
"FAIL: full-rate propagation and GNSS-aided rest recovery must "
"remain bounded; sparse sampling must expose a large transient but "
"recover after an external at-rest decision.\n");
return 1;
}
printf("PASS: full-rate and residual-tilt cases remained bounded; sparse "
"sampling exposed the transient and external at-rest recovered it.\n");
return 0;
}
+49 -5
View File
@@ -3,11 +3,11 @@
#include <stdio.h>
#include <string.h>
#include "eskf.h"
#include "fusion_engine.h"
#include "fusion_bus.h"
#include "fusion_gps_adapter.h"
#include "fusion_motion.h"
#include "fusion/eskf.h"
#include "fusion/fusion_engine.h"
#include "fusion/fusion_bus.h"
#include "fusion/fusion_gps_adapter.h"
#include "fusion/fusion_motion.h"
#define TEST_PI 3.14159265358979323846264338327950288
@@ -78,6 +78,47 @@ static void test_stationary_propagation(void)
CHECK(eskf_state_is_finite(&filter), "stationary state remains finite");
}
static void test_zero_update_primitives(void)
{
eskf_t filter;
eskf_config_t config;
identity_filter(&filter, &config);
filter.velocity_mps[0] = 1.2;
filter.velocity_mps[1] = -0.8;
filter.velocity_mps[2] = 0.3;
CHECK(eskf_zero_velocity_update(&filter, 0.03),
"direct ZUPT measurement update succeeds");
CHECK(eskf_speed_mps(&filter) < 0.005,
"direct ZUPT drives a stale velocity estimate close to zero");
double stationary_gyro[3] = {0.0010, -0.0006, 0.0003};
double gyro_std[3] = {0.0002, 0.0002, 0.0002};
for(unsigned int update = 0u ; update < 20u ; update++)
{
CHECK(eskf_zero_angular_rate_update(
&filter, stationary_gyro, gyro_std),
"direct zero-angular-rate update succeeds");
}
for(unsigned int axis = 0u ; axis < 3u ; axis++)
{
CHECK(fabs(filter.gyro_bias_rps[axis] - stationary_gyro[axis]) < 1.0e-5,
"zero-angular-rate update converges gyro bias to stationary reading");
}
CHECK(eskf_state_is_finite(&filter),
"ZUPT and zero-angular-rate updates preserve finite ESKF state");
for(unsigned int row = 0u ; row < ESKF_ERROR_STATE_DIM ; row++)
{
CHECK(filter.covariance[row][row] > 0.0,
"zero updates preserve positive covariance diagonal");
for(unsigned int col = 0u ; col < ESKF_ERROR_STATE_DIM ; col++)
{
CHECK(fabs(filter.covariance[row][col] -
filter.covariance[col][row]) < 1.0e-12,
"zero updates preserve covariance symmetry");
}
}
}
static void test_gravity_alignment(void)
{
eskf_t filter;
@@ -446,6 +487,7 @@ static void initialize_engine_for_replay(fusion_engine_t* engine)
imu.accel_mps2[2] = config.eskf.gravity_mps2;
imu.temperature_c = 25.0;
imu.sequence = (uint32_t)(tick / 10u);
imu.vehicle_at_rest = true;
CHECK(fusion_engine_push_imu(engine, &imu),
"stationary calibration IMU accepted");
}
@@ -475,6 +517,7 @@ static void push_replay_imu(fusion_engine_t* engine, uint64_t tick)
imu.accel_mps2[2] = engine->config.eskf.gravity_mps2;
imu.temperature_c = 25.0;
imu.sequence = (uint32_t)(tick / 10u);
imu.vehicle_at_rest = fabs(imu.accel_mps2[0]) < 0.1;
CHECK(fusion_engine_push_imu(engine, &imu), "replay profile IMU accepted");
}
@@ -703,6 +746,7 @@ static void test_history_rolls_checkpoint_forward(void)
int main(void)
{
test_stationary_propagation();
test_zero_update_primitives();
test_gravity_alignment();
test_position_nis_gate();
test_motion_transitions();
@@ -0,0 +1,131 @@
#include "fusion/fusion_rest_detector.h"
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
static int failures;
#define CHECK(condition, message) do { \
if(!(condition)) { \
fprintf(stderr, "FAIL: %s\n", message); \
failures++; \
} \
} while(0)
static imu_sample_t sample_at(uint64_t ticks, uint64_t timebase_hz,
double gyro_x_rps, unsigned int sequence)
{
imu_sample_t sample;
memset(&sample, 0, sizeof(sample));
sample.timestamp_ticks = ticks;
double time_s = (double)ticks / (double)timebase_hz;
sample.accel_mps2[0] = 0.004 * sin(11.0 * time_s);
sample.accel_mps2[1] = 0.004 * cos(13.0 * time_s);
sample.accel_mps2[2] = 9.80665 + 0.004 * sin(17.0 * time_s);
sample.gyro_rps[0] = gyro_x_rps;
sample.gyro_rps[1] = 0.0002 * sin(7.0 * time_s);
sample.gyro_rps[2] = 0.0002 * cos(9.0 * time_s);
sample.temperature_c = 25.0;
sample.sequence = sequence;
return sample;
}
static void test_rate(unsigned int rate_hz)
{
const uint64_t timebase_hz = 1000000u;
fusion_rest_detector_config_t config;
fusion_rest_detector_t detector;
fusion_rest_detector_default_config(&config);
CHECK(fusion_rest_detector_init(
&detector, &config, timebase_hz, true),
"rest detector initializes in unarmed/landed mode");
bool initial_rest_seen = false;
bool impulse_cleared_rest = false;
double reentry_time_s = NAN;
uint64_t sample_count = 6u * rate_hz;
for(uint64_t index = 0u ; index <= sample_count ; index++)
{
uint64_t ticks = (index * timebase_hz + rate_hz / 2u) / rate_hz;
double time_s = (double)ticks / (double)timebase_hz;
double gyro_x = ((time_s >= 2.0) &&
(time_s < 2.0 + 2.0 / (double)rate_hz)) ? 4.0 : 0.0;
imu_sample_t sample = sample_at(
ticks, timebase_hz, gyro_x, (unsigned int)index);
CHECK(fusion_rest_detector_update(&detector, &sample),
"monotonic IMU sample updates rest detector");
if((time_s >= 1.0) && (time_s < 2.0) && detector.at_rest)
{
initial_rest_seen = true;
}
if((time_s >= 2.0) && (time_s < 2.2) && !detector.at_rest)
{
impulse_cleared_rest = true;
}
if((time_s > 2.0) && !isfinite(reentry_time_s) && detector.at_rest)
{
reentry_time_s = time_s;
}
}
CHECK(initial_rest_seen, "quiet landed platform reaches at-rest after dwell");
CHECK(impulse_cleared_rest, "high-frequency gyro impulse clears at-rest");
CHECK(isfinite(reentry_time_s) && (reentry_time_s >= 3.0) &&
(reentry_time_s <= 5.5),
"rest returns only after vibration decay and one-second dwell");
printf("rest-detector %3u Hz: reentry=%.3f s accel-vibe=%.6f gyro-vibe=%.6f\n",
rate_hz, reentry_time_s,
detector.accel_vibration_metric_mps2,
detector.gyro_vibration_metric_rps);
}
static void test_platform_gate_and_timestamps(void)
{
const uint64_t timebase_hz = 1000000u;
fusion_rest_detector_config_t config;
fusion_rest_detector_t detector;
fusion_rest_detector_default_config(&config);
CHECK(fusion_rest_detector_init(
&detector, &config, timebase_hz, false),
"rest detector initializes in mobile/not-landed mode");
for(unsigned int index = 0u ; index <= 150u ; index++)
{
imu_sample_t sample = sample_at(
(uint64_t)index * 10000u, timebase_hz, 0.0, index);
CHECK(fusion_rest_detector_update(&detector, &sample),
"mobile quiet samples remain valid");
}
CHECK(!detector.at_rest,
"quiet IMU cannot authorize ZUPT while platform is not landed");
imu_sample_t duplicate = sample_at(1500000u, timebase_hz, 0.0, 151u);
CHECK(!fusion_rest_detector_update(&detector, &duplicate),
"duplicate timestamp is rejected");
fusion_rest_detector_set_landed(&detector, true, 1500000u);
for(unsigned int index = 151u ; index <= 260u ; index++)
{
imu_sample_t sample = sample_at(
(uint64_t)index * 10000u, timebase_hz, 0.0, index);
CHECK(fusion_rest_detector_update(&detector, &sample),
"landed quiet samples update rest detector");
}
CHECK(detector.at_rest,
"external landed fact plus quiet dwell authorizes at-rest");
}
int main(void)
{
test_rate(52u);
test_rate(104u);
test_rate(208u);
test_platform_gate_and_timestamps();
if(failures != 0)
{
fprintf(stderr, "%d rest-detector test(s) failed\n", failures);
return 1;
}
printf("PX4-style platform rest detector tests passed.\n");
return 0;
}
@@ -4,7 +4,7 @@
#include <stdio.h>
#include <string.h>
#include "fusion_engine.h"
#include "fusion/fusion_engine.h"
#define SYNTH_PI 3.14159265358979323846264338327950288
#define SYNTH_TIMEBASE_HZ 1000000u
@@ -311,6 +311,9 @@ static synth_metrics_t run_dynamic_scenario(uint32_t scenario_seed)
}
imu.temperature_c = 25.0 + 0.5 * sin((double)tick / 10000000.0);
imu.sequence = (uint32_t)(tick / imu_period_ticks);
imu.vehicle_at_rest =
(norm3(truth.velocity_mps) < 0.05) &&
(norm3(acceleration_nav) < 0.05) && (fabs(yaw_rate) < 0.02);
CHECK(fusion_engine_push_imu(&engine, &imu), "dynamic synthetic IMU accepted");
if(engine.state == FUSION_ENGINE_FAULT)
{
@@ -462,8 +465,8 @@ static void check_dynamic_metrics(const synth_metrics_t* metrics)
"GPS reject diagnostics include injected outliers");
CHECK(metrics->replay_count >= 50u && metrics->replayed_imu_count > 1000u,
"delayed GPS exercises fixed-lag replay extensively");
CHECK(metrics->zero_updates > 300u,
"stationary periods apply rate-limited ZUPT updates");
CHECK(metrics->zero_updates > 150u,
"stationary periods apply PX4-rate 5 Hz ZUPT updates");
CHECK(metrics->horizontal_rmse_m < 2.0,
"bias/noise/delay trajectory horizontal RMSE is bounded");
CHECK(metrics->vertical_rmse_m < 2.5,
@@ -521,6 +524,7 @@ static stationary_metrics_t run_stationary_500hz_scenario(void)
}
imu.temperature_c = 24.0;
imu.sequence = (uint32_t)(tick / imu_period_ticks);
imu.vehicle_at_rest = true;
CHECK(fusion_engine_push_imu(&engine, &imu), "500 Hz stationary IMU accepted");
if((tick >= next_gps_tick) && !gps_pending)
@@ -575,8 +579,8 @@ static void check_stationary_metrics(const stationary_metrics_t* metrics)
"30 s / 500 Hz noise never creates a false moving state");
CHECK(metrics->accepted_gps >= 26u,
"30 s / 500 Hz delayed stationary GPS remains accepted");
CHECK(metrics->zero_updates >= 500u,
"30 s / 500 Hz stationary run keeps rate-limited ZUPT active");
CHECK(metrics->zero_updates >= 120u,
"30 s / 500 Hz stationary run keeps PX4-rate ZUPT active");
CHECK(metrics->final_horizontal_error_m < 1.0,
"30 s / 500 Hz stationary horizontal error is bounded");
CHECK(metrics->final_vertical_error_m < 1.0,
@@ -0,0 +1,178 @@
#include "fusion/fusion_engine.h"
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#define TEST_TIMEBASE_HZ 1000000u
#define TEST_IMU_HZ 52u
#define TEST_GPS_PERIOD_TICKS 200000u
static fusion_imu_measurement_t stationary_imu(uint64_t timestamp_ticks,
double gravity_mps2)
{
fusion_imu_measurement_t imu;
memset(&imu, 0, sizeof(imu));
imu.timestamp_ticks = timestamp_ticks;
imu.accel_mps2[2] = gravity_mps2;
imu.temperature_c = 25.0;
imu.vehicle_at_rest = true;
return imu;
}
static fusion_position_measurement_t gps_position(
uint64_t timestamp_ticks,
double east_m,
double north_m,
double up_m)
{
fusion_position_measurement_t gps;
memset(&gps, 0, sizeof(gps));
gps.timestamp_ticks = timestamp_ticks;
gps.position_enu_m[0] = east_m;
gps.position_enu_m[1] = north_m;
gps.position_enu_m[2] = up_m;
gps.covariance_enu_m2[0] = 4.0;
gps.covariance_enu_m2[4] = 4.0;
gps.covariance_enu_m2[8] = 16.0;
gps.source_quality = 100.0;
gps.source_valid = true;
return gps;
}
static bool calibrate_at_origin(fusion_engine_t* engine,
fusion_engine_config_t* config)
{
fusion_engine_default_config(config, TEST_TIMEBASE_HZ);
if(!fusion_engine_init(engine, config))
{
return false;
}
fusion_position_measurement_t origin = gps_position(1u, 0.0, 0.0, 0.0);
if(!fusion_engine_push_position(engine, &origin, NULL, NULL))
{
return false;
}
for(uint64_t sample = 0u ; sample <= 3u * TEST_IMU_HZ ; sample++)
{
uint64_t ticks = sample * TEST_TIMEBASE_HZ / TEST_IMU_HZ;
fusion_imu_measurement_t imu = stationary_imu(ticks, config->eskf.gravity_mps2);
if(!fusion_engine_push_imu(engine, &imu))
{
return false;
}
}
return engine->state == FUSION_ENGINE_RUNNING;
}
static int startup_offset_recovers(uint64_t gps_period_ticks)
{
static fusion_engine_t engine;
fusion_engine_config_t config;
if(!calibrate_at_origin(&engine, &config))
{
fprintf(stderr, "FAIL: calibration did not complete\n");
return 1;
}
/* Models a poor first LLH origin followed by the stable, correct fixes seen
* on UART. ENU remains relative to the first origin, so the correct fixes
* legitimately appear far from the initialized ESKF position. */
const double expected[3] = {80.0, 40.0, -170.0};
uint64_t next_gps_ticks = 3200000u;
for(uint64_t sample = 3u * TEST_IMU_HZ + 1u ;
sample <= 12u * TEST_IMU_HZ ; sample++)
{
uint64_t ticks = sample * TEST_TIMEBASE_HZ / TEST_IMU_HZ;
fusion_imu_measurement_t imu = stationary_imu(ticks, config.eskf.gravity_mps2);
if(!fusion_engine_push_imu(&engine, &imu))
{
fprintf(stderr, "FAIL: IMU propagation failed\n");
return 1;
}
while(next_gps_ticks <= ticks)
{
fusion_position_measurement_t gps = gps_position(
next_gps_ticks, expected[0], expected[1], expected[2]);
(void)fusion_engine_push_position(&engine, &gps, NULL, NULL);
next_gps_ticks += gps_period_ticks;
}
}
double error = sqrt(
pow(engine.current.filter.position_m[0] - expected[0], 2.0) +
pow(engine.current.filter.position_m[1] - expected[1], 2.0) +
pow(engine.current.filter.position_m[2] - expected[2], 2.0));
printf("startup offset GPS=%4llu ms: error=%.3f m accepted=%llu "
"rejected=%llu recovery=%llu\n",
(unsigned long long)(gps_period_ticks / 1000u), error,
(unsigned long long)engine.diagnostics.position_accepted,
(unsigned long long)engine.diagnostics.position_nis_rejected,
(unsigned long long)engine.diagnostics.position_recovery_count);
if((error > 5.0) || (engine.diagnostics.position_recovery_count != 1u))
{
fprintf(stderr, "FAIL: consistent GPS did not recover the wrong startup position\n");
return 1;
}
return 0;
}
static int isolated_outlier_does_not_reset(void)
{
static fusion_engine_t engine;
fusion_engine_config_t config;
if(!calibrate_at_origin(&engine, &config))
{
fprintf(stderr, "FAIL: outlier scenario calibration did not complete\n");
return 1;
}
for(uint64_t gps_ticks = 3200000u ; gps_ticks <= 7000000u ;
gps_ticks += TEST_GPS_PERIOD_TICKS)
{
uint64_t imu_ticks = gps_ticks + 10000u;
fusion_imu_measurement_t imu = stationary_imu(
imu_ticks, config.eskf.gravity_mps2);
if(!fusion_engine_push_imu(&engine, &imu))
{
fprintf(stderr, "FAIL: outlier scenario IMU propagation failed\n");
return 1;
}
bool outlier = (gps_ticks == 5000000u);
fusion_position_measurement_t gps = gps_position(
gps_ticks, outlier ? 150.0 : 0.0, outlier ? -120.0 : 0.0,
outlier ? 80.0 : 0.0);
(void)fusion_engine_push_position(&engine, &gps, NULL, NULL);
}
double distance = sqrt(
pow(engine.current.filter.position_m[0], 2.0) +
pow(engine.current.filter.position_m[1], 2.0) +
pow(engine.current.filter.position_m[2], 2.0));
printf("single outlier: error=%.3f m rejected=%llu recovery=%llu\n",
distance,
(unsigned long long)engine.diagnostics.position_nis_rejected,
(unsigned long long)engine.diagnostics.position_recovery_count);
if((distance > 1.0) || (engine.diagnostics.position_nis_rejected == 0u) ||
(engine.diagnostics.position_recovery_count != 0u))
{
fprintf(stderr, "FAIL: isolated GPS outlier moved/reset the ESKF\n");
return 1;
}
return 0;
}
int main(void)
{
int failures = startup_offset_recovers(100000u);
failures += startup_offset_recovers(200000u);
failures += startup_offset_recovers(1000000u);
failures += isolated_outlier_does_not_reset();
if(failures == 0)
{
puts("GPS reacquisition tests passed");
}
return failures == 0 ? 0 : 1;
}
@@ -4,8 +4,9 @@
#include <stdio.h>
#include <string.h>
#include "fusion_engine.h"
#include "imu_sensor.h"
#include "fusion/fusion_engine.h"
#include "adapters/imu_fusion_profile.h"
#include "imu/imu_sensor.h"
#define QUALITY_PI 3.14159265358979323846264338327950288
#define QUALITY_TIMEBASE_HZ 1000000u
@@ -158,7 +159,7 @@ static quality_metrics_t run_profile(imu_sensor_backend_t backend,
{
static fusion_engine_t engine;
fusion_imu_quality_profile_t profile;
CHECK(imu_sensor_quality_profile_for_backend(backend, &profile),
CHECK(imu_fusion_quality_profile_for_backend(backend, &profile),
"sensor fusion quality profile exists");
fusion_engine_config_t config;
fusion_engine_default_config(&config, QUALITY_TIMEBASE_HZ);
@@ -255,6 +256,8 @@ static quality_metrics_t run_profile(imu_sensor_backend_t backend,
}
imu.temperature_c = 25.0 + 0.05 * time_s;
imu.sequence = (uint32_t)(tick / imu_period_ticks);
imu.vehicle_at_rest = !truth_is_dynamic(
&truth, acceleration_nav, yaw_rate);
CHECK(fusion_engine_push_imu(&engine, &imu),
"quality-profile IMU is accepted");
@@ -353,8 +356,8 @@ static void check_metrics(const quality_metrics_t* metrics,
(metrics->final_stationary_reentry_s <=
(noisier_st_profile ? 29.5 : 25.0)),
"stationary re-entry waits for motion hysteresis and then recovers");
CHECK(metrics->zero_updates > (noisier_st_profile ? 100u : 150u),
"stationary intervals apply rate-limited zero updates");
CHECK(metrics->zero_updates > 90u,
"stationary intervals apply PX4-rate zero updates");
CHECK(metrics->accepted_gps >= 15u,
"GPS updates remain usable with the sensor profile");
CHECK(metrics->horizontal_rmse_m < (noisier_st_profile ? 5.0 : 2.0),
@@ -371,10 +374,10 @@ int main(void)
{
fusion_imu_quality_profile_t tdk_profile;
fusion_imu_quality_profile_t st_profile;
CHECK(imu_sensor_quality_profile_for_backend(IMU_SENSOR_TDK_ICM42688,
CHECK(imu_fusion_quality_profile_for_backend(IMU_SENSOR_TDK_ICM42688,
&tdk_profile),
"TDK profile is valid");
CHECK(imu_sensor_quality_profile_for_backend(IMU_SENSOR_ST_LSM6DSOX,
CHECK(imu_fusion_quality_profile_for_backend(IMU_SENSOR_ST_LSM6DSOX,
&st_profile),
"ST profile is valid");
CHECK(tdk_profile.nominal_gyro_noise_density_rps_sqrt_hz[0] <
@@ -0,0 +1,551 @@
#include "fusion/fusion_engine.h"
#include <errno.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define TIMEBASE_HZ 1000000u
#define DURATION_S 50.0
#define PI 3.14159265358979323846264338327950288
typedef enum {
SCENARIO_STATIONARY = 0,
SCENARIO_PEDESTRIAN,
SCENARIO_CAR,
SCENARIO_AIRCRAFT,
SCENARIO_COUNT
} scenario_kind_t;
typedef struct {
double position_m[3];
double velocity_mps[3];
double yaw_rad;
} truth_t;
typedef struct {
double forward_accel_mps2;
double vertical_accel_mps2;
double yaw_rate_rps;
} command_t;
typedef struct {
uint32_t state;
} rng_t;
typedef struct {
double horizontal_rmse_m;
double final_horizontal_error_m;
double maximum_horizontal_error_m;
double maximum_burst_state_step_m;
uint64_t gps_accepted;
uint64_t gps_rejected;
uint64_t position_recoveries;
uint64_t zero_updates;
uint64_t zero_updates_after_startup;
bool finite;
bool recovered_after_burst;
} metrics_t;
static const char* scenario_name(scenario_kind_t scenario)
{
static const char* names[] = {"stationary", "pedestrian", "car", "aircraft"};
return names[(unsigned int)scenario];
}
static bool parse_scenario(const char* text, scenario_kind_t* scenario_out)
{
for(unsigned int index = 0u; index < SCENARIO_COUNT; index++)
{
if(strcmp(text, scenario_name((scenario_kind_t)index)) == 0)
{
*scenario_out = (scenario_kind_t)index;
return true;
}
}
return false;
}
static double square(double value)
{
return value * value;
}
static double rng_uniform(rng_t* rng)
{
rng->state = 1664525u * rng->state + 1013904223u;
return ((double)(rng->state >> 8u) + 0.5) / 16777216.0;
}
static double rng_normal(rng_t* rng)
{
double u1 = fmax(rng_uniform(rng), 1.0e-12);
double u2 = rng_uniform(rng);
return sqrt(-2.0 * log(u1)) * cos(2.0 * PI * u2);
}
static command_t command_at(scenario_kind_t scenario, double time_s)
{
command_t command = {0.0, 0.0, 0.0};
if(scenario == SCENARIO_PEDESTRIAN)
{
if((time_s >= 4.0) && (time_s < 6.0)) command.forward_accel_mps2 = 0.75;
else if((time_s >= 14.0) && (time_s < 16.0)) command.forward_accel_mps2 = -0.75;
else if((time_s >= 21.0) && (time_s < 23.0)) command.forward_accel_mps2 = 0.60;
else if((time_s >= 31.0) && (time_s < 33.0)) command.forward_accel_mps2 = -0.60;
if((time_s >= 24.0) && (time_s < 29.0)) command.yaw_rate_rps = 0.10;
}
else if(scenario == SCENARIO_CAR)
{
if((time_s >= 4.0) && (time_s < 9.0)) command.forward_accel_mps2 = 4.0;
else if((time_s >= 15.0) && (time_s < 18.0)) command.forward_accel_mps2 = -20.0 / 3.0;
else if((time_s >= 25.0) && (time_s < 29.0)) command.forward_accel_mps2 = 5.0;
else if((time_s >= 39.0) && (time_s < 42.0)) command.forward_accel_mps2 = -20.0 / 3.0;
if((time_s >= 31.0) && (time_s < 36.0)) command.yaw_rate_rps = 0.12;
}
else if(scenario == SCENARIO_AIRCRAFT)
{
if((time_s >= 4.0) && (time_s < 14.0)) command.forward_accel_mps2 = 8.0;
else if((time_s >= 26.0) && (time_s < 29.0)) command.forward_accel_mps2 = 4.0;
else if((time_s >= 42.0) && (time_s < 47.0)) command.forward_accel_mps2 = -6.0;
if((time_s >= 6.0) && (time_s < 10.0)) command.vertical_accel_mps2 = 2.0;
else if((time_s >= 18.0) && (time_s < 22.0)) command.vertical_accel_mps2 = -2.0;
if((time_s >= 20.0) && (time_s < 25.0)) command.yaw_rate_rps = 0.20;
else if((time_s >= 34.0) && (time_s < 36.0)) command.yaw_rate_rps = -0.35;
}
return command;
}
static void attitude_at(scenario_kind_t scenario, double time_s,
const truth_t* truth, double euler_rad[3],
double body_rate_rps[3])
{
memset(euler_rad, 0, 3u * sizeof(double));
memset(body_rate_rps, 0, 3u * sizeof(double));
euler_rad[2] = truth->yaw_rad;
command_t command = command_at(scenario, time_s);
body_rate_rps[2] = command.yaw_rate_rps;
if(scenario != SCENARIO_STATIONARY)
{
return;
}
const double roll_max = 20.0 * PI / 180.0;
const double pitch_max = 12.0 * PI / 180.0;
if((time_s >= 10.0) && (time_s < 11.0))
{
euler_rad[0] = roll_max * (time_s - 10.0);
body_rate_rps[0] = roll_max;
}
else if((time_s >= 11.0) && (time_s < 13.0)) euler_rad[0] = roll_max;
else if((time_s >= 13.0) && (time_s < 14.0))
{
euler_rad[0] = roll_max * (14.0 - time_s);
body_rate_rps[0] = -roll_max;
}
if((time_s >= 18.0) && (time_s < 19.0))
{
euler_rad[1] = pitch_max * (time_s - 18.0);
body_rate_rps[1] = pitch_max;
}
else if((time_s >= 19.0) && (time_s < 21.0)) euler_rad[1] = pitch_max;
else if((time_s >= 21.0) && (time_s < 22.0))
{
euler_rad[1] = pitch_max * (22.0 - time_s);
body_rate_rps[1] = -pitch_max;
}
}
static void rotation_body_to_nav(const double euler[3], double rotation[3][3])
{
double cr = cos(euler[0]);
double sr = sin(euler[0]);
double cp = cos(euler[1]);
double sp = sin(euler[1]);
double cy = cos(euler[2]);
double sy = sin(euler[2]);
rotation[0][0] = cy * cp;
rotation[0][1] = cy * sp * sr - sy * cr;
rotation[0][2] = cy * sp * cr + sy * sr;
rotation[1][0] = sy * cp;
rotation[1][1] = sy * sp * sr + cy * cr;
rotation[1][2] = sy * sp * cr - cy * sr;
rotation[2][0] = -sp;
rotation[2][1] = cp * sr;
rotation[2][2] = cp * cr;
}
static void propagate_truth(truth_t* truth, scenario_kind_t scenario,
double start_s, double dt_s)
{
command_t command = command_at(scenario, start_s + 0.5 * dt_s);
double speed = hypot(truth->velocity_mps[0], truth->velocity_mps[1]);
double yaw_mid = truth->yaw_rad + 0.5 * command.yaw_rate_rps * dt_s;
double acceleration[3] = {
command.forward_accel_mps2 * cos(yaw_mid) - speed * command.yaw_rate_rps * sin(yaw_mid),
command.forward_accel_mps2 * sin(yaw_mid) + speed * command.yaw_rate_rps * cos(yaw_mid),
command.vertical_accel_mps2
};
for(unsigned int axis = 0u; axis < 3u; axis++)
{
truth->position_m[axis] += truth->velocity_mps[axis] * dt_s +
0.5 * acceleration[axis] * dt_s * dt_s;
truth->velocity_mps[axis] += acceleration[axis] * dt_s;
}
truth->yaw_rad += command.yaw_rate_rps * dt_s;
}
static bool externally_at_rest(scenario_kind_t scenario, double time_s)
{
if(time_s < 3.5) return true;
if(scenario != SCENARIO_STATIONARY) return false;
/* An external landed/rest supervisor owns this decision. Attitude changes
* and the vibration interval revoke the constraint; quiet IMU alone never
* invents rest for the moving scenarios. */
return !(((time_s >= 9.8) && (time_s < 15.0)) ||
((time_s >= 17.8) && (time_s < 23.0)) ||
((time_s >= 27.0) && (time_s < 30.0)));
}
static fusion_imu_measurement_t make_imu(scenario_kind_t scenario,
const truth_t* truth, double time_s,
uint64_t ticks, double gravity,
unsigned int sequence, rng_t* rng)
{
command_t command = command_at(scenario, time_s);
double speed = hypot(truth->velocity_mps[0], truth->velocity_mps[1]);
double acceleration_nav[3] = {
command.forward_accel_mps2 * cos(truth->yaw_rad) -
speed * command.yaw_rate_rps * sin(truth->yaw_rad),
command.forward_accel_mps2 * sin(truth->yaw_rad) +
speed * command.yaw_rate_rps * cos(truth->yaw_rad),
command.vertical_accel_mps2 + gravity
};
double euler[3];
double body_rate[3];
double rotation[3][3];
attitude_at(scenario, time_s, truth, euler, body_rate);
rotation_body_to_nav(euler, rotation);
fusion_imu_measurement_t imu;
memset(&imu, 0, sizeof(imu));
imu.timestamp_ticks = ticks;
for(unsigned int body = 0u; body < 3u; body++)
{
for(unsigned int nav = 0u; nav < 3u; nav++)
{
imu.accel_mps2[body] += rotation[nav][body] * acceleration_nav[nav];
}
static const double accel_bias[3] = {0.035, -0.025, 0.055};
static const double gyro_bias[3] = {0.0008, -0.0006, 0.0007};
imu.accel_mps2[body] += accel_bias[body] + 0.012 * rng_normal(rng);
imu.gyro_rps[body] = body_rate[body] + gyro_bias[body] +
0.00035 * rng_normal(rng);
}
if((scenario == SCENARIO_STATIONARY) &&
(time_s >= 27.0) && (time_s < 29.0))
{
imu.accel_mps2[0] += 0.80 * sin(2.0 * PI * 7.0 * time_s);
imu.accel_mps2[1] += 0.55 * sin(2.0 * PI * 11.0 * time_s);
imu.gyro_rps[2] += 0.035 * sin(2.0 * PI * 5.0 * time_s);
}
imu.temperature_c = 25.0 + 0.5 * sin(0.08 * time_s);
imu.sequence = sequence;
imu.vehicle_at_rest = externally_at_rest(scenario, time_s);
return imu;
}
static fusion_position_measurement_t make_gps(const truth_t* truth,
double time_s, uint64_t ticks,
rng_t* rng, double gps_position[3])
{
double horizontal_std = time_s < 8.0 ? 8.0 : 4.0;
double vertical_std = time_s < 8.0 ? 12.0 : 7.0;
for(unsigned int axis = 0u; axis < 3u; axis++)
{
double std = axis < 2u ? horizontal_std : vertical_std;
gps_position[axis] = truth->position_m[axis] + std * rng_normal(rng);
}
if((time_s >= 32.0) && (time_s < 34.0))
{
gps_position[0] += 130.0;
gps_position[1] -= 90.0;
gps_position[2] += 55.0;
}
fusion_position_measurement_t gps;
memset(&gps, 0, sizeof(gps));
gps.timestamp_ticks = ticks;
memcpy(gps.position_enu_m, gps_position, sizeof(gps.position_enu_m));
gps.covariance_enu_m2[0] = square(horizontal_std);
gps.covariance_enu_m2[4] = square(horizontal_std);
gps.covariance_enu_m2[8] = square(vertical_std);
/* Exactly four satellites have zero residual degrees of freedom. This is
* therefore a valid WLS output with covariance but no redundant FDE test. */
gps.source_quality = 100.0;
gps.source_valid = true;
return gps;
}
static void local_to_geodetic(const double position[3], double* latitude_deg,
double* longitude_deg, double* height_m)
{
const double origin_latitude_deg = 40.78925034214207;
const double origin_longitude_deg = 29.46277530841728;
const double origin_height_m = 100.0;
const double latitude_rad = origin_latitude_deg * PI / 180.0;
*latitude_deg = origin_latitude_deg + position[1] / 6378137.0 * 180.0 / PI;
*longitude_deg = origin_longitude_deg +
position[0] / (6378137.0 * cos(latitude_rad)) * 180.0 / PI;
*height_m = origin_height_m + position[2];
}
static void write_location(FILE* log_file, const char* source,
scenario_kind_t scenario, double time_s,
const double position[3], unsigned int gps_ms,
unsigned int imu_ms)
{
if(log_file == NULL) return;
double latitude;
double longitude;
double height;
local_to_geodetic(position, &latitude, &longitude, &height);
fprintf(log_file,
"[LOCATION] source=%s scenario=%s t=%.3f lat=%.12f lon=%.12f "
"height=%.3f gdop=3.2 pdop=2.6 hdop=2.0 vdop=1.7 tdop=1.8 "
"tracked=4 used=4 gps_ms=%u imu_ms=%u\n",
source, scenario_name(scenario), time_s, latitude, longitude,
height, gps_ms, imu_ms);
}
static metrics_t run_case(scenario_kind_t scenario, unsigned int gps_ms,
unsigned int imu_ms, FILE* log_file)
{
static fusion_engine_t engine;
memset(&engine, 0, sizeof(engine));
fusion_engine_config_t config;
fusion_engine_default_config(&config, TIMEBASE_HZ);
config.calibration_duration_s = 2.0;
/* Calibration is time-based for asynchronous sensors; retain enough samples
* to estimate variance without silently assuming a particular ODR. */
config.calibration_min_samples = 2u * 1000u / imu_ms + 1u;
metrics_t metrics;
memset(&metrics, 0, sizeof(metrics));
if(!fusion_engine_init(&engine, &config)) return metrics;
truth_t truth;
memset(&truth, 0, sizeof(truth));
rng_t imu_rng = {0x7a31f20du ^ ((uint32_t)scenario << 24u) ^ imu_ms};
rng_t gps_rng = {0xb41d93c7u ^ ((uint32_t)scenario << 20u) ^ gps_ms};
uint64_t imu_period_ticks = (uint64_t)imu_ms * 1000u;
uint64_t gps_period_ticks = (uint64_t)gps_ms * 1000u;
uint64_t next_imu_ticks = 0u;
uint64_t next_gps_ticks = 0u;
uint64_t next_plot_ticks = 0u;
uint64_t end_ticks = (uint64_t)(DURATION_S * TIMEBASE_HZ);
uint64_t last_ticks = 0u;
unsigned int imu_sequence = 0u;
double error_squared_sum = 0.0;
uint64_t error_count = 0u;
uint64_t accepted_at_burst_end = 0u;
uint64_t zero_updates_at_startup_end = 0u;
double previous_horizontal[2] = {0.0, 0.0};
bool have_previous = false;
if(log_file != NULL)
{
fprintf(log_file, "# ESKF synthetic replay; blue=GPS red=IMU-corrected green=truth\n");
fprintf(log_file, "# scenario=%s gps_ms=%u imu_ms=%u satellites=4\n",
scenario_name(scenario), gps_ms, imu_ms);
}
while(true)
{
uint64_t ticks = next_imu_ticks;
if(next_gps_ticks < ticks) ticks = next_gps_ticks;
if(next_plot_ticks < ticks) ticks = next_plot_ticks;
if(ticks > end_ticks) break;
double start_s = (double)last_ticks / TIMEBASE_HZ;
double dt_s = (double)(ticks - last_ticks) / TIMEBASE_HZ;
propagate_truth(&truth, scenario, start_s, dt_s);
last_ticks = ticks;
double time_s = (double)ticks / TIMEBASE_HZ;
if(ticks == next_gps_ticks)
{
/* A 3 s dropout exercises asynchronous dead reckoning. */
if(!((time_s >= 25.0) && (time_s < 28.0)))
{
double gps_position[3];
fusion_position_measurement_t gps = make_gps(
&truth, time_s, ticks, &gps_rng, gps_position);
eskf_update_result_t result = ESKF_UPDATE_INVALID;
(void)fusion_engine_push_position(&engine, &gps, &result, NULL);
write_location(log_file, "gps", scenario, time_s, gps_position,
gps_ms, imu_ms);
}
next_gps_ticks += gps_period_ticks;
}
if(ticks == next_imu_ticks)
{
fusion_imu_measurement_t imu = make_imu(
scenario, &truth, time_s, ticks, config.eskf.gravity_mps2,
imu_sequence++, &imu_rng);
if(!fusion_engine_push_imu(&engine, &imu)) break;
next_imu_ticks += imu_period_ticks;
}
if(engine.state == FUSION_ENGINE_RUNNING)
{
if((time_s >= 4.0) && (zero_updates_at_startup_end == 0u))
zero_updates_at_startup_end =
engine.diagnostics.zero_velocity_update_count;
double horizontal_error = hypot(
engine.current.filter.position_m[0] - truth.position_m[0],
engine.current.filter.position_m[1] - truth.position_m[1]);
metrics.maximum_horizontal_error_m = fmax(
metrics.maximum_horizontal_error_m, horizontal_error);
if(time_s >= 8.0)
{
error_squared_sum += square(horizontal_error);
error_count++;
}
if(have_previous && (time_s >= 32.0) && (time_s < 34.2))
{
double step = hypot(
engine.current.filter.position_m[0] - previous_horizontal[0],
engine.current.filter.position_m[1] - previous_horizontal[1]);
metrics.maximum_burst_state_step_m = fmax(
metrics.maximum_burst_state_step_m, step);
}
previous_horizontal[0] = engine.current.filter.position_m[0];
previous_horizontal[1] = engine.current.filter.position_m[1];
have_previous = true;
if((time_s >= 34.0) && (accepted_at_burst_end == 0u))
accepted_at_burst_end = engine.diagnostics.position_accepted;
}
if(ticks == next_plot_ticks)
{
write_location(log_file, "trajectory", scenario, time_s,
truth.position_m, gps_ms, imu_ms);
if(engine.state == FUSION_ENGINE_RUNNING)
write_location(log_file, "imu_corrected", scenario, time_s,
engine.current.filter.position_m, gps_ms, imu_ms);
next_plot_ticks += 200000u;
}
}
double final_error = hypot(
engine.current.filter.position_m[0] - truth.position_m[0],
engine.current.filter.position_m[1] - truth.position_m[1]);
metrics.horizontal_rmse_m = error_count > 0u
? sqrt(error_squared_sum / (double)error_count) : INFINITY;
metrics.final_horizontal_error_m = final_error;
metrics.gps_accepted = engine.diagnostics.position_accepted;
metrics.gps_rejected = engine.diagnostics.position_nis_rejected;
metrics.position_recoveries = engine.diagnostics.position_recovery_count;
metrics.zero_updates = engine.diagnostics.zero_velocity_update_count;
metrics.zero_updates_after_startup = metrics.zero_updates >= zero_updates_at_startup_end
? metrics.zero_updates - zero_updates_at_startup_end : metrics.zero_updates;
metrics.recovered_after_burst = engine.diagnostics.position_accepted >
accepted_at_burst_end;
metrics.finite = (engine.state == FUSION_ENGINE_RUNNING) &&
eskf_state_is_finite(&engine.current.filter) && isfinite(final_error);
return metrics;
}
static void print_metrics(scenario_kind_t scenario, unsigned int gps_ms,
unsigned int imu_ms, const metrics_t* metrics)
{
printf("%-10s GPS=%4ums IMU=%3ums rmse=%7.2fm final=%7.2fm max=%7.2fm "
"burst-step=%6.2fm gps=%3llu/%2llu recovery=%llu zupt=%llu(+%llu) finite=%u\n",
scenario_name(scenario), gps_ms, imu_ms, metrics->horizontal_rmse_m,
metrics->final_horizontal_error_m, metrics->maximum_horizontal_error_m,
metrics->maximum_burst_state_step_m,
(unsigned long long)metrics->gps_accepted,
(unsigned long long)metrics->gps_rejected,
(unsigned long long)metrics->position_recoveries,
(unsigned long long)metrics->zero_updates,
(unsigned long long)metrics->zero_updates_after_startup,
metrics->finite ? 1u : 0u);
}
static int run_matrix(void)
{
static const unsigned int gps_periods_ms[] = {200u, 500u, 1000u};
static const unsigned int imu_periods_ms[] = {19u, 20u, 200u, 300u};
bool passed = true;
printf("Four-satellite asynchronous platform matrix; 3 s outage and 2 s coherent GNSS fault\n");
for(unsigned int scenario = 0u; scenario < SCENARIO_COUNT; scenario++)
{
for(unsigned int gps_index = 0u; gps_index < 3u; gps_index++)
{
for(unsigned int imu_index = 0u; imu_index < 4u; imu_index++)
{
metrics_t metrics = run_case((scenario_kind_t)scenario,
gps_periods_ms[gps_index], imu_periods_ms[imu_index], NULL);
print_metrics((scenario_kind_t)scenario, gps_periods_ms[gps_index],
imu_periods_ms[imu_index], &metrics);
bool common_ok = metrics.finite && metrics.recovered_after_burst &&
(metrics.position_recoveries == 0u) &&
(metrics.maximum_burst_state_step_m < 40.0);
bool rest_ok = (scenario == SCENARIO_STATIONARY)
? (metrics.zero_updates_after_startup > 0u)
: (metrics.zero_updates_after_startup == 0u);
bool production_rate_ok = (imu_periods_ms[imu_index] > 20u) ||
(metrics.final_horizontal_error_m < 25.0);
passed = passed && common_ok && rest_ok && production_rate_ok;
}
}
}
if(!passed)
{
fprintf(stderr, "FAIL: platform scenario matrix contract.\n");
return 1;
}
printf("PASS: 19/20 ms production rates stayed bounded; 200/300 ms coarse IMU "
"rates remained finite and their dynamic accuracy loss is reported, not hidden.\n");
return 0;
}
int main(int argc, char** argv)
{
if(argc == 1) return run_matrix();
if((argc == 6) && (strcmp(argv[1], "--log") == 0))
{
scenario_kind_t scenario;
if(!parse_scenario(argv[2], &scenario))
{
fprintf(stderr, "unknown scenario: %s\n", argv[2]);
return 2;
}
unsigned int gps_ms = (unsigned int)strtoul(argv[3], NULL, 10);
unsigned int imu_ms = (unsigned int)strtoul(argv[4], NULL, 10);
if(!((gps_ms == 200u) || (gps_ms == 500u) || (gps_ms == 1000u)) ||
!((imu_ms == 19u) || (imu_ms == 20u) ||
(imu_ms == 200u) || (imu_ms == 300u)))
{
fprintf(stderr, "supported GPS: 200/500/1000 ms; IMU: 19/20/200/300 ms\n");
return 2;
}
FILE* log_file = fopen(argv[5], "w");
if(log_file == NULL)
{
fprintf(stderr, "cannot create %s: %s\n", argv[5], strerror(errno));
return 2;
}
metrics_t metrics = run_case(scenario, gps_ms, imu_ms, log_file);
if(fclose(log_file) != 0)
{
fprintf(stderr, "cannot close %s\n", argv[5]);
return 2;
}
print_metrics(scenario, gps_ms, imu_ms, &metrics);
printf("wrote replay log: %s\n", argv[5]);
return metrics.finite ? 0 : 1;
}
fprintf(stderr, "usage: %s [--log stationary|pedestrian|car|aircraft "
"200|500|1000 19|20|200|300 output.log]\n", argv[0]);
return 2;
}
@@ -9,7 +9,7 @@
#include <string.h>
#include <time.h>
#include "gps_time_anchor.h"
#include "gps/gps_time_anchor.h"
#define MAX_SATELLITES 9
#define STATE_DIM 4