250 lines
8.2 KiB
C++
250 lines
8.2 KiB
C++
#include "doctest/doctest.h"
|
|
#include "fgc/FirmwareConfig.h"
|
|
|
|
#include <algorithm>
|
|
|
|
using namespace fgc;
|
|
|
|
namespace {
|
|
|
|
// Tower 121's yaw, fully declared: the case the handshake exists to serve.
|
|
AxisConfigSource iotaYaw() {
|
|
AxisConfigSource s;
|
|
s.have_current = true;
|
|
s.global_scaler = 80;
|
|
s.run_irun = 31;
|
|
s.homing_irun = 7;
|
|
s.center_count = 500000;
|
|
s.soft_margin = 5000;
|
|
s.have_travel = true;
|
|
s.steps_per_rev = 1210778;
|
|
s.have_deviation = true;
|
|
s.enc_deviation = 1500;
|
|
s.normal_vmax = 39738; // 8 deg/s at 3552.87 counts/deg
|
|
s.homing_vmax = 30000;
|
|
s.center_vmax = 25000;
|
|
s.have_wiring = true;
|
|
s.sw_mode = 0; // yaw: no physical stops, StallGuard homing
|
|
s.has_encoder = true;
|
|
return s;
|
|
}
|
|
|
|
bool sent(const FirmwareConfigPlan& p, const std::string& text) {
|
|
return std::find(p.sets.begin(), p.sets.end(), text) != p.sets.end();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("a NEED line parses into an axis and its keys") {
|
|
auto r = parseNeedLine("NEED Y steps_per_rev global_scaler center margin");
|
|
REQUIRE(r);
|
|
CHECK(r->axis == 'Y');
|
|
REQUIRE(r->keys.size() == 4);
|
|
CHECK(r->keys[0] == "steps_per_rev");
|
|
CHECK(r->keys[3] == "margin");
|
|
|
|
auto p = parseNeedLine("NEED P sw_mode");
|
|
REQUIRE(p);
|
|
CHECK(p->axis == 'P');
|
|
}
|
|
|
|
TEST_CASE("an axis that needs nothing is well-formed, not a parse failure") {
|
|
// How a firmware built for fewer axes than the rig has answers for the axis
|
|
// it does not drive.
|
|
auto r = parseNeedLine("NEED P");
|
|
REQUIRE(r);
|
|
CHECK(r->axis == 'P');
|
|
CHECK(r->keys.empty());
|
|
CHECK(planFirmwareConfig(*r, iotaYaw()).complete());
|
|
}
|
|
|
|
TEST_CASE("anything that is not a NEED line is ignored") {
|
|
CHECK_FALSE(parseNeedLine("ST Y:R,0,0,00000000,0,0,0,S"));
|
|
CHECK_FALSE(parseNeedLine("NEEDCFG"));
|
|
CHECK_FALSE(parseNeedLine("NEED"));
|
|
CHECK_FALSE(parseNeedLine("NEED X steps_per_rev")); // not an axis
|
|
CHECK_FALSE(parseNeedLine("NEED YY steps_per_rev"));
|
|
CHECK_FALSE(parseNeedLine(""));
|
|
}
|
|
|
|
TEST_CASE("a fully declared axis is answered completely, in the order asked") {
|
|
auto r = parseNeedLine(
|
|
"NEED Y steps_per_rev global_scaler sw_mode enc_deviation normal_speed homing_speed "
|
|
"center_speed center margin");
|
|
REQUIRE(r);
|
|
auto p = planFirmwareConfig(*r, iotaYaw());
|
|
|
|
CHECK(p.complete());
|
|
CHECK(p.unanswered.empty());
|
|
CHECK(p.unknown.empty());
|
|
REQUIRE(p.sets.size() == 9);
|
|
CHECK(p.sets.front() == "SET Y steps_per_rev 1210778");
|
|
CHECK(sent(p, "SET Y global_scaler 80"));
|
|
CHECK(sent(p, "SET Y enc_deviation 1500"));
|
|
CHECK(sent(p, "SET Y normal_speed 39738"));
|
|
CHECK(p.sets.back() == "SET Y margin 5000");
|
|
}
|
|
|
|
TEST_CASE("center/margin are accepted under both their names") {
|
|
auto legacy = parseNeedLine("NEED Y center margin");
|
|
auto full = parseNeedLine("NEED Y center_count soft_margin");
|
|
REQUIRE(legacy);
|
|
REQUIRE(full);
|
|
CHECK(planFirmwareConfig(*legacy, iotaYaw()).sets[0] == "SET Y center 500000");
|
|
CHECK(planFirmwareConfig(*full, iotaYaw()).sets[0] == "SET Y center_count 500000");
|
|
}
|
|
|
|
TEST_CASE("an undeclared current is unanswered, never sent as zero") {
|
|
// A silently zeroed current is a motor that cannot hold, so the key must go
|
|
// unanswered and leave the firmware in NEEDCFG.
|
|
auto s = iotaYaw();
|
|
s.have_current = false;
|
|
auto r = parseNeedLine("NEED Y global_scaler run_irun homing_irun");
|
|
REQUIRE(r);
|
|
auto p = planFirmwareConfig(*r, s);
|
|
|
|
CHECK_FALSE(p.complete());
|
|
CHECK(p.sets.empty());
|
|
CHECK(p.unanswered.size() == 3);
|
|
}
|
|
|
|
TEST_CASE("a speed that could not be converted is unanswered, not VMAX 0") {
|
|
// VMAX 0 is a stopped axis, not a slow one. AxisSpeed refuses an
|
|
// uncalibrated scale, and that refusal has to survive into the handshake.
|
|
auto s = iotaYaw();
|
|
s.normal_vmax = 0;
|
|
auto r = parseNeedLine("NEED Y normal_speed homing_speed");
|
|
REQUIRE(r);
|
|
auto p = planFirmwareConfig(*r, s);
|
|
|
|
CHECK_FALSE(p.complete());
|
|
REQUIRE(p.unanswered.size() == 1);
|
|
CHECK(p.unanswered[0] == "normal_speed");
|
|
CHECK(sent(p, "SET Y homing_speed 30000"));
|
|
}
|
|
|
|
TEST_CASE("a key this host has never heard of is a version skew, not a gap") {
|
|
// The two need different fixes - upgrade the host vs. fill in hardware.ini -
|
|
// so they are reported separately.
|
|
auto r = parseNeedLine("NEED Y steps_per_rev flux_capacitor");
|
|
REQUIRE(r);
|
|
auto p = planFirmwareConfig(*r, iotaYaw());
|
|
|
|
CHECK_FALSE(p.complete());
|
|
CHECK(p.unanswered.empty());
|
|
REQUIRE(p.unknown.size() == 1);
|
|
CHECK(p.unknown[0] == "flux_capacitor");
|
|
// The keys it DID understand are still answered.
|
|
CHECK(sent(p, "SET Y steps_per_rev 1210778"));
|
|
}
|
|
|
|
TEST_CASE("has_encoder is sent as 1/0, and follows the wiring declaration") {
|
|
auto s = iotaYaw();
|
|
s.has_encoder = false;
|
|
auto r = parseNeedLine("NEED Y has_encoder");
|
|
REQUIRE(r);
|
|
CHECK(planFirmwareConfig(*r, s).sets[0] == "SET Y has_encoder 0");
|
|
|
|
s.have_wiring = false;
|
|
CHECK_FALSE(planFirmwareConfig(*r, s).complete());
|
|
}
|
|
|
|
TEST_CASE("the pitch axis is addressed as P") {
|
|
auto r = parseNeedLine("NEED P center margin");
|
|
REQUIRE(r);
|
|
auto p = planFirmwareConfig(*r, iotaYaw());
|
|
CHECK(p.sets[0] == "SET P center 500000");
|
|
CHECK(p.sets[1] == "SET P margin 5000");
|
|
}
|
|
|
|
// --- End to end through the mock's NEEDCFG mode -----------------------------
|
|
|
|
#include "fgc/mock/MockMotorController.h"
|
|
|
|
TEST_CASE("the mock in NEEDCFG asks, and refuses to move until answered") {
|
|
MockMotorController m(/*needcfg=*/true);
|
|
m.start();
|
|
|
|
auto lines = m.takeNeedLines();
|
|
REQUIRE(lines.size() == 2);
|
|
CHECK(lines[0].rfind("NEED Y ", 0) == 0);
|
|
CHECK(lines[1].rfind("NEED P ", 0) == 0);
|
|
// Taking them clears the queue: they are a request, not a state to poll.
|
|
CHECK(m.takeNeedLines().empty());
|
|
|
|
// Before the answer, motion commands are rejected.
|
|
CHECK_FALSE(m.configured());
|
|
m.sendCommand("HOME");
|
|
CHECK_FALSE(m.telemetry().yaw.ready());
|
|
|
|
// Answer the way the host would.
|
|
AxisConfigSource src;
|
|
src.have_current = true;
|
|
src.global_scaler = 80;
|
|
src.center_count = 500000;
|
|
src.soft_margin = 5000;
|
|
src.have_travel = true;
|
|
src.steps_per_rev = 1210778;
|
|
src.have_deviation = true;
|
|
src.enc_deviation = 1500;
|
|
src.normal_vmax = 39738;
|
|
src.homing_vmax = 30000;
|
|
src.center_vmax = 25000;
|
|
src.have_wiring = true;
|
|
|
|
// Answer BOTH axes, as the host does - the axis unlocks only when every key
|
|
// it asked for has been answered.
|
|
for (const auto& l : lines) {
|
|
auto n = parseNeedLine(l);
|
|
REQUIRE(n);
|
|
auto plan = planFirmwareConfig(*n, src);
|
|
REQUIRE(plan.complete());
|
|
for (const auto& cmd : plan.sets) m.sendCommand(cmd);
|
|
}
|
|
|
|
// The values landed, under the names the firmware asked for.
|
|
const auto got = m.settings();
|
|
CHECK(got.at("Y.steps_per_rev") == "1210778");
|
|
CHECK(got.at("Y.global_scaler") == "80");
|
|
CHECK(got.at("Y.normal_speed") == "39738");
|
|
CHECK(got.at("Y.center") == "500000");
|
|
CHECK(got.at("Y.margin") == "5000");
|
|
|
|
// And the axis now moves.
|
|
CHECK(m.configured());
|
|
m.sendCommand("HOME");
|
|
CHECK(m.telemetry().yaw.ready());
|
|
}
|
|
|
|
TEST_CASE("the default mock is untouched by the handshake") {
|
|
// Every existing test constructs it this way; it must behave as before.
|
|
MockMotorController m;
|
|
m.start();
|
|
CHECK(m.takeNeedLines().empty());
|
|
m.sendCommand("HOME");
|
|
CHECK(m.telemetry().yaw.ready());
|
|
}
|
|
|
|
TEST_CASE("a partial answer leaves the axis in NEEDCFG") {
|
|
// The case that matters: hardware.ini declares most of what the firmware
|
|
// wants, so most SETs land - and the axis still must not move.
|
|
MockMotorController m(/*needcfg=*/true);
|
|
m.start();
|
|
auto lines = m.takeNeedLines();
|
|
REQUIRE(lines.size() == 2);
|
|
|
|
auto need = parseNeedLine(lines[0]);
|
|
REQUIRE(need);
|
|
AxisConfigSource partial; // only the always-known anchors
|
|
partial.center_count = 500000;
|
|
partial.soft_margin = 5000;
|
|
|
|
auto plan = planFirmwareConfig(*need, partial);
|
|
CHECK_FALSE(plan.complete());
|
|
for (const auto& cmd : plan.sets) m.sendCommand(cmd);
|
|
|
|
CHECK_FALSE(m.configured());
|
|
m.sendCommand("HOME");
|
|
CHECK_FALSE(m.telemetry().yaw.ready());
|
|
}
|