Add sensors Sensor interface and a verified host test setup

Sensor/SensorValue in components/sensors/include/sensor.h is modeled
on Zephyr's sensor API: a fetch()/get() split so multi-channel sensors
(e.g. BME280) pay the hardware read cost once, fixed-point SensorValue
(no float/double -- ESP32-C6 has no hardware FPU), a fine-grained
SensorChannel enum, and an optional interrupt-driven set_trigger() for
the battery-first sleep/wake design.

Adds components/sensors/test_apps/host, a self-contained ESP-IDF
project building against the `linux` target so this logic is
host-testable per AGENTS.md's testing philosophy -- verified in a
clean run against the pinned espressif/idf:v6.0.2 image (builds, 4/4
tests pass). Replaces the earlier generic top-level test/README.md
placeholder now that a real per-component pattern exists.

Also fixes .gitignore's build/managed_components patterns to match
nested paths, not just the repo root.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Ronny Eia
2026-07-08 20:44:48 +02:00
parent 524714148b
commit f1be175dc2
10 changed files with 220 additions and 12 deletions

View File

@@ -0,0 +1,4 @@
idf_component_register(
SRCS "test_sensor.cpp"
PRIV_REQUIRES sensors unity
)

View File

@@ -0,0 +1,85 @@
#include "sensor.h"
#include "unity.h"
using namespace kvida;
namespace {
// Minimal fake sensor exercising the fetch()/get() cache contract and
// SensorValue's fixed-point convention (val1 + val2 * 1e-6).
class FakeSensor : public Sensor {
public:
const char *id() const override { return "fake0"; }
void begin() override {}
bool supports(SensorChannel channel) const override
{
return channel == SensorChannel::AmbientTemperature;
}
bool fetch() override
{
fetched_ = true;
return true;
}
bool get(SensorChannel channel, SensorValue &out) override
{
if (!fetched_ || channel != SensorChannel::AmbientTemperature) {
return false;
}
out = SensorValue{21, 500000, "C", 1234};
return true;
}
private:
bool fetched_ = false;
};
double to_double(const SensorValue &v)
{
return v.val1 + v.val2 * 1e-6;
}
} // namespace
TEST_CASE("get fails before fetch", "[sensor]")
{
FakeSensor sensor;
SensorValue value{};
TEST_ASSERT_FALSE(sensor.get(SensorChannel::AmbientTemperature, value));
}
TEST_CASE("fetch then get returns the cached value", "[sensor]")
{
FakeSensor sensor;
sensor.begin();
TEST_ASSERT_TRUE(sensor.fetch());
SensorValue value{};
TEST_ASSERT_TRUE(sensor.get(SensorChannel::AmbientTemperature, value));
TEST_ASSERT_EQUAL_DOUBLE(21.5, to_double(value));
}
TEST_CASE("get fails for an unsupported channel", "[sensor]")
{
FakeSensor sensor;
sensor.begin();
sensor.fetch();
SensorValue value{};
TEST_ASSERT_FALSE(sensor.get(SensorChannel::Humidity, value));
}
TEST_CASE("default set_trigger reports unsupported", "[sensor]")
{
FakeSensor sensor;
TEST_ASSERT_FALSE(sensor.set_trigger(SensorTriggerType::DataReady, nullptr, nullptr));
}
extern "C" void app_main(void)
{
UNITY_BEGIN();
unity_run_all_tests();
UNITY_END();
}