James Kuszmaul | cf32412 | 2023-01-14 14:07:17 -0800 | [diff] [blame] | 1 | // 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 Kuszmaul | b13e13f | 2023-11-22 20:44:04 -0800 | [diff] [blame^] | 7 | #include <frc/Errors.h> |
| 8 | |
James Kuszmaul | cf32412 | 2023-01-14 14:07:17 -0800 | [diff] [blame] | 9 | #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 Kuszmaul | b13e13f | 2023-11-22 20:44:04 -0800 | [diff] [blame^] | 24 | #include "frc/RobotBase.h" |
| 25 | #include "frc/RobotController.h" |
James Kuszmaul | cf32412 | 2023-01-14 14:07:17 -0800 | [diff] [blame] | 26 | |
| 27 | using namespace frc; |
| 28 | |
| 29 | namespace { |
| 30 | |
| 31 | struct Thread final : public wpi::SafeThread { |
| 32 | Thread(std::string_view dir, std::string_view filename, double period); |
James Kuszmaul | b13e13f | 2023-11-22 20:44:04 -0800 | [diff] [blame^] | 33 | ~Thread() override; |
James Kuszmaul | cf32412 | 2023-01-14 14:07:17 -0800 | [diff] [blame] | 34 | |
| 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 | |
| 49 | struct 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. |
| 58 | static constexpr uintmax_t kFreeSpaceThreshold = 50000000; |
| 59 | static constexpr int kFileCountThreshold = 10; |
| 60 | |
| 61 | static 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 Kuszmaul | b13e13f | 2023-11-22 20:44:04 -0800 | [diff] [blame^] | 74 | 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 Kuszmaul | cf32412 | 2023-01-14 14:07:17 -0800 | [diff] [blame] | 79 | #endif |
James Kuszmaul | b13e13f | 2023-11-22 20:44:04 -0800 | [diff] [blame^] | 80 | return filesystem::GetOperatingDirectory(); |
James Kuszmaul | cf32412 | 2023-01-14 14:07:17 -0800 | [diff] [blame] | 81 | } |
| 82 | |
| 83 | static 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 | |
| 99 | Thread::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 Kuszmaul | b13e13f | 2023-11-22 20:44:04 -0800 | [diff] [blame^] | 107 | Thread::~Thread() { |
| 108 | StopNTLog(); |
| 109 | } |
| 110 | |
James Kuszmaul | cf32412 | 2023-01-14 14:07:17 -0800 | [diff] [blame] | 111 | void Thread::Main() { |
| 112 | // based on free disk space, scan for "old" FRC_*.wpilog files and remove |
| 113 | { |
James Kuszmaul | b13e13f | 2023-11-22 20:44:04 -0800 | [diff] [blame^] | 114 | 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 Kuszmaul | cf32412 | 2023-01-14 14:07:17 -0800 | [diff] [blame] | 122 | 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 Kuszmaul | cf32412 | 2023-01-14 14:07:17 -0800 | [diff] [blame] | 126 | 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 Kuszmaul | b13e13f | 2023-11-22 20:44:04 -0800 | [diff] [blame^] | 147 | FRC_ReportError(warn::Warning, "DataLogManager: Deleted {}", |
| 148 | entry.path().string()); |
James Kuszmaul | cf32412 | 2023-01-14 14:07:17 -0800 | [diff] [blame] | 149 | 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 Kuszmaul | b13e13f | 2023-11-22 20:44:04 -0800 | [diff] [blame^] | 158 | } 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 Kuszmaul | cf32412 | 2023-01-14 14:07:17 -0800 | [diff] [blame] | 165 | } |
| 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 Kuszmaul | b13e13f | 2023-11-22 20:44:04 -0800 | [diff] [blame^] | 214 | if (RobotController::IsSystemTimeValid()) { |
| 215 | std::time_t now = std::time(nullptr); |
| 216 | auto tm = std::gmtime(&now); |
James Kuszmaul | cf32412 | 2023-01-14 14:07:17 -0800 | [diff] [blame] | 217 | 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 Kuszmaul | b13e13f | 2023-11-22 20:44:04 -0800 | [diff] [blame^] | 232 | if (fmsAttachCount > 250) { // 5 seconds |
James Kuszmaul | cf32412 | 2023-01-14 14:07:17 -0800 | [diff] [blame] | 233 | // 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 Kuszmaul | b13e13f | 2023-11-22 20:44:04 -0800 | [diff] [blame^] | 268 | if (RobotController::IsSystemTimeValid()) { |
| 269 | sysTimeEntry.Append(wpi::GetSystemTime(), wpi::Now()); |
| 270 | } |
James Kuszmaul | cf32412 | 2023-01-14 14:07:17 -0800 | [diff] [blame] | 271 | } |
| 272 | } |
| 273 | DriverStation::RemoveRefreshedDataEventHandle(newDataEvent.GetHandle()); |
| 274 | } |
| 275 | |
| 276 | void 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 | |
| 285 | void 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 | |
| 293 | Instance::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 | |
| 313 | static Instance& GetInstance(std::string_view dir = "", |
| 314 | std::string_view filename = "", |
| 315 | double period = 0.25) { |
| 316 | static Instance instance(dir, filename, period); |
James Kuszmaul | b13e13f | 2023-11-22 20:44:04 -0800 | [diff] [blame^] | 317 | if (!instance.owner) { |
| 318 | instance.owner.Start(MakeLogDir(dir), filename, period); |
| 319 | } |
James Kuszmaul | cf32412 | 2023-01-14 14:07:17 -0800 | [diff] [blame] | 320 | return instance; |
| 321 | } |
| 322 | |
| 323 | void DataLogManager::Start(std::string_view dir, std::string_view filename, |
| 324 | double period) { |
| 325 | GetInstance(dir, filename, period); |
| 326 | } |
| 327 | |
James Kuszmaul | b13e13f | 2023-11-22 20:44:04 -0800 | [diff] [blame^] | 328 | void DataLogManager::Stop() { |
| 329 | auto& inst = GetInstance(); |
| 330 | inst.owner.GetThread()->m_log.Stop(); |
| 331 | inst.owner.Stop(); |
| 332 | } |
| 333 | |
James Kuszmaul | cf32412 | 2023-01-14 14:07:17 -0800 | [diff] [blame] | 334 | void DataLogManager::Log(std::string_view message) { |
| 335 | GetInstance().owner.GetThread()->m_messageLog.Append(message); |
| 336 | fmt::print("{}\n", message); |
| 337 | } |
| 338 | |
| 339 | wpi::log::DataLog& DataLogManager::GetLog() { |
| 340 | return GetInstance().owner.GetThread()->m_log; |
| 341 | } |
| 342 | |
| 343 | std::string DataLogManager::GetLogDir() { |
| 344 | return GetInstance().owner.GetThread()->m_logDir; |
| 345 | } |
| 346 | |
| 347 | void 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 | } |