Scan sweep now always runs front to back instead of ping-ponging
This commit is contained in:
parent
4d5e2e4194
commit
5053b82811
|
|
@ -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
|
; 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.
|
; line, '#' comments allowed) - edit it to define exact scan coordinates.
|
||||||
; If grid_file is blank, a grid is generated: yaw_intervals positions across
|
; 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 =
|
grid_file =
|
||||||
yaw_intervals = 56
|
yaw_intervals = 56
|
||||||
yaw_min_deg = -90
|
yaw_min_deg = -90
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ corrupted.
|
||||||
3. **Control input** — `IControlChannel::poll()` returns the latest `ControlCommand` (control code + target
|
3. **Control input** — `IControlChannel::poll()` returns the latest `ControlCommand` (control code + target
|
||||||
heading), clearing its "available" flags so each update is acted on once.
|
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:
|
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`.
|
- 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.
|
- 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
|
5. **Acquisition + quality gate** (`GatedCameraSource`) — `trigger()` does not simply take a picture. It
|
||||||
|
|
|
||||||
|
|
@ -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
|
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).
|
`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
|
`[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.
|
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
|
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
|
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
|
firmware `ST` lines (per-axis state + encoder counts), parsed by
|
||||||
[TelemetryParser](../src/core/TelemetryParser.cpp).
|
[TelemetryParser](../src/core/TelemetryParser.cpp).
|
||||||
|
|
|
||||||
|
|
@ -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/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/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/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/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/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) |
|
| [include/fgc/HelpText.h](../include/fgc/HelpText.h), [src/core/HelpText.cpp](../src/core/HelpText.cpp) | Operator command catalog (`helpCatalog`) + `renderHelp` (console & TUI help) |
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ flags** so each update is acted on once (`MqttControlChannel::poll()`).
|
||||||
|
|
||||||
| ControlCode | Behaviour ([CaptureScheduler](../src/core/CaptureScheduler.cpp)) |
|
| 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. |
|
| `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.
|
When a ControlCode message arrives, the program echoes the current code back on the StatusCode topic.
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ namespace fgc {
|
||||||
// layer (see GatedCameraSource) - the scheduler only cares that trigger()
|
// layer (see GatedCameraSource) - the scheduler only cares that trigger()
|
||||||
// eventually succeeds, and retries next tick if it does not.
|
// 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).
|
// ControlCode 1 = drive yaw to the MQTT-supplied target heading (pitch held).
|
||||||
// Degrees are converted to encoder counts via Geometry.
|
// Degrees are converted to encoder counts via Geometry.
|
||||||
class CaptureScheduler {
|
class CaptureScheduler {
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,13 @@ struct ScanPoint {
|
||||||
double pitch_deg = 0.0;
|
double pitch_deg = 0.0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// The ordered list of scan waypoints plus a ping-pong (boustrophedon) cursor:
|
// The ordered list of scan waypoints plus a one-way cursor: the auto-sweep walks
|
||||||
// the auto-sweep walks the list forward to the last point, then backward to the
|
// the list front to back, then returns to the first point and repeats, so every
|
||||||
// first, repeating - so a bounded (e.g. 180 deg) yaw arc never has to wrap.
|
// 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
|
// Points come either from an operator-edited CSV (ScanConfig::grid_file) or, if
|
||||||
// none is given, are generated from the [Scan] parameters.
|
// none is given, are generated from the [Scan] parameters.
|
||||||
|
|
@ -35,14 +39,13 @@ public:
|
||||||
const ScanPoint& current() const { return points_[index_]; }
|
const ScanPoint& current() const { return points_[index_]; }
|
||||||
const std::vector<ScanPoint>& points() const { return points_; }
|
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 advance();
|
||||||
void reset() { index_ = 0; forward_ = true; }
|
void reset() { index_ = 0; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::vector<ScanPoint> points_;
|
std::vector<ScanPoint> points_;
|
||||||
std::size_t index_ = 0;
|
std::size_t index_ = 0;
|
||||||
bool forward_ = true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Exposed for unit testing.
|
// Exposed for unit testing.
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ std::vector<ScanPoint> generateScanGrid(const ScanConfig& cfg) {
|
||||||
if (pitches.empty()) pitches.push_back(0.0);
|
if (pitches.empty()) pitches.push_back(0.0);
|
||||||
int n = cfg.yaw_intervals;
|
int n = cfg.yaw_intervals;
|
||||||
if (n < 1) n = 1;
|
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 (double pitch : pitches) {
|
||||||
for (int i = 0; i < n; ++i) {
|
for (int i = 0; i < n; ++i) {
|
||||||
double frac = n > 1 ? static_cast<double>(i) / (n - 1) : 0.0;
|
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() {
|
void ScanGrid::advance() {
|
||||||
if (points_.size() <= 1) return;
|
if (points_.empty()) return;
|
||||||
if (forward_) {
|
// Wrap rather than reverse: every pass visits the waypoints in the same
|
||||||
if (index_ + 1 < points_.size()) {
|
// order, at the cost of one long return slew back to the start.
|
||||||
++index_;
|
index_ = (index_ + 1) % points_.size();
|
||||||
} else {
|
|
||||||
forward_ = false;
|
|
||||||
--index_;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (index_ > 0) {
|
|
||||||
--index_;
|
|
||||||
} else {
|
|
||||||
forward_ = true;
|
|
||||||
++index_;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace fgc
|
} // namespace fgc
|
||||||
|
|
|
||||||
|
|
@ -33,21 +33,50 @@ TEST_CASE("generateScanGrid produces yaw_intervals x pitch_levels points") {
|
||||||
CHECK(pts[3].pitch_deg == doctest::Approx(30.0));
|
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}});
|
ScanGrid g({{0, 0}, {1, 0}, {2, 0}});
|
||||||
CHECK(g.current().yaw_deg == doctest::Approx(0.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(1.0));
|
||||||
g.advance(); CHECK(g.current().yaw_deg == doctest::Approx(2.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)); // wrapped, not reversed
|
||||||
g.advance(); 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(1.0)); // reversed again
|
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") {
|
TEST_CASE("ScanGrid single point and empty are safe") {
|
||||||
ScanGrid one({{5, 5}});
|
ScanGrid one({{5, 5}});
|
||||||
one.advance();
|
one.advance();
|
||||||
CHECK(one.current().yaw_deg == doctest::Approx(5.0));
|
CHECK(one.current().yaw_deg == doctest::Approx(5.0));
|
||||||
|
one.advance();
|
||||||
|
CHECK(one.index() == 0);
|
||||||
|
|
||||||
ScanGrid none;
|
ScanGrid none;
|
||||||
CHECK(none.empty());
|
CHECK(none.empty());
|
||||||
|
none.advance(); // must not divide by zero or index an empty vector
|
||||||
|
CHECK(none.empty());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue