fwt_software/tests/test_mcuenv.cpp

137 lines
5.2 KiB
C++

#include <doctest/doctest.h>
#include "fgc/Config.h"
#include "fgc/TelemetryParser.h"
#include "fgc/sensors/McuEnvSensor.h"
#include <cstdio>
#include <fstream>
#include <stdexcept>
#include <string>
using namespace fgc;
namespace {
// Only latestEnv() matters here; the rest is the minimum the interface demands.
struct EnvOnlyMotor : IMotorController {
std::optional<McuEnvReading> 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<McuEnvReading> 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());
}