#include #include "fgc/Config.h" #include "fgc/TelemetryParser.h" #include "fgc/sensors/McuEnvSensor.h" #include #include #include #include using namespace fgc; namespace { // Only latestEnv() matters here; the rest is the minimum the interface demands. struct EnvOnlyMotor : IMotorController { std::optional env; void start() override {} void stop() override {} void sendCommand(const std::string&) override {} MotorTelemetry telemetry() override { return {}; } std::string lastDump() override { return ""; } std::string lastDiag() override { return ""; } unsigned diagSeq() const override { return 0; } bool connected() const override { return true; } std::optional latestEnv() const override { return env; } }; McuEnvReading reading(int t, int h, bool ok, int fan, int rpm, long long at) { McuEnvReading r; r.temp_c = t; r.humidity = h; r.sensor_ok = ok; r.fan_duty = fan; r.fan_rpm = rpm; r.epoch_ms = at; return r; } } // namespace TEST_CASE("an ENV line from the firmware parses, keys in any order") { auto r = parseEnvLine("ENV t=23 h=45 ok=1 fan=0 rpm=0"); REQUIRE(r); CHECK(r->temp_c == 23); CHECK(r->humidity == 45); CHECK(r->sensor_ok); CHECK(r->fan_duty == 0); r = parseEnvLine("ENV rpm=1180 fan=255 ok=0 h=51 t=-4"); REQUIRE(r); CHECK(r->temp_c == -4); CHECK_FALSE(r->sensor_ok); CHECK(r->fan_duty == 255); CHECK(r->fan_rpm == 1180); } TEST_CASE("anything that is not a whole ENV line is not a reading") { CHECK_FALSE(parseEnvLine("ST Y:A,1,2,0,0,0,0,S")); CHECK_FALSE(parseEnvLine("DUMP ENV t=23 h=45 ok=1")); // the dump's copy, handled there CHECK_FALSE(parseEnvLine("ENV t=23 h=45")); // no ok: cannot tell good from bad CHECK_FALSE(parseEnvLine("ENV t=2x h=45 ok=1")); } TEST_CASE("the MCU sensor serves a fresh good reading, and nothing else") { EnvOnlyMotor motor; long long now = 1'000'000; McuEnvSensor s(motor, [&] { return now; }); CHECK_FALSE(s.sample()); // no ENV line yet motor.env = reading(24, 40, true, 0, 0, now); auto v = s.sample(); REQUIRE(v); CHECK(v->temp_c == doctest::Approx(24.0)); CHECK(v->humidity_pct == doctest::Approx(40.0)); CHECK(s.connected()); CHECK(s.name() == "DHT11"); SUBCASE("a failed read is not served as a reading") { motor.env = reading(24, 40, false, 255, 0, now); CHECK_FALSE(s.sample()); } SUBCASE("a line older than 2.5 read periods is stale") { now += McuEnvSensor::kStaleMs + 1; CHECK_FALSE(s.sample()); CHECK_FALSE(s.connected()); } } TEST_CASE("a fan driven hard with no rpm looks stalled; idle or turning does not") { CHECK(fanLooksStalled(reading(30, 40, true, 200, 0, 0))); CHECK(fanLooksStalled(reading(0, 0, false, 255, 0, 0))); // the failsafe, not turning CHECK_FALSE(fanLooksStalled(reading(20, 40, true, 0, 0, 0))); // off, as it should be CHECK_FALSE(fanLooksStalled(reading(30, 40, true, 100, 0, 0))); // too slow to be sure CHECK_FALSE(fanLooksStalled(reading(30, 40, true, 200, 900, 0))); } TEST_CASE("hardware.ini: cooling needs the DHT11, and only 'fan' or 'none'") { const std::string cfg_path = "test_mcuenv_config.ini"; const std::string hw_path = "test_mcuenv_hardware.ini"; { std::ofstream c(cfg_path); c << "[General]\ntower_name = Legacy\n[Camera.rgb]\nid = X\nprofile_dir = /tmp/p\n"; } auto write_hw = [&](const std::string& hardware, const std::string& pins) { std::ofstream h(hw_path); h << "[Board]\nmodel = lattepanda-3-delta\nfirmware_env = leonardo\n" "[Hardware]\naxes = yaw\nimu = none\ncameras = rgb\n" << hardware << "[Pins]\nyaw_cs = 8\nyaw_en = 7\n" << pins; }; SUBCASE("a legacy tower loads, with its MCU pins") { write_hw("env = dht11\ncooling = fan\n", "dht = 9\nfan_pwm = 10\nfan_tach = 11\nspi_idle_cs = 2\n"); auto c = ConfigLoader::loadFromFile(cfg_path, hw_path); CHECK(c.hardware.env == "dht11"); CHECK(c.hardware.cooling == "fan"); CHECK(c.hardware.pins.dht == 9); CHECK(c.hardware.pins.fan_tach == 11); CHECK(c.hardware.pins.spi_idle_cs == 2); } SUBCASE("a fan without the DHT11 is refused") { write_hw("env = sht41\ncooling = fan\n", "fan_pwm = 10\n"); CHECK_THROWS_WITH_AS(ConfigLoader::loadFromFile(cfg_path, hw_path), doctest::Contains("needs Hardware.env = dht11"), std::runtime_error); } SUBCASE("an unknown cooling is refused") { write_hw("env = dht11\ncooling = water\n", "dht = 9\n"); CHECK_THROWS_WITH_AS(ConfigLoader::loadFromFile(cfg_path, hw_path), doctest::Contains("'none' or 'fan'"), std::runtime_error); } SUBCASE("towers without it are unchanged") { write_hw("env = sht41\n", ""); auto c = ConfigLoader::loadFromFile(cfg_path, hw_path); CHECK(c.hardware.cooling.empty()); CHECK(c.hardware.pins.dht == -1); } std::remove(cfg_path.c_str()); std::remove(hw_path.c_str()); }