Fixed bug nudging befor calibration is run

This commit is contained in:
pgdalmeida 2026-06-29 13:58:44 +02:00
parent 25e39e09f2
commit e721ac74c2
Signed by: pedro.almeida
GPG Key ID: D4A6C394DF13F1D7
6 changed files with 78 additions and 14 deletions

View File

@ -217,9 +217,9 @@ away in the log. It only appears once something has run.
When a `gimbal calib` completes it is applied to the live session and the strip shows a highlighted
yes/no prompt — **`Save this calibration to config as the new default? (y / n)`**. Press **`y`** to
write the fitted `[Motor]` `*_counts_per_deg` / `*_zero_count` back into the `config.ini` the program
was launched with (replacing those keys in place, preserving everything else) so it persists across
restarts; press **`n`** to keep it for this session only. The keys are active only while the prompt is
write the full `[Motor]` per-axis map — `*_counts_per_deg`, `*_zero_count`, and `*_min_deg`/`*_max_deg`
— back into the `config.ini` the program was launched with (replacing those keys in place, preserving
everything else) so it persists across restarts; press **`n`** to keep it for this session only. The keys are active only while the prompt is
showing. (Until you answer `y`, calibration remains session-only — see
[known-issues.md](known-issues.md).)

View File

@ -53,7 +53,13 @@ doctest unit-test suite (`ctest`).
- **`gimbal calib` persistence**: the fitted `counts_per_deg`/`zero_count` are always applied to the live
session (display, manual moves, MQTT heading, **and** the capture scheduler) and written to
`logs/calib_*.log`. In the **TUI**, the activity strip then prompts `Save … as the new default? (y/n)`
`y` writes them into `[Motor]` in `config.ini` (persists across restarts), `n` keeps them
`y` writes the full per-axis `[Motor]` map (`*_counts_per_deg`, `*_zero_count`,
`*_min_deg`/`*_max_deg`) into `config.ini` (persists across restarts), `n` keeps them
session-only. In the **headless** console there is no prompt, so calibration stays session-only there;
copy the logged values into `[Motor]` by hand to keep them.
- **`gimbal nudge`** moves a fixed fraction of the **homed** endstop-to-endstop travel (from the
firmware dump), so it is independent of the degrees↔counts calibration. If no dump has been captured
yet (not homed / no `gimbal dump`), nudge requests one and does nothing that press — it never falls
back to the configured degree clamps (which, if `*_min_deg`/`*_max_deg` were unset, produced absurd
±100000° steps).
- DHT11 temperature/humidity is still a Sensors-panel placeholder (the IMU half is integrated).

View File

@ -530,15 +530,17 @@ struct Application::Impl {
return static_cast<bool>(iss >> a >> b);
}
// Soft-limit travel (counts) for an axis from the last dump, else the
// configured degree range converted to counts. Returns 0 if unknown.
long axisRangeCounts(char axis) {
// Homed physical travel (counts) for an axis: the endstop-to-endstop span from
// the last firmware dump. Returns 0 if no dump has been captured yet (caller
// should request one). Deliberately does NOT fall back to the configured degree
// range × counts_per_deg — that conflates calibration with physical travel and,
// when min/max are unset, yields an absurd span.
long homedTravelCounts(char axis) {
DumpData d = parseDump(motor->lastDump());
if (d.valid)
for (const auto& ax : d.axes)
if (ax.axis == axis) return std::labs(ax.lim_pos - ax.lim_neg);
const AxisMap& m = (axis == 'Y') ? cfg.geometry.yaw : cfg.geometry.pitch;
return std::labs(m.toCounts(m.max_deg) - m.toCounts(m.min_deg));
return 0;
}
// `gimbal <subcommand>` — unified, lowercase motor control. `move` is degrees
@ -579,8 +581,16 @@ struct Application::Impl {
char axis = (tok[2][0] == 'p' || tok[2][0] == 'P') ? 'P' : 'Y';
double pct = 0;
try { pct = std::stod(tok[3]); } catch (...) { LOG_WARN << "gimbal nudge: bad percent"; return; }
long range = axisRangeCounts(axis);
if (range <= 0) { LOG_WARN << "gimbal nudge: travel unknown; run gimbal home/dump first"; motor->sendCommand("DUMP"); return; }
// Nudge is a fraction of the *homed* physical travel (endstop-to-endstop
// from the firmware dump), so it is independent of calibration and never
// a fraction of the configured degree clamps. If no dump is available
// yet, request one and bail rather than guessing.
long range = homedTravelCounts(axis);
if (range <= 0) {
LOG_WARN << "gimbal nudge: homed travel unknown; home or 'gimbal dump' first";
motor->sendCommand("DUMP");
return;
}
MotorTelemetry tel = motor->telemetry();
long cur = (axis == 'Y') ? tel.yaw.xenc : tel.pitch.xenc;
long target = cur + static_cast<long>(pct / 100.0 * range);

View File

@ -219,11 +219,19 @@ bool saveMotorCalibration(const std::string& path, const Geometry& geo) {
std::snprintf(b, sizeof(b), "%.10g", v);
return std::string(b);
};
// Persist the full per-axis map, not just the fitted counts_per_deg/zero_count:
// min_deg/max_deg are part of the geometry and consumers (e.g. travel-range
// estimates) rely on them, so writing them keeps the [Motor] block complete and
// self-consistent rather than depending on pre-existing lines.
const std::vector<std::pair<std::string, std::string>> kv = {
{"yaw_counts_per_deg", num(geo.yaw.counts_per_deg)},
{"yaw_zero_count", std::to_string(geo.yaw.zero_count)},
{"yaw_min_deg", num(geo.yaw.min_deg)},
{"yaw_max_deg", num(geo.yaw.max_deg)},
{"pitch_counts_per_deg", num(geo.pitch.counts_per_deg)},
{"pitch_zero_count", std::to_string(geo.pitch.zero_count)},
{"pitch_min_deg", num(geo.pitch.min_deg)},
{"pitch_max_deg", num(geo.pitch.max_deg)},
};
const std::string updated = updateIniSectionKeys(buf.str(), "Motor", kv);

View File

@ -615,12 +615,12 @@ void TuiUi::uiLoop() {
}
if (overlay != Overlay::None && e == Event::Escape) { overlay = Overlay::None; return true; }
// Arrow keys nudge the gimbal in steps (only when no overlay is open):
// Left/Right = yaw -/+5%, Up/Down = pitch +/-10% of travel.
// Left/Right = yaw -/+5%, Up/Down = pitch -/+10% of travel.
if (overlay == Overlay::None && sink_) {
if (e == Event::ArrowLeft) { sink_("gimbal nudge yaw -5"); return true; }
if (e == Event::ArrowRight) { sink_("gimbal nudge yaw 5"); return true; }
if (e == Event::ArrowUp) { sink_("gimbal nudge pitch 10"); return true; }
if (e == Event::ArrowDown) { sink_("gimbal nudge pitch -10"); return true; }
if (e == Event::ArrowUp) { sink_("gimbal nudge pitch -10"); return true; }
if (e == Event::ArrowDown) { sink_("gimbal nudge pitch 10"); return true; }
}
if (!e.is_character()) return false;
const std::string& c = e.character();

View File

@ -3,7 +3,9 @@
#include "fgc/Config.h"
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <fstream>
using namespace fgc;
@ -78,3 +80,41 @@ TEST_CASE("updateIniSectionKeys appends keys missing from the section") {
// re-parsing must see the appended key under [Motor]
CHECK(out.find("pitch_zero_count = 7") != std::string::npos);
}
TEST_CASE("saveMotorCalibration round-trips the full per-axis map") {
// Write a starting config, save a calibrated Geometry into it, reload, and
// confirm every [Motor] parameter (incl. min/max_deg) survives, while another
// section is left untouched.
const std::string path = "test_calib_roundtrip.ini";
{
std::ofstream f(path, std::ios::trunc);
f << "[General]\ntower_name = rig7\n\n"
"[Motor]\n"
"yaw_counts_per_deg = 1.0\n"
"yaw_zero_count = 0\n"
"yaw_min_deg = -90\n"
"yaw_max_deg = 90\n"
"pitch_counts_per_deg = 1.0\n"
"pitch_zero_count = 0\n"
"pitch_min_deg = 0\n"
"pitch_max_deg = 60\n";
}
Geometry geo;
geo.yaw = {983.33, 500123, -88.0, 92.0};
geo.pitch = {8333.33, -4200, -2.0, 61.5};
REQUIRE(saveMotorCalibration(path, geo));
AppConfig cfg = ConfigLoader::loadFromFile(path);
CHECK(cfg.general.tower_name == "rig7"); // unrelated section preserved
CHECK(cfg.geometry.yaw.counts_per_deg == doctest::Approx(983.33));
CHECK(cfg.geometry.yaw.zero_count == 500123);
CHECK(cfg.geometry.yaw.min_deg == doctest::Approx(-88.0));
CHECK(cfg.geometry.yaw.max_deg == doctest::Approx(92.0));
CHECK(cfg.geometry.pitch.counts_per_deg == doctest::Approx(8333.33));
CHECK(cfg.geometry.pitch.zero_count == -4200);
CHECK(cfg.geometry.pitch.min_deg == doctest::Approx(-2.0));
CHECK(cfg.geometry.pitch.max_deg == doctest::Approx(61.5));
std::remove(path.c_str());
}