blob: 09ac07762c59edf033982babe872ade76637e4c8 [file] [log] [blame]
James Kuszmaulcf324122023-01-14 14:07:17 -08001// Copyright (c) FIRST and other WPILib contributors.
2// Open Source Software; you can modify and/or share it under the terms of
3// the WPILib BSD license file in the root directory of this project.
4
5#include "frc/DataLogManager.h"
6
James Kuszmaulb13e13f2023-11-22 20:44:04 -08007#include <frc/Errors.h>
8
James Kuszmaulcf324122023-01-14 14:07:17 -08009#include <algorithm>
10#include <ctime>
11#include <random>
12
13#include <fmt/chrono.h>
14#include <fmt/format.h>
15#include <networktables/NetworkTableInstance.h>
16#include <wpi/DataLog.h>
17#include <wpi/SafeThread.h>
18#include <wpi/StringExtras.h>
19#include <wpi/fs.h>
20#include <wpi/timestamp.h>
21
22#include "frc/DriverStation.h"
23#include "frc/Filesystem.h"
James Kuszmaulb13e13f2023-11-22 20:44:04 -080024#include "frc/RobotBase.h"
25#include "frc/RobotController.h"
James Kuszmaulcf324122023-01-14 14:07:17 -080026
27using namespace frc;
28
29namespace {
30
31struct Thread final : public wpi::SafeThread {
32 Thread(std::string_view dir, std::string_view filename, double period);
James Kuszmaulb13e13f2023-11-22 20:44:04 -080033 ~Thread() override;
James Kuszmaulcf324122023-01-14 14:07:17 -080034
35 void Main() final;
36
37 void StartNTLog();
38 void StopNTLog();
39
40 std::string m_logDir;
41 bool m_filenameOverride;
42 wpi::log::DataLog m_log;
43 bool m_ntLoggerEnabled = false;
44 NT_DataLogger m_ntEntryLogger = 0;
45 NT_ConnectionDataLogger m_ntConnLogger = 0;
46 wpi::log::StringLogEntry m_messageLog;
47};
48
49struct Instance {
50 Instance(std::string_view dir, std::string_view filename, double period);
51 wpi::SafeThreadOwner<Thread> owner;
52};
53
54} // namespace
55
56// if less than this much free space, delete log files until there is this much
57// free space OR there are this many files remaining.
58static constexpr uintmax_t kFreeSpaceThreshold = 50000000;
59static constexpr int kFileCountThreshold = 10;
60
61static std::string MakeLogDir(std::string_view dir) {
62 if (!dir.empty()) {
63 return std::string{dir};
64 }
65#ifdef __FRC_ROBORIO__
66 // prefer a mounted USB drive if one is accessible
James Kuszmaulcf324122023-01-14 14:07:17 -080067 std::error_code ec;
Maxwell Henderson80bec322024-01-09 15:48:44 -080068 auto s = fs::status("/u", ec);
James Kuszmaulcf324122023-01-14 14:07:17 -080069 if (!ec && fs::is_directory(s) &&
70 (s.permissions() & fs::perms::others_write) != fs::perms::none) {
Maxwell Henderson80bec322024-01-09 15:48:44 -080071 fs::create_directory("/u/logs", ec);
72 return "/u/logs";
James Kuszmaulcf324122023-01-14 14:07:17 -080073 }
James Kuszmaulb13e13f2023-11-22 20:44:04 -080074 if (RobotBase::GetRuntimeType() == kRoboRIO) {
75 FRC_ReportError(warn::Warning,
76 "DataLogManager: Logging to RoboRIO 1 internal storage is "
77 "not recommended! Plug in a FAT32 formatted flash drive!");
78 }
Maxwell Henderson80bec322024-01-09 15:48:44 -080079 fs::create_directory("/home/lvuser/logs", ec);
80 return "/home/lvuser/logs";
81#else
82 std::string logDir = filesystem::GetOperatingDirectory() + "/logs";
83 std::error_code ec;
84 fs::create_directory(logDir, ec);
85 return logDir;
James Kuszmaulcf324122023-01-14 14:07:17 -080086#endif
James Kuszmaulcf324122023-01-14 14:07:17 -080087}
88
89static std::string MakeLogFilename(std::string_view filenameOverride) {
90 if (!filenameOverride.empty()) {
91 return std::string{filenameOverride};
92 }
93 static std::random_device dev;
94 static std::mt19937 rng(dev());
95 std::uniform_int_distribution<int> dist(0, 15);
96 const char* v = "0123456789abcdef";
97 std::string filename = "FRC_TBD_";
98 for (int i = 0; i < 16; i++) {
99 filename += v[dist(rng)];
100 }
101 filename += ".wpilog";
102 return filename;
103}
104
105Thread::Thread(std::string_view dir, std::string_view filename, double period)
106 : m_logDir{dir},
107 m_filenameOverride{!filename.empty()},
108 m_log{dir, MakeLogFilename(filename), period},
109 m_messageLog{m_log, "messages"} {
110 StartNTLog();
111}
112
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800113Thread::~Thread() {
114 StopNTLog();
115}
116
James Kuszmaulcf324122023-01-14 14:07:17 -0800117void Thread::Main() {
118 // based on free disk space, scan for "old" FRC_*.wpilog files and remove
119 {
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800120 std::error_code ec;
121 uintmax_t freeSpace;
122 auto freeSpaceInfo = fs::space(m_logDir, ec);
123 if (!ec) {
124 freeSpace = freeSpaceInfo.available;
125 } else {
126 freeSpace = UINTMAX_MAX;
127 }
James Kuszmaulcf324122023-01-14 14:07:17 -0800128 if (freeSpace < kFreeSpaceThreshold) {
129 // Delete oldest FRC_*.wpilog files (ignore FRC_TBD_*.wpilog as we just
130 // created one)
131 std::vector<fs::directory_entry> entries;
James Kuszmaulcf324122023-01-14 14:07:17 -0800132 for (auto&& entry : fs::directory_iterator{m_logDir, ec}) {
133 auto stem = entry.path().stem().string();
134 if (wpi::starts_with(stem, "FRC_") &&
135 entry.path().extension() == ".wpilog" &&
136 !wpi::starts_with(stem, "FRC_TBD_")) {
137 entries.emplace_back(entry);
138 }
139 }
140 std::sort(entries.begin(), entries.end(),
141 [](const auto& a, const auto& b) {
142 return a.last_write_time() < b.last_write_time();
143 });
144
145 int count = entries.size();
146 for (auto&& entry : entries) {
147 --count;
148 if (count < kFileCountThreshold) {
149 break;
150 }
151 auto size = entry.file_size();
152 if (fs::remove(entry.path(), ec)) {
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800153 FRC_ReportError(warn::Warning, "DataLogManager: Deleted {}",
154 entry.path().string());
James Kuszmaulcf324122023-01-14 14:07:17 -0800155 freeSpace += size;
156 if (freeSpace >= kFreeSpaceThreshold) {
157 break;
158 }
159 } else {
160 fmt::print(stderr, "DataLogManager: could not delete {}\n",
161 entry.path().string());
162 }
163 }
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800164 } else if (freeSpace < 2 * kFreeSpaceThreshold) {
165 FRC_ReportError(
166 warn::Warning,
167 "DataLogManager: Log storage device has {} MB of free space "
168 "remaining! Logs will get deleted below {} MB of free space. "
169 "Consider deleting logs off the storage device.",
170 freeSpace / 1000000, kFreeSpaceThreshold / 1000000);
James Kuszmaulcf324122023-01-14 14:07:17 -0800171 }
172 }
173
174 int timeoutCount = 0;
175 bool paused = false;
176 int dsAttachCount = 0;
177 int fmsAttachCount = 0;
178 bool dsRenamed = m_filenameOverride;
179 bool fmsRenamed = m_filenameOverride;
180 int sysTimeCount = 0;
181 wpi::log::IntegerLogEntry sysTimeEntry{
182 m_log, "systemTime",
183 "{\"source\":\"DataLogManager\",\"format\":\"time_t_us\"}"};
184
185 wpi::Event newDataEvent;
186 DriverStation::ProvideRefreshedDataEventHandle(newDataEvent.GetHandle());
187
188 for (;;) {
189 bool timedOut = false;
190 bool newData =
191 wpi::WaitForObject(newDataEvent.GetHandle(), 0.25, &timedOut);
192 if (!m_active) {
193 break;
194 }
195 if (!newData) {
196 ++timeoutCount;
197 // pause logging after being disconnected for 10 seconds
198 if (timeoutCount > 40 && !paused) {
199 timeoutCount = 0;
200 paused = true;
201 m_log.Pause();
202 }
203 continue;
204 }
205 // when we connect to the DS, resume logging
206 timeoutCount = 0;
207 if (paused) {
208 paused = false;
209 m_log.Resume();
210 }
211
212 if (!dsRenamed) {
213 // track DS attach
214 if (DriverStation::IsDSAttached()) {
215 ++dsAttachCount;
216 } else {
217 dsAttachCount = 0;
218 }
219 if (dsAttachCount > 50) { // 1 second
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800220 if (RobotController::IsSystemTimeValid()) {
221 std::time_t now = std::time(nullptr);
222 auto tm = std::gmtime(&now);
James Kuszmaulcf324122023-01-14 14:07:17 -0800223 m_log.SetFilename(fmt::format("FRC_{:%Y%m%d_%H%M%S}.wpilog", *tm));
224 dsRenamed = true;
225 } else {
226 dsAttachCount = 0; // wait a bit and try again
227 }
228 }
229 }
230
231 if (!fmsRenamed) {
232 // track FMS attach
233 if (DriverStation::IsFMSAttached()) {
234 ++fmsAttachCount;
235 } else {
236 fmsAttachCount = 0;
237 }
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800238 if (fmsAttachCount > 250) { // 5 seconds
James Kuszmaulcf324122023-01-14 14:07:17 -0800239 // match info comes through TCP, so we need to double-check we've
240 // actually received it
241 auto matchType = DriverStation::GetMatchType();
242 if (matchType != DriverStation::kNone) {
243 // rename per match info
244 char matchTypeChar;
245 switch (matchType) {
246 case DriverStation::kPractice:
247 matchTypeChar = 'P';
248 break;
249 case DriverStation::kQualification:
250 matchTypeChar = 'Q';
251 break;
252 case DriverStation::kElimination:
253 matchTypeChar = 'E';
254 break;
255 default:
256 matchTypeChar = '_';
257 break;
258 }
259 std::time_t now = std::time(nullptr);
260 m_log.SetFilename(
261 fmt::format("FRC_{:%Y%m%d_%H%M%S}_{}_{}{}.wpilog",
262 *std::gmtime(&now), DriverStation::GetEventName(),
263 matchTypeChar, DriverStation::GetMatchNumber()));
264 fmsRenamed = true;
265 dsRenamed = true; // don't override FMS rename
266 }
267 }
268 }
269
270 // Write system time every ~5 seconds
271 ++sysTimeCount;
272 if (sysTimeCount >= 250) {
273 sysTimeCount = 0;
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800274 if (RobotController::IsSystemTimeValid()) {
275 sysTimeEntry.Append(wpi::GetSystemTime(), wpi::Now());
276 }
James Kuszmaulcf324122023-01-14 14:07:17 -0800277 }
278 }
279 DriverStation::RemoveRefreshedDataEventHandle(newDataEvent.GetHandle());
280}
281
282void Thread::StartNTLog() {
283 if (!m_ntLoggerEnabled) {
284 m_ntLoggerEnabled = true;
285 auto inst = nt::NetworkTableInstance::GetDefault();
286 m_ntEntryLogger = inst.StartEntryDataLog(m_log, "", "NT:");
287 m_ntConnLogger = inst.StartConnectionDataLog(m_log, "NTConnection");
288 }
289}
290
291void Thread::StopNTLog() {
292 if (m_ntLoggerEnabled) {
293 m_ntLoggerEnabled = false;
294 nt::NetworkTableInstance::StopEntryDataLog(m_ntEntryLogger);
295 nt::NetworkTableInstance::StopConnectionDataLog(m_ntConnLogger);
296 }
297}
298
299Instance::Instance(std::string_view dir, std::string_view filename,
300 double period) {
301 // Delete all previously existing FRC_TBD_*.wpilog files. These only exist
302 // when the robot never connects to the DS, so they are very unlikely to
303 // have useful data and just clutter the filesystem.
304 auto logDir = MakeLogDir(dir);
305 std::error_code ec;
306 for (auto&& entry : fs::directory_iterator{logDir, ec}) {
307 if (wpi::starts_with(entry.path().stem().string(), "FRC_TBD_") &&
308 entry.path().extension() == ".wpilog") {
309 if (!fs::remove(entry, ec)) {
310 fmt::print(stderr, "DataLogManager: could not delete {}\n",
311 entry.path().string());
312 }
313 }
314 }
315
316 owner.Start(logDir, filename, period);
317}
318
319static Instance& GetInstance(std::string_view dir = "",
320 std::string_view filename = "",
321 double period = 0.25) {
322 static Instance instance(dir, filename, period);
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800323 if (!instance.owner) {
324 instance.owner.Start(MakeLogDir(dir), filename, period);
325 }
James Kuszmaulcf324122023-01-14 14:07:17 -0800326 return instance;
327}
328
329void DataLogManager::Start(std::string_view dir, std::string_view filename,
330 double period) {
331 GetInstance(dir, filename, period);
332}
333
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800334void DataLogManager::Stop() {
335 auto& inst = GetInstance();
336 inst.owner.GetThread()->m_log.Stop();
Maxwell Henderson80bec322024-01-09 15:48:44 -0800337 inst.owner.Join();
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800338}
339
James Kuszmaulcf324122023-01-14 14:07:17 -0800340void DataLogManager::Log(std::string_view message) {
341 GetInstance().owner.GetThread()->m_messageLog.Append(message);
342 fmt::print("{}\n", message);
343}
344
345wpi::log::DataLog& DataLogManager::GetLog() {
346 return GetInstance().owner.GetThread()->m_log;
347}
348
349std::string DataLogManager::GetLogDir() {
350 return GetInstance().owner.GetThread()->m_logDir;
351}
352
353void DataLogManager::LogNetworkTables(bool enabled) {
354 if (auto thr = GetInstance().owner.GetThread()) {
355 if (enabled) {
356 thr->StartNTLog();
357 } else if (!enabled) {
358 thr->StopNTLog();
359 }
360 }
361}