libstp
Robotics library for Botball
Loading...
Searching...
No Matches
recorder.hpp
Go to the documentation of this file.
1#pragma once
2
3// Post-run localization recorder.
4//
5// Captures particle-filter state to a JSONL file (one object per line)
6// for offline visualization in the Web-IDE. Format contract:
7// modules/libstp-localization/docs/recording-format.md
8//
9// Threading contract (see recording-format.md §"Recorder Threading Contract"):
10// * recordFrame() runs on the filter's tick thread, *while holding the
11// filter mutex*. It MUST be cheap — snapshot copy + queue push, no I/O,
12// no logging from the hot path.
13// * A dedicated writer std::jthread pops frames and serializes JSON. Joined
14// on stop()/dtor.
15// * Overflow policy: drop OLDEST. The filter is never stalled by writer
16// backpressure — debug recording is best-effort.
17//
18// Disk contract (buffer_in_ram, default true):
19// * The writer serializes into a chunked in-RAM buffer and performs NO disk
20// I/O during the run. The whole recording is written to the file once, at
21// stop(). This is deliberate: streaming 100+ MB to the SD card mid-run
22// saturates the card's I/O and starves *other* processes' real-time work
23// (e.g. the STM32 SPI reader's control loop) even though this thread never
24// blocks the filter. Set buffer_in_ram=false to stream+flush periodically
25// instead (crash-resilient, but reintroduces mid-run disk pressure) — use
26// only when debugging a crash that truncates the recording.
27
28#include "foundation/types.hpp"
31
32#include <Eigen/Core>
33#include <atomic>
34#include <chrono>
35#include <condition_variable>
36#include <cstddef>
37#include <cstdint>
38#include <deque>
39#include <fstream>
40#include <memory>
41#include <mutex>
42#include <optional>
43#include <string>
44#include <utility>
45#include <vector>
46
47namespace libstp::localization {
48
50 std::string path;
51 double record_hz{20.0};
52 double tick_hz{100.0};
54 // Pre-formatted JSON fragments embedded verbatim into the header. The
55 // caller (Python binding / CLI) is responsible for valid JSON — the
56 // recorder does not escape these. Use "null" / "[]" for "unset".
57 std::string robot_json{"null"};
58 std::string sensors_json{"[]"};
59 std::string table_map_json{"null"};
60 std::string notes{};
61 // Bounded queue size. Default ~50 s headroom at 20 Hz.
62 std::size_t queue_capacity{1024};
63 // How often (in frames) the writer fsyncs/flushes the stream. Only used
64 // when buffer_in_ram is false (streaming mode).
65 std::size_t flush_every{32};
66 // Buffer the entire recording in RAM and write it to disk only at stop(),
67 // instead of streaming to the SD card during the run. See "Disk contract"
68 // above. Default true (never touch the disk mid-run).
69 bool buffer_in_ram{true};
70 // Retention: keep only the N most recent localization recordings (this one
71 // plus the N-1 previous) and delete older ones at start, so ~150 MB-per-run
72 // files don't fill the SD card. Matches sibling recordings by filename
73 // across run directories: <runs_root>/*/<this file's name>. 0 or negative
74 // disables pruning (keep everything).
76};
77
79public:
80 // One snapshot worth of filter state. Constructed under the filter mutex.
81 struct FrameInput {
83 Eigen::Vector3d estimate_sigma{0.0, 0.0, 0.0};
84 std::optional<libstp::foundation::Pose> odom_delta;
85 // (pose, weight) for each particle. Caller copies in.
86 std::vector<std::pair<libstp::foundation::Pose, double>> particles;
87 // Observation snapshot collected since last recorded frame. Empty
88 // on most ticks; populated only at resync points.
89 std::vector<Observation::SurfaceMeasurement> observations;
90 std::optional<libstp::foundation::Pose> observation_pose;
91 std::optional<Eigen::Vector3d> observation_sigma;
92 bool resampled{false};
93 // Monotonic capture instant, stamped inside recordFrame() when the frame
94 // is accepted. The writer derives the recording's relative time axis from
95 // this (not from a re-sampled clock at serialize time), so the axis is both
96 // jitter-free and immune to wall-clock steps. Callers leave it default.
97 std::chrono::steady_clock::time_point capture_time{};
98 };
99
102
107
108 // Submit a frame. Non-blocking. Drops oldest on overflow. Downsamples
109 // internally based on record_hz/tick_hz, EXCEPT when ``force`` is true
110 // (set this when observations or a resample happened — these must not
111 // be dropped). Returns true if the frame was queued.
112 bool recordFrame(FrameInput frame, bool force);
113
114 // Idempotent. Flushes and joins the writer thread.
115 void stop();
116
117 // False if the file could not be opened. recordFrame becomes a no-op.
118 [[nodiscard]] bool ok() const noexcept { return m_ok.load(); }
119
120private:
121 void writerLoop(libstp::threading::stop_token stop);
122 void writeHeader();
123 void serializeFrame(const FrameInput& frame, std::uint64_t t_ns);
124
125 // Route serialized bytes to the RAM buffer (buffer_in_ram) or straight to
126 // the ofstream (streaming). Called only from the writer thread / ctor.
127 void emit(const char* data, std::size_t n);
128 // Write the RAM buffer to the ofstream. Called once, from stop(), after the
129 // writer thread has been joined (so no concurrent access to the buffer).
130 void flushRamToDisk();
131
132 // Delete older sibling recordings, keeping only the newest
133 // m_cfg.retain_recordings (this run included). Best-effort; never throws.
134 // Called once from the constructor after the new file is opened.
135 void pruneOldRecordings();
136
137 RecorderConfig m_cfg;
138 std::uint64_t m_startedAtUnixNs{0};
139 std::chrono::system_clock::time_point m_startWall{};
140 // Monotonic t=0 for the relative per-frame time axis. Captured together with
141 // m_startWall so the two share an origin, but the frame timeline is measured
142 // against this steady anchor and never jumps if the system clock is stepped.
143 std::chrono::steady_clock::time_point m_startSteady{};
144
145 // Downsampling — recordFrame() side (filter thread).
146 std::uint64_t m_tickCounter{0};
147 std::uint32_t m_downsampleStride{1};
148
149 // Bounded SPSC-ish queue. We keep it simple: mutex + condvar. The hot
150 // path (recordFrame) holds the queue mutex for ≤ one push / one drop.
151 std::mutex m_queueMutex;
152 std::condition_variable m_queueCv;
153 std::deque<FrameInput> m_queue;
154 std::uint64_t m_droppedSinceLastReport{0};
155
156 std::atomic<bool> m_ok{false};
157 std::atomic<bool> m_stopRequested{false};
158
159 std::ofstream m_out;
160
161 // RAM buffer (buffer_in_ram mode). Chunked into ~1 MiB blocks so the buffer
162 // grows without giant contiguous reallocations / 2× peak memory. Touched
163 // only by the writer thread during the run and by stop() after the join.
164 static constexpr std::size_t kRamChunkBytes = std::size_t{1} << 20; // 1 MiB
165 std::deque<std::string> m_ramChunks;
166 std::string m_ramCurrent;
167
169};
170
171} // namespace libstp::localization
LocalizationRecorder & operator=(const LocalizationRecorder &)=delete
LocalizationRecorder(LocalizationRecorder &&)=delete
LocalizationRecorder & operator=(LocalizationRecorder &&)=delete
LocalizationRecorder(const LocalizationRecorder &)=delete
bool recordFrame(FrameInput frame, bool force)
compat::stop_token stop_token
compat::jthread jthread
Position plus planar heading used by several motion-related modules.
Definition types.hpp:12
std::vector< Observation::SurfaceMeasurement > observations
Definition recorder.hpp:89
std::optional< libstp::foundation::Pose > observation_pose
Definition recorder.hpp:90
std::vector< std::pair< libstp::foundation::Pose, double > > particles
Definition recorder.hpp:86
std::optional< libstp::foundation::Pose > odom_delta
Definition recorder.hpp:84
std::chrono::steady_clock::time_point capture_time
Definition recorder.hpp:97
std::optional< Eigen::Vector3d > observation_sigma
Definition recorder.hpp:91
int particle_count_hint
For the header line.
Definition recorder.hpp:53
double record_hz
Target rate after downsampling.
Definition recorder.hpp:51
std::string path
Absolute target file path.
Definition recorder.hpp:50
double tick_hz
For the header line.
Definition recorder.hpp:52