You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
300 lines
9.5 KiB
300 lines
9.5 KiB
5 years ago
|
/****************************************************************************
|
||
|
*
|
||
|
* Copyright (c) 2020 ECL Development Team. All rights reserved.
|
||
|
*
|
||
|
* Redistribution and use in source and binary forms, with or without
|
||
|
* modification, are permitted provided that the following conditions
|
||
|
* are met:
|
||
|
*
|
||
|
* 1. Redistributions of source code must retain the above copyright
|
||
|
* notice, this list of conditions and the following disclaimer.
|
||
|
* 2. Redistributions in binary form must reproduce the above copyright
|
||
|
* notice, this list of conditions and the following disclaimer in
|
||
|
* the documentation and/or other materials provided with the
|
||
|
* distribution.
|
||
|
* 3. Neither the name PX4 nor the names of its contributors may be
|
||
|
* used to endorse or promote products derived from this software
|
||
|
* without specific prior written permission.
|
||
|
*
|
||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||
|
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||
|
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||
|
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||
|
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||
|
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
|
||
|
* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
|
||
|
* AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||
|
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||
|
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||
|
*
|
||
|
****************************************************************************/
|
||
|
|
||
|
#include <gtest/gtest.h>
|
||
|
#include <math.h>
|
||
|
#include "EKF/common.h"
|
||
|
#include "EKF/sensor_range_finder.hpp"
|
||
|
#include <matrix/math.hpp>
|
||
|
|
||
|
using estimator::rangeSample;
|
||
|
using matrix::Dcmf;
|
||
|
using matrix::Eulerf;
|
||
|
using namespace estimator::sensor;
|
||
|
|
||
|
class SensorRangeFinderTest : public ::testing::Test {
|
||
|
public:
|
||
|
// Setup the Ekf with synthetic measurements
|
||
|
void SetUp() override
|
||
|
{
|
||
|
_range_finder.setTiltOffset(0.f);
|
||
|
_range_finder.setCosMaxTilt(0.707f);
|
||
|
_range_finder.setLimits(_min_range, _max_range);
|
||
|
}
|
||
|
|
||
|
// Use this method to clean up any memory, network etc. after each test
|
||
|
void TearDown() override
|
||
|
{
|
||
|
}
|
||
|
|
||
|
protected:
|
||
|
SensorRangeFinder _range_finder{};
|
||
|
const rangeSample _good_sample{1.f, (uint64_t)2e6, 100}; // {range, time_us, quality}
|
||
|
const float _min_range{0.5f};
|
||
|
const float _max_range{10.f};
|
||
|
|
||
|
void updateSensorAtRate(uint64_t duration_us, uint64_t dt_update_us, uint64_t dt_sensor_us);
|
||
|
void testTilt(const Eulerf &euler, bool should_pass);
|
||
|
};
|
||
|
|
||
|
void SensorRangeFinderTest::updateSensorAtRate(uint64_t duration_us, uint64_t dt_update_us, uint64_t dt_sensor_us)
|
||
|
{
|
||
|
const Dcmf attitude{Eulerf(0.f, 0.f, 0.f)};
|
||
|
|
||
|
rangeSample new_sample = _good_sample;
|
||
|
uint64_t t_now_us = _good_sample.time_us;
|
||
|
for (int i = 0; i < int(duration_us / dt_update_us); i++) {
|
||
|
t_now_us += dt_update_us;
|
||
|
if ((i % int(dt_sensor_us / dt_update_us)) == 0) {
|
||
|
new_sample.rng += 0.2f; // update the range to not trigger the stuck detection
|
||
|
if (new_sample.rng > _max_range) {
|
||
|
new_sample.rng = _min_range;
|
||
|
}
|
||
|
new_sample.time_us = t_now_us;
|
||
|
_range_finder.setSample(new_sample);
|
||
|
}
|
||
|
_range_finder.runChecks(t_now_us, attitude);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void SensorRangeFinderTest::testTilt(const Eulerf &euler, bool should_pass)
|
||
|
{
|
||
|
const Dcmf attitude{euler};
|
||
|
_range_finder.setSample(_good_sample);
|
||
|
_range_finder.runChecks(_good_sample.time_us, attitude);
|
||
|
|
||
|
if (should_pass) {
|
||
|
EXPECT_TRUE(_range_finder.isDataHealthy());
|
||
|
EXPECT_TRUE(_range_finder.isHealthy());
|
||
|
|
||
|
} else {
|
||
|
EXPECT_FALSE(_range_finder.isDataHealthy());
|
||
|
EXPECT_FALSE(_range_finder.isHealthy());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
TEST_F(SensorRangeFinderTest, setRange)
|
||
|
{
|
||
|
rangeSample sample{};
|
||
|
sample.rng = 1.f;
|
||
|
sample.time_us = 1e6;
|
||
|
sample.quality = 9;
|
||
|
|
||
|
_range_finder.setRange(sample.rng);
|
||
|
_range_finder.setDataReadiness(true);
|
||
|
_range_finder.setValidity(true);
|
||
|
EXPECT_TRUE(_range_finder.isDataHealthy());
|
||
|
EXPECT_TRUE(_range_finder.isHealthy());
|
||
|
}
|
||
|
|
||
|
TEST_F(SensorRangeFinderTest, goodData)
|
||
|
{
|
||
|
// WHEN: the drone is leveled and the data is good
|
||
|
const Dcmf attitude{Eulerf(0.f, 0.f, 0.f)};
|
||
|
_range_finder.setSample(_good_sample);
|
||
|
_range_finder.runChecks(_good_sample.time_us, attitude);
|
||
|
|
||
|
// THEN: the data can be used for aiding
|
||
|
EXPECT_TRUE(_range_finder.isDataHealthy());
|
||
|
EXPECT_TRUE(_range_finder.isHealthy());
|
||
|
}
|
||
|
|
||
|
TEST_F(SensorRangeFinderTest, tiltExceeded)
|
||
|
{
|
||
|
const Eulerf zero(0.f, 0.f, 0.f);
|
||
|
const Eulerf pitch_46(0.f, 0.8f, 0.f);
|
||
|
const Eulerf pitch_minus46(0.f, -0.8f, 0.f);
|
||
|
const Eulerf pitch_40(0.f, 0.7f, 1.f);
|
||
|
const Eulerf pitch_minus40(0.f, -0.7f, 0.f);
|
||
|
const Eulerf roll_46(0.8f, 0.f, 0.f);
|
||
|
const Eulerf roll_minus46(-0.8f, 0.f, 0.f);
|
||
|
const Eulerf roll_40(0.7f, 0.f, 2.f);
|
||
|
const Eulerf roll_minus40(-0.7f, 0.f, 3.f);
|
||
|
const Eulerf roll_28_pitch_minus28(0.5f, -0.5f, 4.f);
|
||
|
const Eulerf roll_46_pitch_minus46(0.8f, -0.8f, 4.f);
|
||
|
|
||
|
testTilt(zero, true);
|
||
|
testTilt(pitch_46, false);
|
||
|
testTilt(pitch_minus46, false);
|
||
|
testTilt(pitch_40, true);
|
||
|
testTilt(pitch_minus40, true);
|
||
|
testTilt(roll_46, false);
|
||
|
testTilt(roll_minus46, false);
|
||
|
testTilt(roll_40, true);
|
||
|
testTilt(roll_minus40, true);
|
||
|
testTilt(roll_28_pitch_minus28, true);
|
||
|
testTilt(roll_46_pitch_minus46, false);
|
||
|
|
||
|
}
|
||
|
|
||
|
TEST_F(SensorRangeFinderTest, rangeMaxExceeded)
|
||
|
{
|
||
|
const Dcmf attitude{Eulerf(0.f, 0.f, 0.f)};
|
||
|
|
||
|
// WHEN: the measured range is larger than the maximum
|
||
|
rangeSample bad_sample = _good_sample;
|
||
|
bad_sample.rng = _max_range + 0.01f;
|
||
|
_range_finder.setSample(bad_sample);
|
||
|
_range_finder.runChecks(bad_sample.time_us, attitude);
|
||
|
|
||
|
// THEN: the data should be marked as unhealthy
|
||
|
EXPECT_FALSE(_range_finder.isDataHealthy());
|
||
|
EXPECT_FALSE(_range_finder.isHealthy());
|
||
|
}
|
||
|
|
||
|
TEST_F(SensorRangeFinderTest, rangeMinExceeded)
|
||
|
{
|
||
|
const Dcmf attitude{Eulerf(0.f, 0.f, 0.f)};
|
||
|
|
||
|
// WHEN: the measured range is shorter than the minimum
|
||
|
rangeSample bad_sample = _good_sample;
|
||
|
bad_sample.rng = _min_range - 0.01f;
|
||
|
_range_finder.setSample(bad_sample);
|
||
|
_range_finder.runChecks(bad_sample.time_us, attitude);
|
||
|
|
||
|
// THEN: the data should be marked as unhealthy
|
||
|
EXPECT_FALSE(_range_finder.isDataHealthy());
|
||
|
EXPECT_FALSE(_range_finder.isHealthy());
|
||
|
}
|
||
|
|
||
|
TEST_F(SensorRangeFinderTest, outOfDate)
|
||
|
{
|
||
|
const Dcmf attitude{Eulerf(0.f, 0.f, 0.f)};
|
||
|
|
||
|
// WHEN: the data is outdated
|
||
|
rangeSample outdated_sample = _good_sample;
|
||
|
outdated_sample.time_us = 0;
|
||
|
uint64_t t_now = _good_sample.time_us;
|
||
|
_range_finder.setSample(outdated_sample);
|
||
|
_range_finder.runChecks(t_now, attitude);
|
||
|
|
||
|
// THEN: the data should be marked as unhealthy
|
||
|
EXPECT_FALSE(_range_finder.isDataHealthy());
|
||
|
EXPECT_FALSE(_range_finder.isHealthy());
|
||
|
}
|
||
|
|
||
|
TEST_F(SensorRangeFinderTest, rangeStuck)
|
||
|
{
|
||
|
const Dcmf attitude{Eulerf(0.f, 0.f, 0.f)};
|
||
|
|
||
|
// WHEN: the data is first not valid and then
|
||
|
// constantly the same
|
||
|
rangeSample new_sample = _good_sample;
|
||
|
const uint64_t dt = 3e5;
|
||
|
const uint64_t stuck_timeout = 11e6;
|
||
|
new_sample.quality = 0;
|
||
|
for (int i = 0; i < int(stuck_timeout / dt); i++) {
|
||
|
_range_finder.setSample(new_sample);
|
||
|
_range_finder.runChecks(new_sample.time_us, attitude);
|
||
|
new_sample.time_us += dt;
|
||
|
}
|
||
|
EXPECT_FALSE(_range_finder.isDataHealthy());
|
||
|
EXPECT_FALSE(_range_finder.isHealthy());
|
||
|
|
||
|
new_sample.quality = 100;
|
||
|
// we need a few sample to pass the hysteresis check
|
||
|
for (int i = 0; i < int(2e6 / dt); i++) {
|
||
|
_range_finder.setSample(new_sample);
|
||
|
_range_finder.runChecks(new_sample.time_us, attitude);
|
||
|
new_sample.time_us += dt;
|
||
|
}
|
||
|
|
||
|
// THEN: the data should be marked as unhealthy
|
||
|
// because the sensor is "stuck"
|
||
|
EXPECT_FALSE(_range_finder.isDataHealthy());
|
||
|
EXPECT_FALSE(_range_finder.isHealthy());
|
||
|
}
|
||
|
|
||
|
TEST_F(SensorRangeFinderTest, qualityHysteresis)
|
||
|
{
|
||
|
const Dcmf attitude{Eulerf(0.f, 0.f, 0.f)};
|
||
|
|
||
|
// WHEN: the data is first bad and then good
|
||
|
rangeSample new_sample = _good_sample;
|
||
|
|
||
|
new_sample.quality = 0;
|
||
|
_range_finder.setSample(new_sample);
|
||
|
_range_finder.runChecks(new_sample.time_us, attitude);
|
||
|
EXPECT_FALSE(_range_finder.isDataHealthy());
|
||
|
EXPECT_FALSE(_range_finder.isHealthy());
|
||
|
|
||
|
new_sample.quality = _good_sample.quality;
|
||
|
_range_finder.setSample(new_sample);
|
||
|
_range_finder.runChecks(new_sample.time_us, attitude);
|
||
|
EXPECT_FALSE(_range_finder.isDataHealthy());
|
||
|
EXPECT_FALSE(_range_finder.isHealthy());
|
||
|
|
||
|
// AND: we need to put enough good data to pass the hysteresis
|
||
|
const uint64_t dt = 3e5;
|
||
|
const uint64_t hyst_time = 1e6;
|
||
|
for (int i = 0; i < int(hyst_time / dt) + 2; i++) {
|
||
|
_range_finder.setSample(new_sample);
|
||
|
_range_finder.runChecks(new_sample.time_us, attitude);
|
||
|
new_sample.time_us += dt;
|
||
|
}
|
||
|
|
||
|
// THEN: the data is again declared healthy
|
||
|
EXPECT_TRUE(_range_finder.isDataHealthy());
|
||
|
EXPECT_TRUE(_range_finder.isHealthy());
|
||
|
}
|
||
|
|
||
|
TEST_F(SensorRangeFinderTest, continuity)
|
||
|
{
|
||
|
const Dcmf attitude{Eulerf(0.f, 0.f, 0.f)};
|
||
|
|
||
|
// WHEN: the data rate is too slow
|
||
|
const uint64_t dt_update_us = 10e3;
|
||
|
uint64_t dt_sensor_us = 4e6;
|
||
|
uint64_t duration_us = 8e6;
|
||
|
updateSensorAtRate(duration_us, dt_update_us, dt_sensor_us);
|
||
|
|
||
|
// THEN: the data should be marked as unhealthy
|
||
|
// Note that it also fails the out-of-date test here
|
||
|
EXPECT_FALSE(_range_finder.isDataHealthy());
|
||
|
EXPECT_FALSE(_range_finder.isHealthy());
|
||
|
|
||
|
// AND WHEN: the data rate is acceptable
|
||
|
dt_sensor_us = 3e5;
|
||
|
duration_us = 5e5;
|
||
|
updateSensorAtRate(duration_us, dt_update_us, dt_sensor_us);
|
||
|
|
||
|
// THEN: it should still fail until the filter converge
|
||
|
// to the new datarate
|
||
|
EXPECT_FALSE(_range_finder.isDataHealthy());
|
||
|
EXPECT_FALSE(_range_finder.isHealthy());
|
||
|
|
||
|
updateSensorAtRate(duration_us, dt_update_us, dt_sensor_us);
|
||
|
EXPECT_TRUE(_range_finder.isDataHealthy());
|
||
|
EXPECT_TRUE(_range_finder.isHealthy());
|
||
|
}
|