Rework radar system
This commit is contained in:
+15
-21
@@ -10,12 +10,6 @@
|
||||
|
||||
namespace radar::processing {
|
||||
|
||||
enum class HistoryCommand {
|
||||
None,
|
||||
RemoveLast,
|
||||
ClearAll,
|
||||
};
|
||||
|
||||
enum class LegacyGprMode {
|
||||
Point,
|
||||
Extended,
|
||||
@@ -29,16 +23,16 @@ enum class LegacyGprReferenceMode {
|
||||
FirstTxEvent,
|
||||
};
|
||||
|
||||
struct HistoryTarget {
|
||||
std::uint64_t collection_id = 0;
|
||||
std::uint64_t capture_start_ns = 0;
|
||||
};
|
||||
|
||||
struct ProcessingLiveConfig {
|
||||
std::string processor_mode = "pass_through";
|
||||
std::string pass_through_channel = "s21";
|
||||
bool pass_through_fixed_y_enabled = false;
|
||||
float pass_through_y_min_db = -100.0F;
|
||||
float pass_through_y_max_db = 0.0F;
|
||||
std::string bscan_axis = "abs";
|
||||
std::string bscan_channel = "s21";
|
||||
float bscan_cut_m = 0.0F;
|
||||
float bscan_max_depth_m = 8.0F;
|
||||
float bscan_cut_m = 0.824F;
|
||||
float bscan_max_depth_m = 1.0F;
|
||||
float bscan_gain = 1.0F;
|
||||
float bscan_start_freq_mhz = 100.0F;
|
||||
float bscan_stop_freq_mhz = 8800.0F;
|
||||
@@ -51,13 +45,12 @@ struct ProcessingLiveConfig {
|
||||
float gpr_max_depth_m = 14.0F;
|
||||
float gpr_range_comp_power = 0.1F;
|
||||
float gpr_comp_power = 0.2F;
|
||||
// BP object-detection stop level, as a fraction of the global peak (Python
|
||||
// Horns_motion_3libre.py BP_OBJECT_MIN_FRAC); peaks below it are not objects.
|
||||
// BP object-detection stop level, as a fraction of the global peak; peaks
|
||||
// below it are not objects.
|
||||
float gpr_object_min_frac = 0.7F;
|
||||
std::string gpr_score_mode = "combined";
|
||||
// Backprojection intra-sweep speed-correction mode: "int_minus" (full
|
||||
// correction) or "int_focus" (focusing residual only). Mirrors the Python
|
||||
// Horns_motion_3libre.py MOTION_CORRECTION_MODE selector.
|
||||
// correction) or "int_focus" (focusing residual only).
|
||||
std::string gpr_motion_mode = "int_minus";
|
||||
float gpr_speed_m_s = 0.0F;
|
||||
float gpr_look_angle_deg = 0.0F;
|
||||
@@ -81,8 +74,8 @@ struct ProcessingLiveConfig {
|
||||
// Default 0 keeps legacy 1D antenna layouts imaging in the antenna plane.
|
||||
float gpr_imaging_plane_y_m = 0.0F;
|
||||
// Coherent BP object visibility (window + the draw limits below) is applied in
|
||||
// the processor itself, matching Horns_motion_3libre.py — there is NO score
|
||||
// threshold for it. Only legacy GPR still thresholds, on a pair count.
|
||||
// the processor itself, with no score threshold. Only legacy GPR thresholds,
|
||||
// on a pair count.
|
||||
float legacy_gpr_min_visible_pair_count = 0.0F;
|
||||
std::uint32_t gpr_max_detected_objects_to_draw = 0;
|
||||
std::uint32_t gpr_draw_top_m_objects = 0;
|
||||
@@ -111,8 +104,9 @@ struct ProcessingLiveConfig {
|
||||
// "ignore socket speed" checkbox.
|
||||
bool ignore_socket_speed = false;
|
||||
bool reprocess_current_result = true;
|
||||
std::uint64_t history_command_seq = 0;
|
||||
HistoryCommand history_command = HistoryCommand::None;
|
||||
// Persistent desired history: normal settings writes must preserve deletions.
|
||||
std::uint64_t history_clear_before_ns = 0;
|
||||
std::vector<HistoryTarget> history_removed{};
|
||||
std::uint64_t settings_revision = 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "data_processor.hpp"
|
||||
#include "runtime_wait.hpp"
|
||||
#include "pipeline_runtime.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
@@ -69,9 +70,7 @@ void DataProcessor::run(const std::atomic<bool>& stop_requested) {
|
||||
std::vector<ipc::PreprocessedCollection> preprocessed_history{};
|
||||
const std::size_t history_limit = replay_history_limit(config_);
|
||||
std::uint64_t last_replayed_revision = live_config_loader_.revision();
|
||||
std::uint64_t last_applied_history_command_seq = 0;
|
||||
std::uint64_t error_count = 0;
|
||||
std::uint64_t consecutive_errors = 0;
|
||||
runtime::CollectionErrors errors;
|
||||
std::uint64_t oversized_results = 0;
|
||||
// track the socket-fed speed used for the last reprocess so a change
|
||||
// arriving without a live-config revision bump still triggers a reprocess of
|
||||
@@ -95,17 +94,19 @@ void DataProcessor::run(const std::atomic<bool>& stop_requested) {
|
||||
auto& processor = resolve_processor(live_config);
|
||||
const bool socket_speed_dirty = current_socket_speed != last_reprocessed_socket_speed;
|
||||
|
||||
if (live_revision != last_replayed_revision || socket_speed_dirty) {
|
||||
if (live_config.history_command_seq > last_applied_history_command_seq) {
|
||||
if (live_config.history_command == HistoryCommand::RemoveLast) {
|
||||
if (!preprocessed_history.empty()) {
|
||||
preprocessed_history.pop_back();
|
||||
}
|
||||
} else if (live_config.history_command == HistoryCommand::ClearAll) {
|
||||
preprocessed_history.clear();
|
||||
}
|
||||
last_applied_history_command_seq = live_config.history_command_seq;
|
||||
const auto was_removed = [&](const ipc::PreprocessedCollection& collection) {
|
||||
if (live_config.history_clear_before_ns &&
|
||||
collection.capture_start_ns <= live_config.history_clear_before_ns) {
|
||||
return true;
|
||||
}
|
||||
return std::any_of(live_config.history_removed.begin(), live_config.history_removed.end(),
|
||||
[&](const HistoryTarget& target) {
|
||||
return collection.collection_id == target.collection_id &&
|
||||
collection.capture_start_ns == target.capture_start_ns;
|
||||
});
|
||||
};
|
||||
if (live_revision != last_replayed_revision || socket_speed_dirty) {
|
||||
std::erase_if(preprocessed_history, was_removed);
|
||||
|
||||
if (!live_config.reprocess_current_result) {
|
||||
// Socket-fed speed updates should affect only future preprocessed collections,
|
||||
@@ -139,6 +140,12 @@ void DataProcessor::run(const std::atomic<bool>& stop_requested) {
|
||||
|
||||
if (preprocessed_ring_.pop(bytes)) {
|
||||
auto preprocessed = ipc::deserialize_preprocessed_collection(bytes);
|
||||
if (!preprocessed.capture_start_ns || preprocessed.capture_end_ns < preprocessed.capture_start_ns) {
|
||||
throw std::runtime_error("Preprocessed collection requires a valid acquisition window");
|
||||
}
|
||||
if (was_removed(preprocessed)) {
|
||||
continue; // A deletion also applies to queued input and later GUI replays.
|
||||
}
|
||||
// Sweeps the GUI feeds back to recompute its B-scan keep their original
|
||||
// timestamps, so they are no newer than the newest retained sweep. They
|
||||
// are processed like any other but stay out of the history, which must
|
||||
@@ -146,7 +153,7 @@ void DataProcessor::run(const std::atomic<bool>& stop_requested) {
|
||||
// background from it), and away from the locator, which reports the
|
||||
// current scene.
|
||||
const bool fed_back = !preprocessed_history.empty() &&
|
||||
preprocessed.monotonic_ns <= preprocessed_history.back().monotonic_ns;
|
||||
preprocessed.capture_start_ns <= preprocessed_history.back().capture_start_ns;
|
||||
if (!fed_back) {
|
||||
preprocessed_history.push_back(std::move(preprocessed));
|
||||
while (preprocessed_history.size() > history_limit) {
|
||||
@@ -157,7 +164,7 @@ void DataProcessor::run(const std::atomic<bool>& stop_requested) {
|
||||
const auto earlier = std::partition_point(
|
||||
preprocessed_history.begin(),
|
||||
preprocessed_history.end(),
|
||||
[&](const ipc::PreprocessedCollection& retained) { return retained.monotonic_ns < current.monotonic_ns; }
|
||||
[&](const ipc::PreprocessedCollection& retained) { return retained.capture_start_ns < current.capture_start_ns; }
|
||||
);
|
||||
|
||||
const auto result_collection = process_collection(
|
||||
@@ -170,29 +177,13 @@ void DataProcessor::run(const std::atomic<bool>& stop_requested) {
|
||||
if (!fed_back) {
|
||||
publish_locator(result_collection, live_config);
|
||||
}
|
||||
consecutive_errors = 0; // made progress; reset the escalation counter
|
||||
errors.progress();
|
||||
continue;
|
||||
}
|
||||
|
||||
(void)config::wait_or_stop(config_.runtime.idle_sleep_ms, stop_requested);
|
||||
} catch (const std::exception& exc) {
|
||||
// A single bad collection (torn ring slot, decode/processing error) must
|
||||
// not kill the long-running processor: drop it and keep going.
|
||||
++error_count;
|
||||
++consecutive_errors;
|
||||
// Escalate a PERSISTENT failure (a deterministic config error drops every
|
||||
// collection forever): log it loudly and distinctly — not buried in the
|
||||
// every-100 throttle — so liveness-only supervision still surfaces it.
|
||||
if (consecutive_errors % 500 == 0) {
|
||||
std::cerr << "data_processor: " << consecutive_errors
|
||||
<< " consecutive failures — likely a permanent configuration error: "
|
||||
<< exc.what() << '\n';
|
||||
} else if (error_count % 100 == 1) {
|
||||
std::cerr << "data_processor: dropped collection after error (count="
|
||||
<< error_count << "): " << exc.what() << '\n';
|
||||
}
|
||||
// Back-off (floored at 1ms even if idle_sleep_ms==0) so a persistent error
|
||||
// cannot busy-spin a core or flood the log.
|
||||
errors.report("data_processor", exc);
|
||||
(void)config::wait_or_stop(std::max(1U, config_.runtime.idle_sleep_ms), stop_requested);
|
||||
}
|
||||
}
|
||||
@@ -210,15 +201,13 @@ auto DataProcessor::process_collection(
|
||||
result.processing_duration_ns = static_cast<std::uint64_t>(
|
||||
std::chrono::duration_cast<std::chrono::nanoseconds>(finished_at - started_at).count()
|
||||
);
|
||||
// Four 16-bit words retain the uint64 exactly in the existing float32 table format.
|
||||
ipc::ResultPayload revision{};
|
||||
revision.processing_name = "processing_revision";
|
||||
revision.kind = ipc::ResultKind::TableF32;
|
||||
revision.table_columns = 4U;
|
||||
for (unsigned shift = 0; shift < 64; shift += 16) {
|
||||
revision.table_values.push_back(static_cast<float>((live_config.settings_revision >> shift) & 0xffffU));
|
||||
}
|
||||
result.collection_payloads.push_back(std::move(revision));
|
||||
result.collection_id = preprocessed.collection_id;
|
||||
result.monotonic_ns = ipc::current_monotonic_ns();
|
||||
result.settings_revision = live_config.settings_revision;
|
||||
result.capture_start_ns = preprocessed.capture_start_ns;
|
||||
result.capture_end_ns = preprocessed.capture_end_ns;
|
||||
// Socket speed can change without a GUI revision; retain the actual value per result.
|
||||
result.speed_m_s = live_config.gpr_speed_m_s;
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -268,7 +257,7 @@ auto DataProcessor::build_locator_filter(const ProcessingLiveConfig& live_config
|
||||
filter.draw_limits.reset();
|
||||
}
|
||||
// Coherent BP already emits the FINAL visible object set from the processor
|
||||
// (window + N/M, no score threshold — matching Horns_motion_3libre.py), so the
|
||||
// (window + N/M, no score threshold), so the
|
||||
// socket forwards it verbatim. The default FilterParams passes everything through
|
||||
// (it only drops non-finite rows), keeping the filtering logic in one place.
|
||||
return filter;
|
||||
|
||||
@@ -1,43 +1,14 @@
|
||||
#include <atomic>
|
||||
#include <csignal>
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "data_processor.hpp"
|
||||
#include "locator/tcp_server.hpp"
|
||||
#include "run_config.hpp"
|
||||
#include "pipeline_runtime.hpp"
|
||||
#include "shm_ring.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* kDefaultConfigPath = "run_config.json";
|
||||
|
||||
std::atomic<bool> g_stop_requested{false};
|
||||
|
||||
void signal_handler(int /*signal*/) {
|
||||
g_stop_requested.store(true, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void install_signal_handlers() {
|
||||
std::signal(SIGINT, signal_handler);
|
||||
std::signal(SIGTERM, signal_handler);
|
||||
// Writing to a peer-closed socket would otherwise terminate the process.
|
||||
std::signal(SIGPIPE, SIG_IGN);
|
||||
}
|
||||
|
||||
[[nodiscard]] auto read_config_path(int argc, char** argv) -> std::string {
|
||||
std::string config_path = kDefaultConfigPath;
|
||||
for (int index = 1; index < argc; ++index) {
|
||||
const std::string arg = argv[index];
|
||||
if (arg == "--config" && (index + 1) < argc) {
|
||||
config_path = argv[++index];
|
||||
}
|
||||
}
|
||||
return config_path;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto start_locator_server(const radar::config::RunConfig& config)
|
||||
-> std::unique_ptr<radar::locator::TcpServer> {
|
||||
if (!config.locator_server.enabled) {
|
||||
@@ -56,11 +27,8 @@ void install_signal_handlers() {
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
install_signal_handlers();
|
||||
|
||||
try {
|
||||
const auto config_path = read_config_path(argc, argv);
|
||||
const auto config = radar::config::load_run_config(config_path);
|
||||
const auto config = radar::runtime::initialize(argc, argv);
|
||||
|
||||
auto preprocessed_ring = radar::ipc::ShmRing::open_or_create(
|
||||
config.rings.preprocessed.name,
|
||||
@@ -82,7 +50,7 @@ int main(int argc, char** argv) {
|
||||
radar::processing::create_default_processors(),
|
||||
locator_server.get()
|
||||
);
|
||||
processor.run(g_stop_requested);
|
||||
processor.run(radar::runtime::stop_requested);
|
||||
return 0;
|
||||
} catch (const std::exception& exception) {
|
||||
std::cerr << "data_processor error: " << exception.what() << '\n';
|
||||
|
||||
@@ -19,19 +19,6 @@ namespace {
|
||||
|
||||
using Json = nlohmann::json;
|
||||
|
||||
[[nodiscard]] auto parse_history_command(const std::string& value) -> HistoryCommand {
|
||||
if (value == "none") {
|
||||
return HistoryCommand::None;
|
||||
}
|
||||
if (value == "remove_last") {
|
||||
return HistoryCommand::RemoveLast;
|
||||
}
|
||||
if (value == "clear_all") {
|
||||
return HistoryCommand::ClearAll;
|
||||
}
|
||||
throw std::runtime_error("processing.history_command must be one of: none, remove_last, clear_all");
|
||||
}
|
||||
|
||||
[[nodiscard]] auto parse_legacy_gpr_mode(const std::string& value, const std::string& field_name) -> LegacyGprMode {
|
||||
if (value == "point") {
|
||||
return LegacyGprMode::Point;
|
||||
@@ -86,13 +73,6 @@ void apply_legacy_gpr_algorithm_alias(ProcessingLiveConfig& config, const std::s
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] auto parse_s_parameter_channel(const std::string& value, const std::string& field_name) -> std::string {
|
||||
if (value == "s21" || value == "s11") {
|
||||
return value;
|
||||
}
|
||||
throw std::runtime_error(field_name + " must be one of: s21, s11");
|
||||
}
|
||||
|
||||
[[nodiscard]] auto parse_f32(const Json& value, const std::string& field_name) -> float {
|
||||
if (!value.is_number()) {
|
||||
throw std::runtime_error(field_name + " must be number");
|
||||
@@ -226,23 +206,7 @@ auto parse_geometry(const Json& value, const char* position_field, std::uint32_t
|
||||
config::validate_gpr_config(*config.gpr, run_config);
|
||||
}
|
||||
assign_optional(root, "processor_mode", config.processor_mode);
|
||||
if (const auto found = root.find("pass_through_channel"); found != root.end()) {
|
||||
if (!found->is_string()) {
|
||||
throw std::runtime_error("processing.pass_through_channel must be string");
|
||||
}
|
||||
config.pass_through_channel =
|
||||
parse_s_parameter_channel(found->get<std::string>(), "processing.pass_through_channel");
|
||||
}
|
||||
assign_optional(root, "pass_through_fixed_y_enabled", config.pass_through_fixed_y_enabled);
|
||||
assign_optional(root, "pass_through_y_min_db", config.pass_through_y_min_db);
|
||||
assign_optional(root, "pass_through_y_max_db", config.pass_through_y_max_db);
|
||||
assign_optional(root, "bscan_axis", config.bscan_axis);
|
||||
if (const auto found = root.find("bscan_channel"); found != root.end()) {
|
||||
if (!found->is_string()) {
|
||||
throw std::runtime_error("processing.bscan_channel must be string");
|
||||
}
|
||||
config.bscan_channel = parse_s_parameter_channel(found->get<std::string>(), "processing.bscan_channel");
|
||||
}
|
||||
assign_optional(root, "bscan_cut_m", config.bscan_cut_m);
|
||||
assign_optional(root, "bscan_max_depth_m", config.bscan_max_depth_m);
|
||||
assign_optional(root, "bscan_gain", config.bscan_gain);
|
||||
@@ -344,14 +308,23 @@ auto parse_geometry(const Json& value, const char* position_field, std::uint32_t
|
||||
}
|
||||
assign_optional(root, "ignore_socket_speed", config.ignore_socket_speed);
|
||||
assign_optional(root, "reprocess_current_result", config.reprocess_current_result);
|
||||
if (const auto found = root.find("history_command_seq"); found != root.end()) {
|
||||
config.history_command_seq = parse_u64_number(*found, "processing.history_command_seq");
|
||||
if (const auto found = root.find("history_clear_before_ns"); found != root.end()) {
|
||||
config.history_clear_before_ns = parse_u64_number(*found, "processing.history_clear_before_ns");
|
||||
}
|
||||
if (const auto found = root.find("history_command"); found != root.end()) {
|
||||
if (!found->is_string()) {
|
||||
throw std::runtime_error("processing.history_command must be string");
|
||||
if (const auto found = root.find("history_removed"); found != root.end()) {
|
||||
if (!found->is_array()) {
|
||||
throw std::runtime_error("processing.history_removed must be an array");
|
||||
}
|
||||
for (const auto& value : *found) {
|
||||
HistoryTarget target{
|
||||
parse_u64_number(value.at("collection_id"), "history_removed.collection_id"),
|
||||
parse_u64_number(value.at("capture_start_ns"), "history_removed.capture_start_ns"),
|
||||
};
|
||||
if (!target.capture_start_ns) {
|
||||
throw std::runtime_error("history_removed.capture_start_ns must be positive");
|
||||
}
|
||||
config.history_removed.push_back(target);
|
||||
}
|
||||
config.history_command = parse_history_command(found->get<std::string>());
|
||||
}
|
||||
return config;
|
||||
}
|
||||
@@ -393,9 +366,10 @@ auto ProcessingLiveConfigLoader::refresh_if_needed() -> ProcessingLiveConfig {
|
||||
return current_;
|
||||
}
|
||||
|
||||
current_ = parse_live_config(json_text, path_, run_config_);
|
||||
// Remember rejected content too; retry only when the file actually changes.
|
||||
last_json_text_ = json_text;
|
||||
has_last_json_text_ = true;
|
||||
current_ = parse_live_config(json_text, path_, run_config_);
|
||||
++revision_;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << "data_processor warning: failed to refresh live processing config: " << error.what() << '\n';
|
||||
|
||||
Reference in New Issue
Block a user