Take the scan waypoints only from the CSV file, never generate them
This commit is contained in:
parent
ab9fa3dbb0
commit
a21be0d4e8
|
|
@ -206,19 +206,17 @@ homing_timeout_s = 65
|
||||||
|
|
||||||
[Scan]
|
[Scan]
|
||||||
; Capture scan grid (the (yaw,pitch) waypoints auto-sweep steps through).
|
; Capture scan grid (the (yaw,pitch) waypoints auto-sweep steps through).
|
||||||
; If grid_file is set, that CSV is used verbatim (one "yaw_deg,pitch_deg" per
|
; grid_file is the ONLY source: that CSV is used verbatim (one
|
||||||
; line, '#' comments allowed) - edit it to define exact scan coordinates.
|
; "yaw_deg,pitch_deg" per line, '#' comments allowed). Nothing is generated -
|
||||||
; If grid_file is blank, a grid is generated: yaw_intervals positions across
|
; leave it blank and auto-sweep stays off with a message saying so, rather than
|
||||||
; [yaw_min_deg, yaw_max_deg] at each pitch level.
|
; the tower inventing an arc nobody chose.
|
||||||
|
; A relative path resolves against the directory of the config file it is
|
||||||
|
; written in (config.ini sits at the repo root, so: config/scan.csv).
|
||||||
; The sweep always runs front to back, then returns to the first waypoint and
|
; 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
|
; 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
|
; leg is one long slew (the yaw arc cannot wrap), so order the grid with that
|
||||||
; in mind.
|
; in mind.
|
||||||
grid_file =
|
grid_file = config/scan.csv
|
||||||
yaw_intervals = 56
|
|
||||||
yaw_min_deg = -90
|
|
||||||
yaw_max_deg = 90
|
|
||||||
pitch_levels = 10,30,50
|
|
||||||
|
|
||||||
[Paths]
|
[Paths]
|
||||||
; Directory for saved .jxl images. Supports leading ~ and $ENV expansion.
|
; Directory for saved .jxl images. Supports leading ~ and $ENV expansion.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
# Staeffelsberg scan grid: 56 bearings round the full circle at one elevation,
|
||||||
|
# matching the old firmware's 'ud56' sweep. Generated once from the retired
|
||||||
|
# [Scan] grid generator (0 -> 353.57 deg in 56 steps, pitch 0) and kept here as
|
||||||
|
# the explicit list, because the CSV is now the only source of waypoints.
|
||||||
|
#
|
||||||
|
# The sweep runs first to last, then returns to the first and repeats - always
|
||||||
|
# the same order, never in reverse. The return leg is one long slew back across
|
||||||
|
# the arc; that is the price of every pass being directly comparable.
|
||||||
|
0.00,0
|
||||||
|
6.43,0
|
||||||
|
12.86,0
|
||||||
|
19.29,0
|
||||||
|
25.71,0
|
||||||
|
32.14,0
|
||||||
|
38.57,0
|
||||||
|
45.00,0
|
||||||
|
51.43,0
|
||||||
|
57.86,0
|
||||||
|
64.29,0
|
||||||
|
70.71,0
|
||||||
|
77.14,0
|
||||||
|
83.57,0
|
||||||
|
90.00,0
|
||||||
|
96.43,0
|
||||||
|
102.86,0
|
||||||
|
109.29,0
|
||||||
|
115.71,0
|
||||||
|
122.14,0
|
||||||
|
128.57,0
|
||||||
|
135.00,0
|
||||||
|
141.43,0
|
||||||
|
147.86,0
|
||||||
|
154.29,0
|
||||||
|
160.71,0
|
||||||
|
167.14,0
|
||||||
|
173.57,0
|
||||||
|
180.00,0
|
||||||
|
186.43,0
|
||||||
|
192.86,0
|
||||||
|
199.28,0
|
||||||
|
205.71,0
|
||||||
|
212.14,0
|
||||||
|
218.57,0
|
||||||
|
225.00,0
|
||||||
|
231.43,0
|
||||||
|
237.86,0
|
||||||
|
244.28,0
|
||||||
|
250.71,0
|
||||||
|
257.14,0
|
||||||
|
263.57,0
|
||||||
|
270.00,0
|
||||||
|
276.43,0
|
||||||
|
282.86,0
|
||||||
|
289.28,0
|
||||||
|
295.71,0
|
||||||
|
302.14,0
|
||||||
|
308.57,0
|
||||||
|
315.00,0
|
||||||
|
321.43,0
|
||||||
|
327.86,0
|
||||||
|
334.28,0
|
||||||
|
340.71,0
|
||||||
|
347.14,0
|
||||||
|
353.57,0
|
||||||
|
|
|
@ -1,24 +1,30 @@
|
||||||
# Scan grid — capture waypoints," one ""yaw_deg","pitch_deg"" per line."
|
# Scan grid - capture waypoints, one "yaw_deg,pitch_deg" per line.
|
||||||
# Edit these to define the exact scan coordinates. Blank lines and lines,,
|
# Edit these to define the exact scan coordinates. Blank lines and lines
|
||||||
# starting with '#' are ignored. The auto-sweep (ControlCode 0) walks the list,,
|
# starting with '#' are ignored. This file is the only source of waypoints:
|
||||||
# forward then backward (ping-pong), triggering the cameras at each settled,
|
# nothing is generated, so what the tower photographs is always what is written
|
||||||
# point. Degrees are converted to encoder counts via the [Motor] calibration.,,
|
# here.
|
||||||
#,,
|
#
|
||||||
# Reference this file from config.ini: [Scan] grid_file = config/scan.csv,,
|
# The auto-sweep (ControlCode 0) walks the list from the first waypoint to the
|
||||||
#,,
|
# last, triggering the cameras at each settled point, then returns to the first
|
||||||
# Example: three elevation rows across a 180° yaw arc.,,
|
# and repeats - always in the same order, never in reverse. That return leg is
|
||||||
0,10,
|
# one long slew (the yaw arc cannot wrap), so order the list with it in mind.
|
||||||
45,10,
|
# Degrees are converted to encoder counts via the [Motor] calibration.
|
||||||
90,10,
|
#
|
||||||
135,10,
|
# Reference this file from config.ini: [Scan] grid_file = config/scan.csv
|
||||||
180,10,
|
#
|
||||||
0,5,
|
# Example: three elevation rows across a 180 deg yaw arc.
|
||||||
45,5,
|
0,10
|
||||||
90,5,
|
45,10
|
||||||
135,5,
|
90,10
|
||||||
180,5,
|
135,10
|
||||||
0,15,
|
180,10
|
||||||
45,15,
|
0,5
|
||||||
90,15,
|
45,5
|
||||||
135,15,
|
90,5
|
||||||
180,15,
|
135,5
|
||||||
|
180,5
|
||||||
|
0,15
|
||||||
|
45,15
|
||||||
|
90,15
|
||||||
|
135,15
|
||||||
|
180,15
|
||||||
|
|
|
||||||
|
Can't render this file because it contains an unexpected character in line 1 and column 38.
|
|
|
@ -86,10 +86,7 @@ Parsed and validated by `ConfigLoader` ([src/core/Config.cpp](../src/core/Config
|
||||||
| `Motor.<axis>` | `enc_deviation` | int | — | \|XACTUAL−XENC\| that means "position untrustworthy". **hardware.ini** |
|
| `Motor.<axis>` | `enc_deviation` | int | — | \|XACTUAL−XENC\| that means "position untrustworthy". **hardware.ini** |
|
||||||
| `Motor.<axis>` | `enc_const` | float | 12.8 | Microsteps per encoder count (the TMC's `ENC_CONST`), up to 4 decimals. **hardware.ini**, optional: only a drivetrain whose encoder differs declares it. Wrong by a few percent, the step counter and encoder drift apart as the axis turns, and homing takes the drift for a stop (staeffelsberg: 12.5, from its old firmware) |
|
| `Motor.<axis>` | `enc_const` | float | 12.8 | Microsteps per encoder count (the TMC's `ENC_CONST`), up to 4 decimals. **hardware.ini**, optional: only a drivetrain whose encoder differs declares it. Wrong by a few percent, the step counter and encoder drift apart as the axis turns, and homing takes the drift for a stop (staeffelsberg: 12.5, from its old firmware) |
|
||||||
| `Motor.<axis>` | `sw_mode` / `has_encoder` | int / bool | — | TMC `SW_MODE` (physical endstop switches on REFL/REFR) and whether this axis's encoder is aligned and usable. **hardware.ini** |
|
| `Motor.<axis>` | `sw_mode` / `has_encoder` | int / bool | — | TMC `SW_MODE` (physical endstop switches on REFL/REFR) and whether this axis's encoder is aligned and usable. **hardware.ini** |
|
||||||
| `Scan` | `grid_file` | string | — | CSV of `yaw_deg,pitch_deg` waypoints; empty → generate |
|
| `Scan` | `grid_file` | string | — | CSV of `yaw_deg,pitch_deg` waypoints. The only source of a scan grid; empty → auto-sweep disabled |
|
||||||
| `Scan` | `yaw_intervals` | int | `56` | Generated yaw positions across `[yaw_min_deg, yaw_max_deg]` |
|
|
||||||
| `Scan` | `yaw_min_deg`/`yaw_max_deg` | float | `-90`/`90` | Generated yaw arc |
|
|
||||||
| `Scan` | `pitch_levels` | csv | `0` | Generated pitch elevations (deg) |
|
|
||||||
|
|
||||||
### `[Motor]` calibration & `[Scan]` grid
|
### `[Motor]` calibration & `[Scan]` grid
|
||||||
|
|
||||||
|
|
@ -179,15 +176,21 @@ the tower can measure it. Yaw is untouched by it.
|
||||||
#### Scan grid
|
#### Scan grid
|
||||||
|
|
||||||
The capture **scan grid** is the ordered `(yaw,pitch)` waypoints auto-sweep visits, always front to
|
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
|
back, then round to the first again — never in reverse. `[Scan] grid_file` points at an editable CSV
|
||||||
coordinates, or leave it blank to generate `yaw_intervals × pitch_levels` points.
|
([config/scan.csv](../config/scan.csv)) and is the **only** source of waypoints.
|
||||||
|
|
||||||
|
**Nothing is generated.** Leave `grid_file` blank (or name a file that is missing, malformed or
|
||||||
|
empty) and the program logs why and leaves auto-sweep off, showing the reason in the TUI's scan-grid
|
||||||
|
panel. A tower that invented its own arc would archive images under bearings nobody chose, with no
|
||||||
|
file to read back to find out what was photographed. The pre-2026-09 keys `yaw_intervals`,
|
||||||
|
`yaw_min_deg`, `yaw_max_deg` and `pitch_levels` are gone; a config still carrying them gets a
|
||||||
|
startup warning naming them.
|
||||||
|
|
||||||
**Grid yaw values are true bearings.** Yaw travel is under a full turn, so there is always an
|
**Grid yaw values are true bearings.** Yaw travel is under a full turn, so there is always an
|
||||||
unreachable arc; waypoints falling in it are **skipped** with a warning rather than clamped to the
|
unreachable arc; waypoints falling in it are **skipped** with a warning rather than clamped to the
|
||||||
nearest limit — clamping would image a direction nobody asked for and publish it under the bearing
|
nearest limit — clamping would image a direction nobody asked for and publish it under the bearing
|
||||||
they did. A one-line audit after each home reports how many waypoints are affected. Generated grids
|
they did. A one-line audit after each home reports how many waypoints are affected. Write an arc
|
||||||
interpolate `yaw_min_deg → yaw_max_deg` linearly and wrap each point, so an arc crossing north is
|
crossing north monotonically (`340` → `380`) rather than letting it wrap back through zero.
|
||||||
written monotonically (`340` → `380`).
|
|
||||||
|
|
||||||
Each camera's output subfolder is its `[Camera.<name>] label` (defaulting to the block name); nothing is derived from the order of the blocks.
|
Each camera's output subfolder is its `[Camera.<name>] label` (defaulting to the block name); nothing is derived from the order of the blocks.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ Per-file reference for the refactored tree, plus the shared data structures.
|
||||||
| [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/FileLogSink.h](../include/fgc/FileLogSink.h), [src/core/FileLogSink.cpp](../src/core/FileLogSink.cpp) | The persistent log file: append, size rotation, count-based pruning, a self-describing header per file. Runs inside `Logger`'s non-recursive lock, so it never logs - failures latch into `failed()`/`lastError()` for the caller to report |
|
| [include/fgc/FileLogSink.h](../include/fgc/FileLogSink.h), [src/core/FileLogSink.cpp](../src/core/FileLogSink.cpp) | The persistent log file: append, size rotation, count-based pruning, a self-describing header per file. Runs inside `Logger`'s non-recursive lock, so it never logs - failures latch into `failed()`/`lastError()` for the caller to report |
|
||||||
| [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) + one-way wrapping cursor (`[Scan]`) |
|
| [include/fgc/ScanGrid.h](../include/fgc/ScanGrid.h), [src/core/ScanGrid.cpp](../src/core/ScanGrid.cpp) | Capture waypoints (CSV only, never 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) |
|
||||||
|
|
|
||||||
|
|
@ -317,14 +317,11 @@ struct UiConfig {
|
||||||
};
|
};
|
||||||
|
|
||||||
// [Scan]: source of the capture scan grid (the (yaw,pitch) waypoints the
|
// [Scan]: source of the capture scan grid (the (yaw,pitch) waypoints the
|
||||||
// auto-sweep steps through). If grid_file is set, the CSV is loaded verbatim;
|
// auto-sweep steps through). The CSV is the only source - it is loaded verbatim
|
||||||
// otherwise a grid is generated from the parameters below (see ScanGrid).
|
// and there is no generated fallback, so what a tower photographs is always a
|
||||||
|
// file somebody wrote and can be read back (see ScanGrid).
|
||||||
struct ScanConfig {
|
struct ScanConfig {
|
||||||
std::string grid_file; // path to an editable yaw_deg,pitch_deg CSV; empty => generate
|
std::string grid_file; // path to an editable yaw_deg,pitch_deg CSV; required for auto-sweep
|
||||||
int yaw_intervals = 56; // generated: yaw positions across [yaw_min_deg, yaw_max_deg]
|
|
||||||
double yaw_min_deg = -90.0;
|
|
||||||
double yaw_max_deg = 90.0;
|
|
||||||
std::string pitch_levels = "0"; // generated: comma list of pitch elevations (deg)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// [Test] / [TestProfile.<name>]: the hardware self-test command. A profile is a
|
// [Test] / [TestProfile.<name>]: the hardware self-test command. A profile is a
|
||||||
|
|
|
||||||
|
|
@ -22,15 +22,17 @@ struct ScanPoint {
|
||||||
// The cost is a long return slew from the last waypoint to the first, since the
|
// 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.
|
// 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 from an operator-edited CSV (ScanConfig::grid_file) and nowhere
|
||||||
// none is given, are generated from the [Scan] parameters.
|
// else. There is deliberately no fallback grid: a tower that invented its own
|
||||||
|
// waypoints would archive images under bearings nobody chose, and the operator
|
||||||
|
// would have no file to point at to find out what was photographed.
|
||||||
class ScanGrid {
|
class ScanGrid {
|
||||||
public:
|
public:
|
||||||
ScanGrid() = default;
|
ScanGrid() = default;
|
||||||
explicit ScanGrid(std::vector<ScanPoint> points) : points_(std::move(points)) {}
|
explicit ScanGrid(std::vector<ScanPoint> points) : points_(std::move(points)) {}
|
||||||
|
|
||||||
// Build from config: load grid_file if set (throws std::runtime_error on a
|
// Load grid_file. Throws std::runtime_error if it is unset, missing,
|
||||||
// missing/!malformed file), else generate from the parameters.
|
// malformed or empty; the caller reports that and leaves auto-sweep off.
|
||||||
static ScanGrid fromConfig(const ScanConfig& cfg);
|
static ScanGrid fromConfig(const ScanConfig& cfg);
|
||||||
|
|
||||||
bool empty() const { return points_.empty(); }
|
bool empty() const { return points_.empty(); }
|
||||||
|
|
@ -50,6 +52,5 @@ private:
|
||||||
|
|
||||||
// Exposed for unit testing.
|
// Exposed for unit testing.
|
||||||
std::vector<ScanPoint> parseScanCsv(const std::string& text);
|
std::vector<ScanPoint> parseScanCsv(const std::string& text);
|
||||||
std::vector<ScanPoint> generateScanGrid(const ScanConfig& cfg);
|
|
||||||
|
|
||||||
} // namespace fgc
|
} // namespace fgc
|
||||||
|
|
|
||||||
|
|
@ -157,7 +157,6 @@ struct CaptureView {
|
||||||
// Defined scan grid (the auto-mode waypoints) with the live cursor
|
// Defined scan grid (the auto-mode waypoints) with the live cursor
|
||||||
// flagged, surfaced in the expanded camera view.
|
// flagged, surfaced in the expanded camera view.
|
||||||
std::vector<ScanWaypointView> scan_grid;
|
std::vector<ScanWaypointView> scan_grid;
|
||||||
bool scan_from_file = false; // grid_file CSV vs generated
|
|
||||||
bool scan_pitch = false; // 2-axis rig (show pitch column)
|
bool scan_pitch = false; // 2-axis rig (show pitch column)
|
||||||
std::string scan_error; // grid_file load failure (auto-sweep off)
|
std::string scan_error; // grid_file load failure (auto-sweep off)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -925,7 +925,6 @@ struct Application::Impl {
|
||||||
|
|
||||||
// Defined scan grid + live cursor (read on the control thread, same as the
|
// Defined scan grid + live cursor (read on the control thread, same as the
|
||||||
// scheduler that mutates it, so no locking is needed).
|
// scheduler that mutates it, so no locking is needed).
|
||||||
s.capture.scan_from_file = !cfg.scan.grid_file.empty();
|
|
||||||
s.capture.scan_pitch = t.pitch_present;
|
s.capture.scan_pitch = t.pitch_present;
|
||||||
s.capture.scan_error = scan_load_error_;
|
s.capture.scan_error = scan_load_error_;
|
||||||
s.capture.scan_grid.clear();
|
s.capture.scan_grid.clear();
|
||||||
|
|
|
||||||
|
|
@ -521,12 +521,8 @@ AppConfig ConfigLoader::fromMap(const std::map<std::string, std::string>& kv) {
|
||||||
axisDrivetrain("yaw", cfg.drivetrain.yaw);
|
axisDrivetrain("yaw", cfg.drivetrain.yaw);
|
||||||
axisDrivetrain("pitch", cfg.drivetrain.pitch);
|
axisDrivetrain("pitch", cfg.drivetrain.pitch);
|
||||||
|
|
||||||
// [Scan]: scan-grid source (explicit CSV file, else generated).
|
// [Scan]: the scan-grid CSV. No file, no auto-sweep - nothing is generated.
|
||||||
cfg.scan.grid_file = get(kv, "Scan.grid_file", cfg.scan.grid_file);
|
cfg.scan.grid_file = get(kv, "Scan.grid_file", cfg.scan.grid_file);
|
||||||
cfg.scan.yaw_intervals = getInt(kv, "Scan.yaw_intervals", cfg.scan.yaw_intervals);
|
|
||||||
cfg.scan.yaw_min_deg = getDouble(kv, "Scan.yaw_min_deg", cfg.scan.yaw_min_deg);
|
|
||||||
cfg.scan.yaw_max_deg = getDouble(kv, "Scan.yaw_max_deg", cfg.scan.yaw_max_deg);
|
|
||||||
cfg.scan.pitch_levels = get(kv, "Scan.pitch_levels", cfg.scan.pitch_levels);
|
|
||||||
|
|
||||||
// ---- validation ----
|
// ---- validation ----
|
||||||
if (cfg.general.image_interval <= 0)
|
if (cfg.general.image_interval <= 0)
|
||||||
|
|
@ -601,6 +597,20 @@ AppConfig ConfigLoader::fromMap(const std::map<std::string, std::string>& kv) {
|
||||||
"setup'), and drivetrain keys belong under [Motor.yaw] / "
|
"setup'), and drivetrain keys belong under [Motor.yaw] / "
|
||||||
"[Motor.pitch], not [Motor]";
|
"[Motor.pitch], not [Motor]";
|
||||||
|
|
||||||
|
// Keys from when a blank grid_file meant "generate a grid". The generator is
|
||||||
|
// gone: the CSV is the only source. Ignoring these quietly would leave an
|
||||||
|
// operator believing they had set the scan arc when nothing reads it.
|
||||||
|
static const char* kRemovedScanKeys[] = {
|
||||||
|
"Scan.yaw_intervals", "Scan.yaw_min_deg", "Scan.yaw_max_deg", "Scan.pitch_levels",
|
||||||
|
};
|
||||||
|
std::string stale_scan;
|
||||||
|
for (const char* k : kRemovedScanKeys)
|
||||||
|
if (kv.count(k)) stale_scan += (stale_scan.empty() ? "" : ", ") + std::string(k + 5);
|
||||||
|
if (!stale_scan.empty())
|
||||||
|
LOG_WARN << "[Scan] ignoring keys nothing reads: " << stale_scan
|
||||||
|
<< " -- grids are no longer generated; list the waypoints in the "
|
||||||
|
"grid_file CSV instead";
|
||||||
|
|
||||||
return cfg;
|
return cfg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,20 +16,6 @@ std::string trim(const std::string& s) {
|
||||||
return s.substr(b, e - b + 1);
|
return s.substr(b, e - b + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<double> parseDoubleList(const std::string& csv) {
|
|
||||||
std::vector<double> out;
|
|
||||||
size_t start = 0;
|
|
||||||
while (start <= csv.size()) {
|
|
||||||
size_t comma = csv.find(',', start);
|
|
||||||
std::string tok =
|
|
||||||
trim(csv.substr(start, comma == std::string::npos ? std::string::npos : comma - start));
|
|
||||||
if (!tok.empty()) out.push_back(std::stod(tok));
|
|
||||||
if (comma == std::string::npos) break;
|
|
||||||
start = comma + 1;
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
std::vector<ScanPoint> parseScanCsv(const std::string& text) {
|
std::vector<ScanPoint> parseScanCsv(const std::string& text) {
|
||||||
|
|
@ -58,37 +44,21 @@ std::vector<ScanPoint> parseScanCsv(const std::string& text) {
|
||||||
return pts;
|
return pts;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<ScanPoint> generateScanGrid(const ScanConfig& cfg) {
|
|
||||||
std::vector<ScanPoint> pts;
|
|
||||||
std::vector<double> pitches = parseDoubleList(cfg.pitch_levels);
|
|
||||||
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 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;
|
|
||||||
ScanPoint p;
|
|
||||||
p.yaw_deg = cfg.yaw_min_deg + frac * (cfg.yaw_max_deg - cfg.yaw_min_deg);
|
|
||||||
p.pitch_deg = pitch;
|
|
||||||
pts.push_back(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pts;
|
|
||||||
}
|
|
||||||
|
|
||||||
ScanGrid ScanGrid::fromConfig(const ScanConfig& cfg) {
|
ScanGrid ScanGrid::fromConfig(const ScanConfig& cfg) {
|
||||||
if (!cfg.grid_file.empty()) {
|
// No grid_file, no grid. Generating a default sweep here would put a tower
|
||||||
|
// on the air photographing bearings nobody picked, under a configuration
|
||||||
|
// that shows no waypoints at all; refusing leaves auto-sweep off and says
|
||||||
|
// exactly what is missing.
|
||||||
|
if (cfg.grid_file.empty())
|
||||||
|
throw std::runtime_error("no scan grid configured: set [Scan] grid_file to a CSV of "
|
||||||
|
"'yaw_deg,pitch_deg' waypoints (see config/scan.csv)");
|
||||||
std::ifstream f(cfg.grid_file);
|
std::ifstream f(cfg.grid_file);
|
||||||
if (!f) throw std::runtime_error("cannot open scan grid file: " + cfg.grid_file);
|
if (!f) throw std::runtime_error("cannot open scan grid file: " + cfg.grid_file);
|
||||||
std::stringstream buf;
|
std::stringstream buf;
|
||||||
buf << f.rdbuf();
|
buf << f.rdbuf();
|
||||||
auto pts = parseScanCsv(buf.str());
|
auto pts = parseScanCsv(buf.str());
|
||||||
if (pts.empty())
|
if (pts.empty()) throw std::runtime_error("scan grid file has no waypoints: " + cfg.grid_file);
|
||||||
throw std::runtime_error("scan grid file has no waypoints: " + cfg.grid_file);
|
|
||||||
return ScanGrid(std::move(pts));
|
return ScanGrid(std::move(pts));
|
||||||
}
|
|
||||||
return ScanGrid(generateScanGrid(cfg));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ScanGrid::advance() {
|
void ScanGrid::advance() {
|
||||||
|
|
|
||||||
|
|
@ -501,8 +501,7 @@ Element cameraDetailPanel(const CaptureView& c, ScrollState& st) {
|
||||||
|
|
||||||
// --- Scan grid ---
|
// --- Scan grid ---
|
||||||
rows.push_back(separator());
|
rows.push_back(separator());
|
||||||
rows.push_back(hbox({text("SCAN GRID ") | bold | color(Color::Blue),
|
rows.push_back(hbox({text("SCAN GRID ") | bold | color(Color::Blue), filler(),
|
||||||
text(c.scan_from_file ? "(CSV file)" : "(generated)") | dim, filler(),
|
|
||||||
text(std::to_string(c.scan_grid.size()) + " waypoints") | dim}));
|
text(std::to_string(c.scan_grid.size()) + " waypoints") | dim}));
|
||||||
|
|
||||||
if (!c.scan_error.empty()) {
|
if (!c.scan_error.empty()) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
#include "fgc/ScanGrid.h"
|
#include "fgc/ScanGrid.h"
|
||||||
|
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
using namespace fgc;
|
using namespace fgc;
|
||||||
|
|
||||||
TEST_CASE("parseScanCsv reads waypoints and skips comments/blanks") {
|
TEST_CASE("parseScanCsv reads waypoints and skips comments/blanks") {
|
||||||
|
|
@ -17,20 +19,18 @@ TEST_CASE("parseScanCsv throws on a malformed line") {
|
||||||
CHECK_THROWS(parseScanCsv("1,abc\n"));
|
CHECK_THROWS(parseScanCsv("1,abc\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("generateScanGrid produces yaw_intervals x pitch_levels points") {
|
TEST_CASE("fromConfig refuses to invent a grid when no file is named") {
|
||||||
|
// The generator is gone on purpose: a blank grid_file must leave auto-sweep
|
||||||
|
// off, not produce waypoints nobody chose.
|
||||||
ScanConfig c;
|
ScanConfig c;
|
||||||
c.yaw_intervals = 3;
|
CHECK(c.grid_file.empty());
|
||||||
c.yaw_min_deg = -90;
|
CHECK_THROWS_AS(ScanGrid::fromConfig(c), std::runtime_error);
|
||||||
c.yaw_max_deg = 90;
|
}
|
||||||
c.pitch_levels = "10,30";
|
|
||||||
auto pts = generateScanGrid(c);
|
TEST_CASE("fromConfig reports a grid_file that is not there") {
|
||||||
REQUIRE(pts.size() == 6);
|
ScanConfig c;
|
||||||
// pitch-major, yaw inner (min -> max)
|
c.grid_file = "/nonexistent/definitely-not-a-scan-grid.csv";
|
||||||
CHECK(pts[0].yaw_deg == doctest::Approx(-90.0));
|
CHECK_THROWS_AS(ScanGrid::fromConfig(c), std::runtime_error);
|
||||||
CHECK(pts[0].pitch_deg == doctest::Approx(10.0));
|
|
||||||
CHECK(pts[1].yaw_deg == doctest::Approx(0.0));
|
|
||||||
CHECK(pts[2].yaw_deg == doctest::Approx(90.0));
|
|
||||||
CHECK(pts[3].pitch_deg == doctest::Approx(30.0));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("ScanGrid cursor always runs front to back") {
|
TEST_CASE("ScanGrid cursor always runs front to back") {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue