Scan sweep now always runs front to back instead of ping-ponging

This commit is contained in:
pgdalmeida 2026-08-26 10:01:16 +02:00
parent 4d5e2e4194
commit 5053b82811
Signed by: pedro.almeida
GPG Key ID: D4A6C394DF13F1D7
9 changed files with 60 additions and 35 deletions

View File

@ -188,7 +188,11 @@ enc_error_warn_counts = 400
; If grid_file is set, that CSV is used verbatim (one "yaw_deg,pitch_deg" per
; line, '#' comments allowed) - edit it to define exact scan coordinates.
; If grid_file is blank, a grid is generated: yaw_intervals positions across
; [yaw_min_deg, yaw_max_deg] at each pitch level, traversed ping-pong.
; [yaw_min_deg, yaw_max_deg] at each pitch level.
; The sweep always runs front to back, then returns to the first waypoint and
; repeats, so every pass captures the waypoints in the same order. The return
; leg is one long slew (the yaw arc cannot wrap), so order the grid with that
; in mind.
grid_file =
yaw_intervals = 56
yaw_min_deg = -90

View File

@ -72,7 +72,7 @@ corrupted.
3. **Control input** — `IControlChannel::poll()` returns the latest `ControlCommand` (control code + target
heading), clearing its "available" flags so each update is acted on once.
4. **Capture cycle** (`CaptureScheduler::tick`, per 10 ms) — a move → settle → trigger machine:
- ControlCode 0: `MOVE <yaw>,<pitch>` to the next `ScanGrid` waypoint (ping-pong).
- ControlCode 0: `MOVE <yaw>,<pitch>` to the next `ScanGrid` waypoint (front to back, then wrap).
- ControlCode 1: `MOVE` yaw to `target_HDG` (pitch held), converted to counts via `Geometry`.
- Trigger the cameras once **both axes report standstill at the target**, then advance the grid.
5. **Acquisition + quality gate** (`GatedCameraSource`) — `trigger()` does not simply take a picture. It

View File

@ -130,7 +130,8 @@ the live session immediately; in the TUI the activity strip then offers to save
new default (`y`/`n`). `gimbal stop` cancels a run. Timings (dwell, no-rotation duration, etc.) are the
`CalibParams` defaults in [CalibrationRoutine.h](../include/fgc/CalibrationRoutine.h).
The capture **scan grid** is the ordered `(yaw,pitch)` waypoints auto-sweep visits (ping-pong). Set
The capture **scan grid** is the ordered `(yaw,pitch)` waypoints auto-sweep visits, always front to
back. Set
`[Scan] grid_file` to an editable CSV ([config/scan.csv](../config/scan.csv)) to define exact
coordinates, or leave it blank to generate `yaw_intervals × pitch_levels` points.
@ -387,6 +388,6 @@ The firmware speaks **full-word, newline-terminated** commands in absolute **enc
Capture is a **move → settle → trigger** cycle: the scheduler issues a `MOVE`, waits until both
axes report standstill at the target, then triggers the cameras. **ControlCode 0** walks the scan
grid (ping-pong); **ControlCode 1** drives yaw to `target_HDG` (pitch held). Telemetry arrives as
grid (front to back, repeating); **ControlCode 1** drives yaw to `target_HDG` (pitch held). Telemetry arrives as
firmware `ST` lines (per-axis state + encoder counts), parsed by
[TelemetryParser](../src/core/TelemetryParser.cpp).

View File

@ -10,7 +10,7 @@ Per-file reference for the refactored tree, plus the shared data structures.
| [include/fgc/Paths.h](../include/fgc/Paths.h), [src/core/Paths.cpp](../src/core/Paths.cpp) | `~`/`$ENV` expansion, executable dir, config search order, default output dir |
| [include/fgc/Logger.h](../include/fgc/Logger.h), [src/core/Logger.cpp](../src/core/Logger.cpp) | Leveled, thread-safe logger + per-category wire trace; `LOG_TRACE..LOG_ERROR`, `LOG_TRACE_CAT` |
| [include/fgc/Geometry.h](../include/fgc/Geometry.h), [src/core/Geometry.cpp](../src/core/Geometry.cpp) | Per-axis degrees↔encoder-counts affine map (`[Motor]` calibration) |
| [include/fgc/ScanGrid.h](../include/fgc/ScanGrid.h), [src/core/ScanGrid.cpp](../src/core/ScanGrid.cpp) | Capture waypoints (CSV or generated) + ping-pong cursor (`[Scan]`) |
| [include/fgc/ScanGrid.h](../include/fgc/ScanGrid.h), [src/core/ScanGrid.cpp](../src/core/ScanGrid.cpp) | Capture waypoints (CSV or generated) + one-way wrapping cursor (`[Scan]`) |
| [include/fgc/TelemetryParser.h](../include/fgc/TelemetryParser.h), [src/core/TelemetryParser.cpp](../src/core/TelemetryParser.cpp) | `parseTelemetryLine` (firmware `ST` line) → `std::optional<MotorTelemetry>` |
| [include/fgc/CommandParser.h](../include/fgc/CommandParser.h), [src/core/CommandParser.cpp](../src/core/CommandParser.cpp) | `parseCommand` whitespace tokenizer → `Command` |
| [include/fgc/HelpText.h](../include/fgc/HelpText.h), [src/core/HelpText.cpp](../src/core/HelpText.cpp) | Operator command catalog (`helpCatalog`) + `renderHelp` (console & TUI help) |

View File

@ -41,7 +41,7 @@ flags** so each update is acted on once (`MqttControlChannel::poll()`).
| ControlCode | Behaviour ([CaptureScheduler](../src/core/CaptureScheduler.cpp)) |
|-------------|---------------------------------------------------|
| `0` | **Automatic sweep.** Walk the scan grid (ping-pong): `MOVE` to the next `(yaw,pitch)` waypoint, wait for standstill, trigger cameras. |
| `0` | **Automatic sweep.** Walk the scan grid front to back, then start over: `MOVE` to the next `(yaw,pitch)` waypoint, wait for standstill, trigger cameras. |
| `1` | **Directed.** `MOVE` yaw to `target_HDG` (pitch held), wait for standstill, trigger. |
When a ControlCode message arrives, the program echoes the current code back on the StatusCode topic.

View File

@ -26,7 +26,7 @@ namespace fgc {
// layer (see GatedCameraSource) - the scheduler only cares that trigger()
// eventually succeeds, and retries next tick if it does not.
//
// ControlCode 0 = automatic sweep through the ScanGrid (ping-pong);
// ControlCode 0 = automatic sweep through the ScanGrid (front to back, repeating);
// ControlCode 1 = drive yaw to the MQTT-supplied target heading (pitch held).
// Degrees are converted to encoder counts via Geometry.
class CaptureScheduler {

View File

@ -14,9 +14,13 @@ struct ScanPoint {
double pitch_deg = 0.0;
};
// The ordered list of scan waypoints plus a ping-pong (boustrophedon) cursor:
// the auto-sweep walks the list forward to the last point, then backward to the
// first, repeating - so a bounded (e.g. 180 deg) yaw arc never has to wrap.
// The ordered list of scan waypoints plus a one-way cursor: the auto-sweep walks
// the list front to back, then returns to the first point and repeats, so every
// pass covers the waypoints in the same order and each image sequence is
// directly comparable to the last.
//
// The cost is a long return slew from the last waypoint to the first, since the
// yaw arc is bounded and cannot wrap. Order the grid so that leg is acceptable.
//
// Points come either from an operator-edited CSV (ScanConfig::grid_file) or, if
// none is given, are generated from the [Scan] parameters.
@ -35,14 +39,13 @@ public:
const ScanPoint& current() const { return points_[index_]; }
const std::vector<ScanPoint>& points() const { return points_; }
// Step the cursor one waypoint, reversing direction at either end.
// Step the cursor one waypoint, wrapping from the last back to the first.
void advance();
void reset() { index_ = 0; forward_ = true; }
void reset() { index_ = 0; }
private:
std::vector<ScanPoint> points_;
std::size_t index_ = 0;
bool forward_ = true;
std::size_t index_ = 0;
};
// Exposed for unit testing.

View File

@ -64,7 +64,7 @@ std::vector<ScanPoint> generateScanGrid(const ScanConfig& cfg) {
if (pitches.empty()) pitches.push_back(0.0);
int n = cfg.yaw_intervals;
if (n < 1) n = 1;
// pitch-major, yaw inner (min -> max); the runtime cursor ping-pongs the whole list.
// pitch-major, yaw inner (min -> max); the runtime cursor walks the whole list in order.
for (double pitch : pitches) {
for (int i = 0; i < n; ++i) {
double frac = n > 1 ? static_cast<double>(i) / (n - 1) : 0.0;
@ -92,22 +92,10 @@ ScanGrid ScanGrid::fromConfig(const ScanConfig& cfg) {
}
void ScanGrid::advance() {
if (points_.size() <= 1) return;
if (forward_) {
if (index_ + 1 < points_.size()) {
++index_;
} else {
forward_ = false;
--index_;
}
} else {
if (index_ > 0) {
--index_;
} else {
forward_ = true;
++index_;
}
}
if (points_.empty()) return;
// Wrap rather than reverse: every pass visits the waypoints in the same
// order, at the cost of one long return slew back to the start.
index_ = (index_ + 1) % points_.size();
}
} // namespace fgc

View File

@ -33,21 +33,50 @@ TEST_CASE("generateScanGrid produces yaw_intervals x pitch_levels points") {
CHECK(pts[3].pitch_deg == doctest::Approx(30.0));
}
TEST_CASE("ScanGrid cursor ping-pongs at both ends") {
TEST_CASE("ScanGrid cursor always runs front to back") {
ScanGrid g({{0, 0}, {1, 0}, {2, 0}});
CHECK(g.current().yaw_deg == doctest::Approx(0.0));
g.advance(); CHECK(g.current().yaw_deg == doctest::Approx(1.0));
g.advance(); CHECK(g.current().yaw_deg == doctest::Approx(2.0));
g.advance(); CHECK(g.current().yaw_deg == doctest::Approx(1.0)); // reversed
g.advance(); CHECK(g.current().yaw_deg == doctest::Approx(0.0));
g.advance(); CHECK(g.current().yaw_deg == doctest::Approx(1.0)); // reversed again
g.advance(); CHECK(g.current().yaw_deg == doctest::Approx(0.0)); // wrapped, not reversed
g.advance(); CHECK(g.current().yaw_deg == doctest::Approx(1.0));
g.advance(); CHECK(g.current().yaw_deg == doctest::Approx(2.0));
g.advance(); CHECK(g.current().yaw_deg == doctest::Approx(0.0)); // and again
}
TEST_CASE("ScanGrid index wraps in step with the waypoints") {
ScanGrid g({{0, 0}, {1, 0}, {2, 0}, {3, 0}});
for (std::size_t pass = 0; pass < 3; ++pass) {
for (std::size_t i = 0; i < g.size(); ++i) {
CHECK(g.index() == i);
CHECK(g.current().yaw_deg == doctest::Approx(static_cast<double>(i)));
g.advance();
}
}
CHECK(g.index() == 0);
}
TEST_CASE("ScanGrid reset returns to the first waypoint") {
ScanGrid g({{0, 0}, {1, 0}, {2, 0}});
g.advance();
g.advance();
CHECK(g.index() == 2);
g.reset();
CHECK(g.index() == 0);
CHECK(g.current().yaw_deg == doctest::Approx(0.0));
// Still forward after a reset - there is no direction state to get stuck in.
g.advance(); CHECK(g.current().yaw_deg == doctest::Approx(1.0));
}
TEST_CASE("ScanGrid single point and empty are safe") {
ScanGrid one({{5, 5}});
one.advance();
CHECK(one.current().yaw_deg == doctest::Approx(5.0));
one.advance();
CHECK(one.index() == 0);
ScanGrid none;
CHECK(none.empty());
none.advance(); // must not divide by zero or index an empty vector
CHECK(none.empty());
}