blob: afe9330daa5b58bb4640563f179be2a0c775cfb7 [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
67 constexpr std::string_view usbDir{"/u"};
68 std::error_code ec;
69 auto s = fs::status(usbDir, ec);
70 if (!ec && fs::is_directory(s) &&
71 (s.permissions() & fs::perms::others_write) != fs::perms::none) {
72 return std::string{usbDir};
73 }
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 }
James Kuszmaulcf324122023-01-14 14:07:17 -080079#endif
James Kuszmaulb13e13f2023-11-22 20:44:04 -080080 return filesystem::GetOperatingDirectory();
James Kuszmaulcf324122023-01-14 14:07:17 -080081}
82
83static std::string MakeLogFilename(std::string_view filenameOverride) {
84 if (!filenameOverride.empty()) {
85 return std::string{filenameOverride};
86 }
87 static std::random_device dev;
88 static std::mt19937 rng(dev());
89 std::uniform_int_distribution<int> dist(0, 15);
90 const char* v = "0123456789abcdef";
91 std::string filename = "FRC_TBD_";
92 for (int i = 0; i < 16; i++) {
93 filename += v[dist(rng)];
94 }
95 filename += ".wpilog";
96 return filename;
97}
98
99Thread::Thread(std::string_view dir, std::string_view filename, double period)
100 : m_logDir{dir},
101 m_filenameOverride{!filename.empty()},
102 m_log{dir, MakeLogFilename(filename), period},
103 m_messageLog{m_log, "messages"} {
104 StartNTLog();
105}
106
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800107Thread::~Thread() {
108 StopNTLog();
109}
110
James Kuszmaulcf324122023-01-14 14:07:17 -0800111void Thread::Main() {
112 // based on free disk space, scan for "old" FRC_*.wpilog files and remove
113 {
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800114 std::error_code ec;
115 uintmax_t freeSpace;
116 auto freeSpaceInfo = fs::space(m_logDir, ec);
117 if (!ec) {
118 freeSpace = freeSpaceInfo.available;
119 } else {
120 freeSpace = UINTMAX_MAX;
121 }
James Kuszmaulcf324122023-01-14 14:07:17 -0800122 if (freeSpace < kFreeSpaceThreshold) {
123 // Delete oldest FRC_*.wpilog files (ignore FRC_TBD_*.wpilog as we just
124 // created one)
125 std::vector<fs::directory_entry> entries;
James Kuszmaulcf324122023-01-14 14:07:17 -0800126 for (auto&& entry : fs::directory_iterator{m_logDir, ec}) {
127 auto stem = entry.path().stem().string();
128 if (wpi::starts_with(stem, "FRC_") &&
129 entry.path().extension() == ".wpilog" &&
130 !wpi::starts_with(stem, "FRC_TBD_")) {
131 entries.emplace_back(entry);
132 }
133 }
134 std::sort(entries.begin(), entries.end(),
135 [](const auto& a, const auto& b) {
136 return a.last_write_time() < b.last_write_time();
137 });
138
139 int count = entries.size();
140 for (auto&& entry : entries) {
141 --count;
142 if (count < kFileCountThreshold) {
143 break;
144 }
145 auto size = entry.file_size();
146 if (fs::remove(entry.path(), ec)) {
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800147 FRC_ReportError(warn::Warning, "DataLogManager: Deleted {}",
148 entry.path().string());
James Kuszmaulcf324122023-01-14 14:07:17 -0800149 freeSpace += size;
150 if (freeSpace >= kFreeSpaceThreshold) {
151 break;
152 }
153 } else {
154 fmt::print(stderr, "DataLogManager: could not delete {}\n",
155 entry.path().string());
156 }
157 }
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800158 } else if (freeSpace < 2 * kFreeSpaceThreshold) {
159 FRC_ReportError(
160 warn::Warning,
161 "DataLogManager: Log storage device has {} MB of free space "
162 "remaining! Logs will get deleted below {} MB of free space. "
163 "Consider deleting logs off the storage device.",
164 freeSpace / 1000000, kFreeSpaceThreshold / 1000000);
James Kuszmaulcf324122023-01-14 14:07:17 -0800165 }
166 }
167
168 int timeoutCount = 0;
169 bool paused = false;
170 int dsAttachCount = 0;
171 int fmsAttachCount = 0;
172 bool dsRenamed = m_filenameOverride;
173 bool fmsRenamed = m_filenameOverride;
174 int sysTimeCount = 0;
175 wpi::log::IntegerLogEntry sysTimeEntry{
176 m_log, "systemTime",
177 "{\"source\":\"DataLogManager\",\"format\":\"time_t_us\"}"};
178
179 wpi::Event newDataEvent;
180 DriverStation::ProvideRefreshedDataEventHandle(newDataEvent.GetHandle());
181
182 for (;;) {
183 bool timedOut = false;
184 bool newData =
185 wpi::WaitForObject(newDataEvent.GetHandle(), 0.25, &timedOut);
186 if (!m_active) {
187 break;
188 }
189 if (!newData) {
190 ++timeoutCount;
191 // pause logging after being disconnected for 10 seconds
192 if (timeoutCount > 40 && !paused) {
193 timeoutCount = 0;
194 paused = true;
195 m_log.Pause();
196 }
197 continue;
198 }
199 // when we connect to the DS, resume logging
200 timeoutCount = 0;
201 if (paused) {
202 paused = false;
203 m_log.Resume();
204 }
205
206 if (!dsRenamed) {
207 // track DS attach
208 if (DriverStation::IsDSAttached()) {
209 ++dsAttachCount;
210 } else {
211 dsAttachCount = 0;
212 }
213 if (dsAttachCount > 50) { // 1 second
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800214 if (RobotController::IsSystemTimeValid()) {
215 std::time_t now = std::time(nullptr);
216 auto tm = std::gmtime(&now);
James Kuszmaulcf324122023-01-14 14:07:17 -0800217 m_log.SetFilename(fmt::format("FRC_{:%Y%m%d_%H%M%S}.wpilog", *tm));
218 dsRenamed = true;
219 } else {
220 dsAttachCount = 0; // wait a bit and try again
221 }
222 }
223 }
224
225 if (!fmsRenamed) {
226 // track FMS attach
227 if (DriverStation::IsFMSAttached()) {
228 ++fmsAttachCount;
229 } else {
230 fmsAttachCount = 0;
231 }
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800232 if (fmsAttachCount > 250) { // 5 seconds
James Kuszmaulcf324122023-01-14 14:07:17 -0800233 // match info comes through TCP, so we need to double-check we've
234 // actually received it
235 auto matchType = DriverStation::GetMatchType();
236 if (matchType != DriverStation::kNone) {
237 // rename per match info
238 char matchTypeChar;
239 switch (matchType) {
240 case DriverStation::kPractice:
241 matchTypeChar = 'P';
242 break;
243 case DriverStation::kQualification:
244 matchTypeChar = 'Q';
245 break;
246 case DriverStation::kElimination:
247 matchTypeChar = 'E';
248 break;
249 default:
250 matchTypeChar = '_';
251 break;
252 }
253 std::time_t now = std::time(nullptr);
254 m_log.SetFilename(
255 fmt::format("FRC_{:%Y%m%d_%H%M%S}_{}_{}{}.wpilog",
256 *std::gmtime(&now), DriverStation::GetEventName(),
257 matchTypeChar, DriverStation::GetMatchNumber()));
258 fmsRenamed = true;
259 dsRenamed = true; // don't override FMS rename
260 }
261 }
262 }
263
264 // Write system time every ~5 seconds
265 ++sysTimeCount;
266 if (sysTimeCount >= 250) {
267 sysTimeCount = 0;
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800268 if (RobotController::IsSystemTimeValid()) {
269 sysTimeEntry.Append(wpi::GetSystemTime(), wpi::Now());
270 }
James Kuszmaulcf324122023-01-14 14:07:17 -0800271 }
272 }
273 DriverStation::RemoveRefreshedDataEventHandle(newDataEvent.GetHandle());
274}
275
276void Thread::StartNTLog() {
277 if (!m_ntLoggerEnabled) {
278 m_ntLoggerEnabled = true;
279 auto inst = nt::NetworkTableInstance::GetDefault();
280 m_ntEntryLogger = inst.StartEntryDataLog(m_log, "", "NT:");
281 m_ntConnLogger = inst.StartConnectionDataLog(m_log, "NTConnection");
282 }
283}
284
285void Thread::StopNTLog() {
286 if (m_ntLoggerEnabled) {
287 m_ntLoggerEnabled = false;
288 nt::NetworkTableInstance::StopEntryDataLog(m_ntEntryLogger);
289 nt::NetworkTableInstance::StopConnectionDataLog(m_ntConnLogger);
290 }
291}
292
293Instance::Instance(std::string_view dir, std::string_view filename,
294 double period) {
295 // Delete all previously existing FRC_TBD_*.wpilog files. These only exist
296 // when the robot never connects to the DS, so they are very unlikely to
297 // have useful data and just clutter the filesystem.
298 auto logDir = MakeLogDir(dir);
299 std::error_code ec;
300 for (auto&& entry : fs::directory_iterator{logDir, ec}) {
301 if (wpi::starts_with(entry.path().stem().string(), "FRC_TBD_") &&
302 entry.path().extension() == ".wpilog") {
303 if (!fs::remove(entry, ec)) {
304 fmt::print(stderr, "DataLogManager: could not delete {}\n",
305 entry.path().string());
306 }
307 }
308 }
309
310 owner.Start(logDir, filename, period);
311}
312
313static Instance& GetInstance(std::string_view dir = "",
314 std::string_view filename = "",
315 double period = 0.25) {
316 static Instance instance(dir, filename, period);
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800317 if (!instance.owner) {
318 instance.owner.Start(MakeLogDir(dir), filename, period);
319 }
James Kuszmaulcf324122023-01-14 14:07:17 -0800320 return instance;
321}
322
323void DataLogManager::Start(std::string_view dir, std::string_view filename,
324 double period) {
325 GetInstance(dir, filename, period);
326}
327
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800328void DataLogManager::Stop() {
329 auto& inst = GetInstance();
330 inst.owner.GetThread()->m_log.Stop();
331 inst.owner.Stop();
332}
333
James Kuszmaulcf324122023-01-14 14:07:17 -0800334void DataLogManager::Log(std::string_view message) {
335 GetInstance().owner.GetThread()->m_messageLog.Append(message);
336 fmt::print("{}\n", message);
337}
338
339wpi::log::DataLog& DataLogManager::GetLog() {
340 return GetInstance().owner.GetThread()->m_log;
341}
342
343std::string DataLogManager::GetLogDir() {
344 return GetInstance().owner.GetThread()->m_logDir;
345}
346
347void DataLogManager::LogNetworkTables(bool enabled) {
348 if (auto thr = GetInstance().owner.GetThread()) {
349 if (enabled) {
350 thr->StartNTLog();
351 } else if (!enabled) {
352 thr->StopNTLog();
353 }
354 }
355}