2021-12-12 16:54:10 -08:00
|
|
|
// Copyright (c) FIRST and other WPILib contributors.
|
|
|
|
|
// Open Source Software; you can modify and/or share it under the terms of
|
|
|
|
|
// the WPILib BSD license file in the root directory of this project.
|
|
|
|
|
|
|
|
|
|
#include "frc/DataLogManager.h"
|
|
|
|
|
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
#include <ctime>
|
|
|
|
|
#include <random>
|
|
|
|
|
|
|
|
|
|
#include <fmt/chrono.h>
|
|
|
|
|
#include <fmt/format.h>
|
|
|
|
|
#include <networktables/NetworkTableInstance.h>
|
|
|
|
|
#include <wpi/DataLog.h>
|
|
|
|
|
#include <wpi/SafeThread.h>
|
|
|
|
|
#include <wpi/StringExtras.h>
|
|
|
|
|
#include <wpi/fs.h>
|
|
|
|
|
#include <wpi/timestamp.h>
|
|
|
|
|
|
|
|
|
|
#include "frc/DriverStation.h"
|
|
|
|
|
#include "frc/Filesystem.h"
|
|
|
|
|
|
|
|
|
|
using namespace frc;
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
struct Thread final : public wpi::SafeThread {
|
|
|
|
|
Thread(std::string_view dir, std::string_view filename, double period);
|
2023-02-23 20:13:20 -08:00
|
|
|
~Thread() override;
|
2021-12-12 16:54:10 -08:00
|
|
|
|
|
|
|
|
void Main() final;
|
|
|
|
|
|
|
|
|
|
void StartNTLog();
|
|
|
|
|
void StopNTLog();
|
|
|
|
|
|
|
|
|
|
std::string m_logDir;
|
|
|
|
|
bool m_filenameOverride;
|
|
|
|
|
wpi::log::DataLog m_log;
|
|
|
|
|
bool m_ntLoggerEnabled = false;
|
|
|
|
|
NT_DataLogger m_ntEntryLogger = 0;
|
|
|
|
|
NT_ConnectionDataLogger m_ntConnLogger = 0;
|
|
|
|
|
wpi::log::StringLogEntry m_messageLog;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
struct Instance {
|
|
|
|
|
Instance(std::string_view dir, std::string_view filename, double period);
|
|
|
|
|
wpi::SafeThreadOwner<Thread> owner;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
// if less than this much free space, delete log files until there is this much
|
|
|
|
|
// free space OR there are this many files remaining.
|
|
|
|
|
static constexpr uintmax_t kFreeSpaceThreshold = 50000000;
|
|
|
|
|
static constexpr int kFileCountThreshold = 10;
|
|
|
|
|
|
|
|
|
|
static std::string MakeLogDir(std::string_view dir) {
|
|
|
|
|
if (!dir.empty()) {
|
|
|
|
|
return std::string{dir};
|
|
|
|
|
}
|
|
|
|
|
#ifdef __FRC_ROBORIO__
|
|
|
|
|
// prefer a mounted USB drive if one is accessible
|
|
|
|
|
constexpr std::string_view usbDir{"/u"};
|
|
|
|
|
std::error_code ec;
|
|
|
|
|
auto s = fs::status(usbDir, ec);
|
|
|
|
|
if (!ec && fs::is_directory(s) &&
|
|
|
|
|
(s.permissions() & fs::perms::others_write) != fs::perms::none) {
|
|
|
|
|
return std::string{usbDir};
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
return frc::filesystem::GetOperatingDirectory();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static std::string MakeLogFilename(std::string_view filenameOverride) {
|
|
|
|
|
if (!filenameOverride.empty()) {
|
|
|
|
|
return std::string{filenameOverride};
|
|
|
|
|
}
|
|
|
|
|
static std::random_device dev;
|
|
|
|
|
static std::mt19937 rng(dev());
|
|
|
|
|
std::uniform_int_distribution<int> dist(0, 15);
|
|
|
|
|
const char* v = "0123456789abcdef";
|
|
|
|
|
std::string filename = "FRC_TBD_";
|
|
|
|
|
for (int i = 0; i < 16; i++) {
|
|
|
|
|
filename += v[dist(rng)];
|
|
|
|
|
}
|
|
|
|
|
filename += ".wpilog";
|
|
|
|
|
return filename;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Thread::Thread(std::string_view dir, std::string_view filename, double period)
|
|
|
|
|
: m_logDir{dir},
|
|
|
|
|
m_filenameOverride{!filename.empty()},
|
|
|
|
|
m_log{dir, MakeLogFilename(filename), period},
|
|
|
|
|
m_messageLog{m_log, "messages"} {
|
|
|
|
|
StartNTLog();
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-23 20:13:20 -08:00
|
|
|
Thread::~Thread() {
|
|
|
|
|
StopNTLog();
|
|
|
|
|
}
|
|
|
|
|
|
2021-12-12 16:54:10 -08:00
|
|
|
void Thread::Main() {
|
|
|
|
|
// based on free disk space, scan for "old" FRC_*.wpilog files and remove
|
|
|
|
|
{
|
|
|
|
|
uintmax_t freeSpace = fs::space(m_logDir).free;
|
|
|
|
|
if (freeSpace < kFreeSpaceThreshold) {
|
|
|
|
|
// Delete oldest FRC_*.wpilog files (ignore FRC_TBD_*.wpilog as we just
|
|
|
|
|
// created one)
|
|
|
|
|
std::vector<fs::directory_entry> entries;
|
|
|
|
|
std::error_code ec;
|
|
|
|
|
for (auto&& entry : fs::directory_iterator{m_logDir, ec}) {
|
|
|
|
|
auto stem = entry.path().stem().string();
|
|
|
|
|
if (wpi::starts_with(stem, "FRC_") &&
|
|
|
|
|
entry.path().extension() == ".wpilog" &&
|
|
|
|
|
!wpi::starts_with(stem, "FRC_TBD_")) {
|
|
|
|
|
entries.emplace_back(entry);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
std::sort(entries.begin(), entries.end(),
|
|
|
|
|
[](const auto& a, const auto& b) {
|
|
|
|
|
return a.last_write_time() < b.last_write_time();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
int count = entries.size();
|
|
|
|
|
for (auto&& entry : entries) {
|
|
|
|
|
--count;
|
|
|
|
|
if (count < kFileCountThreshold) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
auto size = entry.file_size();
|
|
|
|
|
if (fs::remove(entry.path(), ec)) {
|
|
|
|
|
freeSpace += size;
|
|
|
|
|
if (freeSpace >= kFreeSpaceThreshold) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
fmt::print(stderr, "DataLogManager: could not delete {}\n",
|
|
|
|
|
entry.path().string());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int timeoutCount = 0;
|
|
|
|
|
bool paused = false;
|
|
|
|
|
int dsAttachCount = 0;
|
|
|
|
|
int fmsAttachCount = 0;
|
|
|
|
|
bool dsRenamed = m_filenameOverride;
|
|
|
|
|
bool fmsRenamed = m_filenameOverride;
|
|
|
|
|
int sysTimeCount = 0;
|
|
|
|
|
wpi::log::IntegerLogEntry sysTimeEntry{
|
|
|
|
|
m_log, "systemTime",
|
|
|
|
|
"{\"source\":\"DataLogManager\",\"format\":\"time_t_us\"}"};
|
|
|
|
|
|
[hal, wpilib] New DS thread model and implementation (#3787)
The current DS thread model has some pretty major issues. It makes it difficult to know if all data is from the same remote packet, and if the data changes while the robot loop is running. Additionally, the DS thread is used for a few other things (MotorSafety and State Tracking for EducationalRobot). This also makes sim difficult, as user code has to wait for the thread to know it has new data.
This change completely rethinks how threading works in the driver station model.
First, the DS HAL system receives a new data callback, either from Netcomm or DriverStationSim. Inside the context of this callback, all the low latency data is read and put into a cache. Doing some investigation on the robot side, this is perfectly safe to do, and also ensures a ds packet will not be parsed before we finish reading the current packet data.
After all data is read, the cache is swapped with a 2nd buffer. This buffer just stores the data, none of the HAL DS calls read from this buffer. An event is then fired, stating there is new data ready to go.
Robot code calls HAL_UpdateDSData(). This swaps the 2nd buffer with a 3rd buffer, which always contains the current data. This data will not be updated until HAL_UpdateDSData is called again. Which solves the state problem.
The high level driver station classes have. an updateData() call, which calls HAL_UpdateDSData, and then update button state variables, then data log and update the NT FMS data table (Java also caches across the JNI boundary here, but that could trivially be removed). An extra event provider is provided, allowing other threads to know when this call has been completed.
IterativeRobotBase calls DS.updateData() at the beginning of each loop, and only once per loop. This means all commands will always have the same state.
All of this means there is no longer a DS thread. Everything happens synchronously. This means Sim and testing is easier, as you can just call DriverStationSim.NotifyNewData(), and then DriverStation.UpdateData(), and you can guarantee that all the DriverStation.*** data is up to date.
As for Motor Safety and Educational Robot State Handling, those can all be handled by their own threads. The Educational Thread only needs to run under EducationalRobot, and MotorSafety will only be started if there is a motor safety object enabled.
2022-10-21 22:01:55 -07:00
|
|
|
wpi::Event newDataEvent;
|
|
|
|
|
DriverStation::ProvideRefreshedDataEventHandle(newDataEvent.GetHandle());
|
|
|
|
|
|
2021-12-12 16:54:10 -08:00
|
|
|
for (;;) {
|
[hal, wpilib] New DS thread model and implementation (#3787)
The current DS thread model has some pretty major issues. It makes it difficult to know if all data is from the same remote packet, and if the data changes while the robot loop is running. Additionally, the DS thread is used for a few other things (MotorSafety and State Tracking for EducationalRobot). This also makes sim difficult, as user code has to wait for the thread to know it has new data.
This change completely rethinks how threading works in the driver station model.
First, the DS HAL system receives a new data callback, either from Netcomm or DriverStationSim. Inside the context of this callback, all the low latency data is read and put into a cache. Doing some investigation on the robot side, this is perfectly safe to do, and also ensures a ds packet will not be parsed before we finish reading the current packet data.
After all data is read, the cache is swapped with a 2nd buffer. This buffer just stores the data, none of the HAL DS calls read from this buffer. An event is then fired, stating there is new data ready to go.
Robot code calls HAL_UpdateDSData(). This swaps the 2nd buffer with a 3rd buffer, which always contains the current data. This data will not be updated until HAL_UpdateDSData is called again. Which solves the state problem.
The high level driver station classes have. an updateData() call, which calls HAL_UpdateDSData, and then update button state variables, then data log and update the NT FMS data table (Java also caches across the JNI boundary here, but that could trivially be removed). An extra event provider is provided, allowing other threads to know when this call has been completed.
IterativeRobotBase calls DS.updateData() at the beginning of each loop, and only once per loop. This means all commands will always have the same state.
All of this means there is no longer a DS thread. Everything happens synchronously. This means Sim and testing is easier, as you can just call DriverStationSim.NotifyNewData(), and then DriverStation.UpdateData(), and you can guarantee that all the DriverStation.*** data is up to date.
As for Motor Safety and Educational Robot State Handling, those can all be handled by their own threads. The Educational Thread only needs to run under EducationalRobot, and MotorSafety will only be started if there is a motor safety object enabled.
2022-10-21 22:01:55 -07:00
|
|
|
bool timedOut = false;
|
|
|
|
|
bool newData =
|
|
|
|
|
wpi::WaitForObject(newDataEvent.GetHandle(), 0.25, &timedOut);
|
2021-12-12 16:54:10 -08:00
|
|
|
if (!m_active) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if (!newData) {
|
|
|
|
|
++timeoutCount;
|
|
|
|
|
// pause logging after being disconnected for 10 seconds
|
|
|
|
|
if (timeoutCount > 40 && !paused) {
|
|
|
|
|
timeoutCount = 0;
|
|
|
|
|
paused = true;
|
|
|
|
|
m_log.Pause();
|
|
|
|
|
}
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
// when we connect to the DS, resume logging
|
|
|
|
|
timeoutCount = 0;
|
|
|
|
|
if (paused) {
|
|
|
|
|
paused = false;
|
|
|
|
|
m_log.Resume();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!dsRenamed) {
|
|
|
|
|
// track DS attach
|
|
|
|
|
if (DriverStation::IsDSAttached()) {
|
|
|
|
|
++dsAttachCount;
|
|
|
|
|
} else {
|
|
|
|
|
dsAttachCount = 0;
|
|
|
|
|
}
|
2023-03-13 21:27:52 -07:00
|
|
|
if (dsAttachCount > 300) { // 6 seconds
|
2021-12-12 16:54:10 -08:00
|
|
|
std::time_t now = std::time(nullptr);
|
|
|
|
|
auto tm = std::gmtime(&now);
|
|
|
|
|
if (tm->tm_year > 100) {
|
|
|
|
|
// assume local clock is now synchronized to DS, so rename based on
|
|
|
|
|
// local time
|
|
|
|
|
m_log.SetFilename(fmt::format("FRC_{:%Y%m%d_%H%M%S}.wpilog", *tm));
|
|
|
|
|
dsRenamed = true;
|
|
|
|
|
} else {
|
|
|
|
|
dsAttachCount = 0; // wait a bit and try again
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!fmsRenamed) {
|
|
|
|
|
// track FMS attach
|
|
|
|
|
if (DriverStation::IsFMSAttached()) {
|
|
|
|
|
++fmsAttachCount;
|
|
|
|
|
} else {
|
|
|
|
|
fmsAttachCount = 0;
|
|
|
|
|
}
|
2023-03-13 21:27:52 -07:00
|
|
|
if (fmsAttachCount > 250) { // 5 seconds
|
2021-12-12 16:54:10 -08:00
|
|
|
// match info comes through TCP, so we need to double-check we've
|
|
|
|
|
// actually received it
|
|
|
|
|
auto matchType = DriverStation::GetMatchType();
|
|
|
|
|
if (matchType != DriverStation::kNone) {
|
|
|
|
|
// rename per match info
|
|
|
|
|
char matchTypeChar;
|
|
|
|
|
switch (matchType) {
|
|
|
|
|
case DriverStation::kPractice:
|
|
|
|
|
matchTypeChar = 'P';
|
|
|
|
|
break;
|
|
|
|
|
case DriverStation::kQualification:
|
|
|
|
|
matchTypeChar = 'Q';
|
|
|
|
|
break;
|
|
|
|
|
case DriverStation::kElimination:
|
|
|
|
|
matchTypeChar = 'E';
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
matchTypeChar = '_';
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
std::time_t now = std::time(nullptr);
|
|
|
|
|
m_log.SetFilename(
|
|
|
|
|
fmt::format("FRC_{:%Y%m%d_%H%M%S}_{}_{}{}.wpilog",
|
|
|
|
|
*std::gmtime(&now), DriverStation::GetEventName(),
|
|
|
|
|
matchTypeChar, DriverStation::GetMatchNumber()));
|
|
|
|
|
fmsRenamed = true;
|
|
|
|
|
dsRenamed = true; // don't override FMS rename
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Write system time every ~5 seconds
|
|
|
|
|
++sysTimeCount;
|
|
|
|
|
if (sysTimeCount >= 250) {
|
|
|
|
|
sysTimeCount = 0;
|
|
|
|
|
sysTimeEntry.Append(wpi::GetSystemTime(), wpi::Now());
|
|
|
|
|
}
|
|
|
|
|
}
|
[hal, wpilib] New DS thread model and implementation (#3787)
The current DS thread model has some pretty major issues. It makes it difficult to know if all data is from the same remote packet, and if the data changes while the robot loop is running. Additionally, the DS thread is used for a few other things (MotorSafety and State Tracking for EducationalRobot). This also makes sim difficult, as user code has to wait for the thread to know it has new data.
This change completely rethinks how threading works in the driver station model.
First, the DS HAL system receives a new data callback, either from Netcomm or DriverStationSim. Inside the context of this callback, all the low latency data is read and put into a cache. Doing some investigation on the robot side, this is perfectly safe to do, and also ensures a ds packet will not be parsed before we finish reading the current packet data.
After all data is read, the cache is swapped with a 2nd buffer. This buffer just stores the data, none of the HAL DS calls read from this buffer. An event is then fired, stating there is new data ready to go.
Robot code calls HAL_UpdateDSData(). This swaps the 2nd buffer with a 3rd buffer, which always contains the current data. This data will not be updated until HAL_UpdateDSData is called again. Which solves the state problem.
The high level driver station classes have. an updateData() call, which calls HAL_UpdateDSData, and then update button state variables, then data log and update the NT FMS data table (Java also caches across the JNI boundary here, but that could trivially be removed). An extra event provider is provided, allowing other threads to know when this call has been completed.
IterativeRobotBase calls DS.updateData() at the beginning of each loop, and only once per loop. This means all commands will always have the same state.
All of this means there is no longer a DS thread. Everything happens synchronously. This means Sim and testing is easier, as you can just call DriverStationSim.NotifyNewData(), and then DriverStation.UpdateData(), and you can guarantee that all the DriverStation.*** data is up to date.
As for Motor Safety and Educational Robot State Handling, those can all be handled by their own threads. The Educational Thread only needs to run under EducationalRobot, and MotorSafety will only be started if there is a motor safety object enabled.
2022-10-21 22:01:55 -07:00
|
|
|
DriverStation::RemoveRefreshedDataEventHandle(newDataEvent.GetHandle());
|
2021-12-12 16:54:10 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Thread::StartNTLog() {
|
|
|
|
|
if (!m_ntLoggerEnabled) {
|
|
|
|
|
m_ntLoggerEnabled = true;
|
|
|
|
|
auto inst = nt::NetworkTableInstance::GetDefault();
|
|
|
|
|
m_ntEntryLogger = inst.StartEntryDataLog(m_log, "", "NT:");
|
|
|
|
|
m_ntConnLogger = inst.StartConnectionDataLog(m_log, "NTConnection");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Thread::StopNTLog() {
|
|
|
|
|
if (m_ntLoggerEnabled) {
|
|
|
|
|
m_ntLoggerEnabled = false;
|
|
|
|
|
nt::NetworkTableInstance::StopEntryDataLog(m_ntEntryLogger);
|
|
|
|
|
nt::NetworkTableInstance::StopConnectionDataLog(m_ntConnLogger);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Instance::Instance(std::string_view dir, std::string_view filename,
|
|
|
|
|
double period) {
|
|
|
|
|
// Delete all previously existing FRC_TBD_*.wpilog files. These only exist
|
|
|
|
|
// when the robot never connects to the DS, so they are very unlikely to
|
|
|
|
|
// have useful data and just clutter the filesystem.
|
|
|
|
|
auto logDir = MakeLogDir(dir);
|
|
|
|
|
std::error_code ec;
|
|
|
|
|
for (auto&& entry : fs::directory_iterator{logDir, ec}) {
|
|
|
|
|
if (wpi::starts_with(entry.path().stem().string(), "FRC_TBD_") &&
|
|
|
|
|
entry.path().extension() == ".wpilog") {
|
|
|
|
|
if (!fs::remove(entry, ec)) {
|
|
|
|
|
fmt::print(stderr, "DataLogManager: could not delete {}\n",
|
|
|
|
|
entry.path().string());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
owner.Start(logDir, filename, period);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static Instance& GetInstance(std::string_view dir = "",
|
|
|
|
|
std::string_view filename = "",
|
|
|
|
|
double period = 0.25) {
|
|
|
|
|
static Instance instance(dir, filename, period);
|
|
|
|
|
return instance;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void DataLogManager::Start(std::string_view dir, std::string_view filename,
|
|
|
|
|
double period) {
|
|
|
|
|
GetInstance(dir, filename, period);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void DataLogManager::Log(std::string_view message) {
|
|
|
|
|
GetInstance().owner.GetThread()->m_messageLog.Append(message);
|
|
|
|
|
fmt::print("{}\n", message);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
wpi::log::DataLog& DataLogManager::GetLog() {
|
|
|
|
|
return GetInstance().owner.GetThread()->m_log;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string DataLogManager::GetLogDir() {
|
|
|
|
|
return GetInstance().owner.GetThread()->m_logDir;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void DataLogManager::LogNetworkTables(bool enabled) {
|
|
|
|
|
if (auto thr = GetInstance().owner.GetThread()) {
|
|
|
|
|
if (enabled) {
|
|
|
|
|
thr->StartNTLog();
|
|
|
|
|
} else if (!enabled) {
|
|
|
|
|
thr->StopNTLog();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|