Branch data Line data Source code
1 : : #ifndef ASYNCLOGGER_H
2 : : #define ASYNCLOGGER_H
3 : :
4 : : #include <vector>
5 : : #include <string>
6 : : #include <fstream>
7 : : #include <thread>
8 : : #include <mutex>
9 : : #include <condition_variable>
10 : : #include <atomic>
11 : : #include <chrono>
12 : : #include <iomanip>
13 : : #include <sstream>
14 : : #include <algorithm> // For std::max
15 : : #include <filesystem>
16 : :
17 : : // Forward declaration
18 : : struct TelemInfoV01;
19 : : class FFBEngine;
20 : :
21 : : // Log frame structure - captures one physics tick
22 : : struct LogFrame {
23 : : double timestamp;
24 : : double delta_time;
25 : :
26 : : // Driver Inputs
27 : : float steering;
28 : : float throttle;
29 : : float brake;
30 : :
31 : : // Vehicle State
32 : : float speed; // m/s
33 : : float lat_accel; // m/s²
34 : : float long_accel; // m/s²
35 : : float yaw_rate; // rad/s
36 : :
37 : : // Front Axle - Raw Telemetry
38 : : float slip_angle_fl;
39 : : float slip_angle_fr;
40 : : float slip_ratio_fl;
41 : : float slip_ratio_fr;
42 : : float grip_fl;
43 : : float grip_fr;
44 : : float load_fl;
45 : : float load_fr;
46 : :
47 : : // Front Axle - Calculated
48 : : float calc_slip_angle_front;
49 : : float calc_grip_front;
50 : :
51 : : // Slope Detection Specific
52 : : float dG_dt; // Derivative of lateral G
53 : : float dAlpha_dt; // Derivative of slip angle
54 : : float slope_current; // dG/dAlpha ratio
55 : : float slope_raw_unclamped; // NEW v0.7.38
56 : : float slope_numerator; // NEW v0.7.38
57 : : float slope_denominator; // NEW v0.7.38
58 : : float hold_timer; // NEW v0.7.38
59 : : float input_slip_smoothed; // NEW v0.7.38
60 : : float slope_smoothed; // Smoothed grip output
61 : : float confidence; // Confidence factor (v0.7.3)
62 : : float surface_type_fl; // NEW v0.7.39
63 : : float surface_type_fr; // NEW v0.7.39
64 : : float slope_torque; // NEW v0.7.40
65 : : float slew_limited_g; // NEW v0.7.40
66 : :
67 : : // Rear Axle
68 : : float calc_grip_rear;
69 : : float grip_delta; // Front - Rear
70 : :
71 : : // FFB Output
72 : : float ffb_total; // Normalized output
73 : : float ffb_base; // Base steering shaft force
74 : : float ffb_shaft_torque; // NEW v0.7.62 (Issue #138)
75 : : float ffb_gen_torque; // NEW v0.7.62 (Issue #138)
76 : : float ffb_sop; // Seat of Pants force
77 : : float ffb_grip_factor; // Applied grip modulation
78 : : float speed_gate; // Speed gate factor
79 : : float load_peak_ref; // NEW: Dynamic normalization reference
80 : : bool clipping; // Output clipping flag
81 : :
82 : : // User Markers
83 : : bool marker; // User-triggered marker
84 : : };
85 : :
86 : : // Session metadata for header
87 : : struct SessionInfo {
88 : : std::string driver_name;
89 : : std::string vehicle_name;
90 : : std::string track_name;
91 : : std::string app_version;
92 : :
93 : : // Key settings snapshot
94 : : float gain;
95 : : float understeer_effect;
96 : : float sop_effect;
97 : : bool slope_enabled;
98 : : float slope_sensitivity;
99 : : float slope_threshold;
100 : : float slope_alpha_threshold;
101 : : float slope_decay_rate;
102 : : bool torque_passthrough; // v0.7.63
103 : : };
104 : :
105 : : class AsyncLogger {
106 : : public:
107 : 16967 : static AsyncLogger& Get() {
108 [ + + + - : 16967 : static AsyncLogger instance;
+ - - - ]
109 : 16967 : return instance;
110 : : }
111 : :
112 : : // Start logging - called from GUI
113 : 17 : void Start(const SessionInfo& info, const std::string& base_path = "") {
114 [ + - ]: 17 : std::lock_guard<std::mutex> lock(m_mutex);
115 [ + + ]: 17 : if (m_running) return;
116 : :
117 [ + - ]: 16 : m_buffer_active.reserve(BUFFER_THRESHOLD * 2);
118 [ + - ]: 16 : m_buffer_writing.reserve(BUFFER_THRESHOLD * 2);
119 : 16 : m_frame_count = 0;
120 : 16 : m_pending_marker = false;
121 : 16 : m_decimation_counter = 0;
122 : :
123 : : // Generate filename
124 : 16 : auto now = std::chrono::system_clock::now();
125 : 16 : auto in_time_t = std::chrono::system_clock::to_time_t(now);
126 : :
127 : : // Use localtime_s for thread safety (MSVC)
128 : : std::tm time_info;
129 : : #ifdef _WIN32
130 : : localtime_s(&time_info, &in_time_t);
131 : : #else
132 : 16 : localtime_r(&in_time_t, &time_info);
133 : : #endif
134 : :
135 [ + - ]: 16 : std::stringstream ss;
136 [ + - ]: 16 : ss << std::put_time(&time_info, "%Y-%m-%d_%H-%M-%S");
137 [ + - ]: 16 : std::string timestamp_str = ss.str();
138 : :
139 [ + - ]: 16 : std::string car = SanitizeFilename(info.vehicle_name);
140 [ + - ]: 16 : std::string track = SanitizeFilename(info.track_name);
141 : :
142 [ + - ]: 16 : std::string path_prefix = base_path;
143 [ + - ]: 16 : if (!path_prefix.empty()) {
144 : : // Ensure directory exists
145 : : try {
146 [ + - + + ]: 17 : std::filesystem::create_directories(path_prefix);
147 : 1 : } catch (...) {
148 : : // Ignore, let file open fail if necessary
149 [ + - ]: 1 : }
150 : :
151 [ + + + - : 16 : if (path_prefix.back() != '/' && path_prefix.back() != '\\') {
+ + ]
152 [ + - ]: 13 : path_prefix += "/";
153 : : }
154 : : }
155 : :
156 [ + - + - : 16 : m_filename = path_prefix + "lmuffb_log_" + timestamp_str + "_" + car + "_" + track + ".csv";
+ - + - +
- + - +
- ]
157 : :
158 : : // Open file
159 [ + - ]: 16 : m_file.open(m_filename);
160 [ + + ]: 16 : if (m_file.is_open()) {
161 [ + - ]: 15 : WriteHeader(info);
162 : 15 : m_running = true;
163 [ + - ]: 15 : m_worker = std::thread(&AsyncLogger::WorkerThread, this);
164 : : }
165 [ + + ]: 17 : }
166 : :
167 : : // Stop logging and flush
168 : 26 : void Stop() noexcept {
169 : : try {
170 : : {
171 [ + - ]: 26 : std::lock_guard<std::mutex> lock(m_mutex);
172 [ + + ]: 26 : if (!m_running) return;
173 : 15 : m_running = false;
174 [ + + ]: 26 : }
175 : 15 : m_cv.notify_one();
176 [ + - ]: 15 : if (m_worker.joinable()) {
177 [ + - ]: 15 : m_worker.join();
178 : : }
179 [ + - ]: 15 : if (m_file.is_open()) {
180 [ + - ]: 15 : m_file.close();
181 : : }
182 : 15 : m_buffer_active.clear();
183 : 15 : m_buffer_writing.clear();
184 : 0 : } catch (...) {
185 : : // Destructor/Stop should not throw
186 : 0 : }
187 : : }
188 : :
189 : : // Log a frame - called from FFB thread (must be fast!)
190 : 2192 : void Log(const LogFrame& frame) {
191 [ + + ]: 3834 : if (!m_running) return;
192 : :
193 : : // Decimation: 400Hz -> 100Hz
194 [ + + + + : 2191 : if (++m_decimation_counter < DECIMATION_FACTOR && !frame.marker && !m_pending_marker) {
+ + + + ]
195 : 1642 : return;
196 : : }
197 : 549 : m_decimation_counter = 0;
198 : :
199 : 549 : LogFrame f = frame;
200 [ + + ]: 549 : if (m_pending_marker) {
201 : 3 : f.marker = true;
202 : 3 : m_pending_marker = false;
203 : : }
204 : :
205 : 549 : bool should_notify = false;
206 : : {
207 [ + - ]: 549 : std::lock_guard<std::mutex> lock(m_mutex);
208 [ - + ]: 549 : if (!m_running) return;
209 [ + - ]: 549 : m_buffer_active.push_back(f);
210 : 549 : should_notify = (m_buffer_active.size() >= BUFFER_THRESHOLD);
211 [ + - ]: 549 : }
212 : :
213 : 549 : m_frame_count++;
214 : :
215 [ + + ]: 549 : if (should_notify) {
216 : 17 : m_cv.notify_one();
217 : : }
218 : : }
219 : :
220 : : // Trigger a user marker
221 : 3 : void SetMarker() { m_pending_marker = true; }
222 : :
223 : : // Status getters
224 : 14733 : bool IsLogging() const { return m_running; }
225 : 8 : size_t GetFrameCount() const { return m_frame_count; }
226 : 3 : std::string GetFilename() const { return m_filename; }
227 : 3 : size_t GetFileSizeBytes() const { return m_file_size_bytes; }
228 : :
229 : : private:
230 : 1 : AsyncLogger() : m_running(false), m_pending_marker(false), m_frame_count(0), m_decimation_counter(0),
231 : 2 : m_file_size_bytes(0), m_last_flush_time(std::chrono::steady_clock::now()) {}
232 : 1 : ~AsyncLogger() { Stop(); }
233 : :
234 : : // No copy
235 : : AsyncLogger(const AsyncLogger&) = delete;
236 : : AsyncLogger& operator=(const AsyncLogger&) = delete;
237 : :
238 : 15 : void WorkerThread() {
239 : : while (true) {
240 [ + - ]: 16 : std::unique_lock<std::mutex> lock(m_mutex);
241 [ + - + + : 41 : m_cv.wait(lock, [this] { return !m_running || !m_buffer_active.empty(); });
+ + ]
242 : :
243 : : // Swap buffers
244 [ + + ]: 16 : if (!m_buffer_active.empty()) {
245 : 7 : std::swap(m_buffer_active, m_buffer_writing);
246 : : }
247 : :
248 : : // If stopped and empty, exit
249 [ + + + + : 16 : if (!m_running && m_buffer_writing.empty()) {
+ + ]
250 : 9 : break;
251 : : }
252 : :
253 [ + - ]: 7 : lock.unlock();
254 : :
255 : : // Write buffer to disk
256 [ + + ]: 142 : for (const auto& frame : m_buffer_writing) {
257 [ + - ]: 135 : WriteFrame(frame);
258 : : }
259 : 7 : m_buffer_writing.clear();
260 : :
261 : : // Periodic flush to minimize data loss on crash
262 : 7 : auto now = std::chrono::steady_clock::now();
263 [ + - + - ]: 7 : auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(now - m_last_flush_time).count();
264 [ + + ]: 7 : if (elapsed >= FLUSH_INTERVAL_SECONDS) {
265 [ + - ]: 1 : m_file.flush();
266 : 1 : m_last_flush_time = now;
267 : : }
268 : :
269 [ + + ]: 7 : if (!m_running) break;
270 [ + + ]: 17 : }
271 : 15 : }
272 : :
273 : 15 : void WriteHeader(const SessionInfo& info) {
274 : 15 : m_file << "# LMUFFB Telemetry Log v1.0\n";
275 : 15 : m_file << "# App Version: " << info.app_version << "\n";
276 : 15 : m_file << "# ========================\n";
277 : 15 : m_file << "# Session Info\n";
278 : 15 : m_file << "# ========================\n";
279 : 15 : m_file << "# Driver: " << info.driver_name << "\n";
280 : 15 : m_file << "# Vehicle: " << info.vehicle_name << "\n";
281 : 15 : m_file << "# Track: " << info.track_name << "\n";
282 : 15 : m_file << "# ========================\n";
283 : 15 : m_file << "# FFB Settings\n";
284 : 15 : m_file << "# ========================\n";
285 : 15 : m_file << "# Gain: " << info.gain << "\n";
286 : 15 : m_file << "# Understeer Effect: " << info.understeer_effect << "\n";
287 : 15 : m_file << "# SoP Effect: " << info.sop_effect << "\n";
288 [ + + ]: 15 : m_file << "# Slope Detection: " << (info.slope_enabled ? "Enabled" : "Disabled") << "\n";
289 : 15 : m_file << "# Slope Sensitivity: " << info.slope_sensitivity << "\n";
290 : 15 : m_file << "# Slope Threshold: " << info.slope_threshold << "\n";
291 : 15 : m_file << "# Slope Alpha Threshold: " << info.slope_alpha_threshold << "\n";
292 : 15 : m_file << "# Slope Decay Rate: " << info.slope_decay_rate << "\n";
293 [ + + ]: 15 : m_file << "# Torque Passthrough: " << (info.torque_passthrough ? "Enabled" : "Disabled") << "\n";
294 : 15 : m_file << "# ========================\n";
295 : :
296 : : // CSV Header
297 : : m_file << "Time,DeltaTime,Speed,LatAccel,LongAccel,YawRate,Steering,Throttle,Brake,"
298 : : << "SlipAngleFL,SlipAngleFR,SlipRatioFL,SlipRatioFR,GripFL,GripFR,LoadFL,LoadFR,"
299 : : << "CalcSlipAngle,CalcGripFront,CalcGripRear,GripDelta,"
300 : : << "dG_dt,dAlpha_dt,SlopeCurrent,SlopeRaw,SlopeNum,SlopeDenom,HoldTimer,InputSlipSmooth,SlopeSmoothed,Confidence,"
301 : : << "SurfaceFL,SurfaceFR,SlopeTorque,SlewLimitedG,"
302 : 15 : << "FFBTotal,FFBBase,FFBShaftTorque,FFBGenTorque,FFBSoP,GripFactor,SpeedGate,LoadPeakRef,Clipping,Marker\n";
303 : 15 : }
304 : :
305 : 135 : void WriteFrame(const LogFrame& frame) {
306 : 135 : m_file << std::fixed << std::setprecision(4)
307 : 135 : << frame.timestamp << "," << frame.delta_time << ","
308 : 135 : << frame.speed << "," << frame.lat_accel << "," << frame.long_accel << "," << frame.yaw_rate << ","
309 : 135 : << frame.steering << "," << frame.throttle << "," << frame.brake << ","
310 : :
311 : 135 : << frame.slip_angle_fl << "," << frame.slip_angle_fr << ","
312 : 135 : << frame.slip_ratio_fl << "," << frame.slip_ratio_fr << ","
313 : 135 : << frame.grip_fl << "," << frame.grip_fr << ","
314 : 135 : << frame.load_fl << "," << frame.load_fr << ","
315 : :
316 : 135 : << frame.calc_slip_angle_front << "," << frame.calc_grip_front << "," << frame.calc_grip_rear << "," << frame.grip_delta << ","
317 : :
318 : 135 : << frame.dG_dt << "," << frame.dAlpha_dt << "," << frame.slope_current << ","
319 : 135 : << frame.slope_raw_unclamped << "," << frame.slope_numerator << "," << frame.slope_denominator << ","
320 : 135 : << frame.hold_timer << "," << frame.input_slip_smoothed << ","
321 : 135 : << frame.slope_smoothed << "," << frame.confidence << ","
322 : 135 : << frame.surface_type_fl << "," << frame.surface_type_fr << ","
323 : 135 : << frame.slope_torque << "," << frame.slew_limited_g << ","
324 : :
325 : 135 : << frame.ffb_total << "," << frame.ffb_base << "," << frame.ffb_shaft_torque << "," << frame.ffb_gen_torque << "," << frame.ffb_sop << ","
326 : 135 : << frame.ffb_grip_factor << "," << frame.speed_gate << "," << frame.load_peak_ref << ","
327 [ - + + + ]: 135 : << (frame.clipping ? 1 : 0) << "," << (frame.marker ? 1 : 0) << "\n";
328 : :
329 : : // Track file size for monitoring
330 : 135 : m_file_size_bytes += 200; // Approximate bytes per line
331 : 135 : }
332 : :
333 : 32 : std::string SanitizeFilename(const std::string& input) {
334 : 32 : std::string out = input;
335 : : // Replace invalid Windows filename characters
336 : 32 : std::replace(out.begin(), out.end(), ' ', '_');
337 : 32 : std::replace(out.begin(), out.end(), '/', '_');
338 : 32 : std::replace(out.begin(), out.end(), '\\', '_');
339 : 32 : std::replace(out.begin(), out.end(), ':', '_');
340 : 32 : std::replace(out.begin(), out.end(), '*', '_');
341 : 32 : std::replace(out.begin(), out.end(), '?', '_');
342 : 32 : std::replace(out.begin(), out.end(), '"', '_');
343 : 32 : std::replace(out.begin(), out.end(), '<', '_');
344 : 32 : std::replace(out.begin(), out.end(), '>', '_');
345 : 32 : std::replace(out.begin(), out.end(), '|', '_');
346 : 32 : return out;
347 : : }
348 : :
349 : : std::ofstream m_file;
350 : : std::string m_filename;
351 : : std::thread m_worker;
352 : :
353 : : std::vector<LogFrame> m_buffer_active;
354 : : std::vector<LogFrame> m_buffer_writing;
355 : :
356 : : std::mutex m_mutex;
357 : : std::condition_variable m_cv;
358 : : std::atomic<bool> m_running;
359 : : std::atomic<bool> m_pending_marker;
360 : : std::atomic<size_t> m_frame_count;
361 : :
362 : : int m_decimation_counter;
363 : : std::atomic<size_t> m_file_size_bytes;
364 : : std::chrono::steady_clock::time_point m_last_flush_time;
365 : :
366 : : static const int DECIMATION_FACTOR = 4; // 400Hz -> 100Hz
367 : : static const size_t BUFFER_THRESHOLD = 200; // ~0.5s of data
368 : : static const int FLUSH_INTERVAL_SECONDS = 5; // Flush every 5 seconds
369 : : };
370 : :
371 : : #endif // ASYNCLOGGER_H
|