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.
|
|
|
|
|
|
|
|
|
|
package edu.wpi.first.wpilibj;
|
|
|
|
|
|
2025-02-19 23:08:17 -06:00
|
|
|
import edu.wpi.first.datalog.DataLog;
|
|
|
|
|
import edu.wpi.first.datalog.DataLogBackgroundWriter;
|
|
|
|
|
import edu.wpi.first.datalog.FileLogger;
|
|
|
|
|
import edu.wpi.first.datalog.IntegerLogEntry;
|
|
|
|
|
import edu.wpi.first.datalog.StringLogEntry;
|
2024-11-29 22:13:31 -08:00
|
|
|
import edu.wpi.first.hal.HAL;
|
2021-12-12 16:54:10 -08:00
|
|
|
import edu.wpi.first.networktables.NetworkTableInstance;
|
|
|
|
|
import edu.wpi.first.util.WPIUtilJNI;
|
[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
|
|
|
import edu.wpi.first.util.concurrent.Event;
|
2021-12-12 16:54:10 -08:00
|
|
|
import java.io.File;
|
|
|
|
|
import java.io.IOException;
|
|
|
|
|
import java.nio.file.Files;
|
|
|
|
|
import java.nio.file.Path;
|
|
|
|
|
import java.nio.file.Paths;
|
|
|
|
|
import java.time.LocalDateTime;
|
|
|
|
|
import java.time.ZoneId;
|
|
|
|
|
import java.time.format.DateTimeFormatter;
|
|
|
|
|
import java.util.Arrays;
|
|
|
|
|
import java.util.Comparator;
|
|
|
|
|
import java.util.Random;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Centralized data log that provides automatic data log file management. It automatically cleans up
|
|
|
|
|
* old files when disk space is low and renames the file based either on current date/time or (if
|
2023-12-22 13:54:17 -05:00
|
|
|
* available) competition match number. The data file will be saved to a USB flash drive in a folder
|
2025-06-02 16:42:56 -07:00
|
|
|
* named "logs" if one is attached, or to /home/systemcore/logs otherwise.
|
2021-12-12 16:54:10 -08:00
|
|
|
*
|
|
|
|
|
* <p>Log files are initially named "FRC_TBD_{random}.wpilog" until the DS connects. After the DS
|
|
|
|
|
* connects, the log file is renamed to "FRC_yyyyMMdd_HHmmss.wpilog" (where the date/time is UTC).
|
|
|
|
|
* If the FMS is connected and provides a match number, the log file is renamed to
|
|
|
|
|
* "FRC_yyyyMMdd_HHmmss_{event}_{match}.wpilog".
|
|
|
|
|
*
|
|
|
|
|
* <p>On startup, all existing FRC_TBD log files are deleted. If there is less than 50 MB of free
|
|
|
|
|
* space on the target storage, FRC_ log files are deleted (oldest to newest) until there is 50 MB
|
|
|
|
|
* free OR there are 10 files remaining.
|
|
|
|
|
*
|
|
|
|
|
* <p>By default, all NetworkTables value changes are stored to the data log.
|
|
|
|
|
*/
|
|
|
|
|
public final class DataLogManager {
|
2024-05-12 14:09:43 -07:00
|
|
|
private static DataLogBackgroundWriter m_log;
|
2023-11-03 20:34:43 -07:00
|
|
|
private static boolean m_stopped;
|
2021-12-12 16:54:10 -08:00
|
|
|
private static String m_logDir;
|
|
|
|
|
private static boolean m_filenameOverride;
|
2023-11-03 20:34:43 -07:00
|
|
|
private static Thread m_thread;
|
2021-12-12 16:54:10 -08:00
|
|
|
private static final ZoneId m_utc = ZoneId.of("UTC");
|
|
|
|
|
private static final DateTimeFormatter m_timeFormatter =
|
|
|
|
|
DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").withZone(m_utc);
|
|
|
|
|
private static boolean m_ntLoggerEnabled = true;
|
|
|
|
|
private static int m_ntEntryLogger;
|
|
|
|
|
private static int m_ntConnLogger;
|
2024-09-13 01:13:06 -04:00
|
|
|
private static boolean m_consoleLoggerEnabled = true;
|
|
|
|
|
private static FileLogger m_consoleLogger;
|
2021-12-12 16:54:10 -08:00
|
|
|
private static StringLogEntry m_messageLog;
|
|
|
|
|
|
|
|
|
|
// if less than this much free space, delete log files until there is this much free space
|
|
|
|
|
// OR there are this many files remaining.
|
|
|
|
|
private static final long kFreeSpaceThreshold = 50000000L;
|
|
|
|
|
private static final int kFileCountThreshold = 10;
|
|
|
|
|
|
|
|
|
|
private DataLogManager() {}
|
|
|
|
|
|
|
|
|
|
/** Start data log manager with default directory location. */
|
|
|
|
|
public static synchronized void start() {
|
|
|
|
|
start("", "", 0.25);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Start data log manager. The parameters have no effect if the data log manager was already
|
|
|
|
|
* started (e.g. by calling another static function).
|
|
|
|
|
*
|
|
|
|
|
* @param dir if not empty, directory to use for data log storage
|
|
|
|
|
*/
|
|
|
|
|
public static synchronized void start(String dir) {
|
|
|
|
|
start(dir, "", 0.25);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Start data log manager. The parameters have no effect if the data log manager was already
|
|
|
|
|
* started (e.g. by calling another static function).
|
|
|
|
|
*
|
|
|
|
|
* @param dir if not empty, directory to use for data log storage
|
|
|
|
|
* @param filename filename to use; if none provided, the filename is automatically generated
|
|
|
|
|
*/
|
|
|
|
|
public static synchronized void start(String dir, String filename) {
|
|
|
|
|
start(dir, filename, 0.25);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Start data log manager. The parameters have no effect if the data log manager was already
|
|
|
|
|
* started (e.g. by calling another static function).
|
|
|
|
|
*
|
|
|
|
|
* @param dir if not empty, directory to use for data log storage
|
|
|
|
|
* @param filename filename to use; if none provided, the filename is automatically generated
|
|
|
|
|
* @param period time between automatic flushes to disk, in seconds; this is a time/storage
|
|
|
|
|
* tradeoff
|
|
|
|
|
*/
|
|
|
|
|
public static synchronized void start(String dir, String filename, double period) {
|
2023-11-03 20:34:43 -07:00
|
|
|
if (m_log == null) {
|
|
|
|
|
m_logDir = makeLogDir(dir);
|
|
|
|
|
m_filenameOverride = !filename.isEmpty();
|
2021-12-12 16:54:10 -08:00
|
|
|
|
2023-11-03 20:34:43 -07:00
|
|
|
// 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.
|
|
|
|
|
File[] files =
|
|
|
|
|
new File(m_logDir)
|
|
|
|
|
.listFiles((d, name) -> name.startsWith("FRC_TBD_") && name.endsWith(".wpilog"));
|
|
|
|
|
if (files != null) {
|
|
|
|
|
for (File file : files) {
|
|
|
|
|
if (!file.delete()) {
|
|
|
|
|
System.err.println("DataLogManager: could not delete " + file);
|
|
|
|
|
}
|
2021-12-12 16:54:10 -08:00
|
|
|
}
|
|
|
|
|
}
|
2024-05-12 14:09:43 -07:00
|
|
|
m_log = new DataLogBackgroundWriter(m_logDir, makeLogFilename(filename), period);
|
2023-11-03 20:34:43 -07:00
|
|
|
m_messageLog = new StringLogEntry(m_log, "messages");
|
|
|
|
|
|
|
|
|
|
// Log all NT entries and connections
|
|
|
|
|
if (m_ntLoggerEnabled) {
|
|
|
|
|
startNtLog();
|
|
|
|
|
}
|
2024-09-13 01:13:06 -04:00
|
|
|
// Log console output
|
|
|
|
|
if (m_consoleLoggerEnabled) {
|
|
|
|
|
startConsoleLog();
|
|
|
|
|
}
|
2023-11-03 20:34:43 -07:00
|
|
|
} else if (m_stopped) {
|
|
|
|
|
m_log.setFilename(makeLogFilename(filename));
|
|
|
|
|
m_log.resume();
|
|
|
|
|
m_stopped = false;
|
2021-12-12 16:54:10 -08:00
|
|
|
}
|
|
|
|
|
|
2023-11-03 20:34:43 -07:00
|
|
|
if (m_thread == null) {
|
|
|
|
|
m_thread = new Thread(DataLogManager::logMain, "DataLogDS");
|
|
|
|
|
m_thread.setDaemon(true);
|
|
|
|
|
m_thread.start();
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-12-12 16:54:10 -08:00
|
|
|
|
2023-11-03 20:34:43 -07:00
|
|
|
/** Stop data log manager. */
|
|
|
|
|
public static synchronized void stop() {
|
|
|
|
|
if (m_thread != null) {
|
|
|
|
|
m_thread.interrupt();
|
|
|
|
|
m_thread = null;
|
|
|
|
|
}
|
|
|
|
|
if (m_log != null) {
|
|
|
|
|
m_log.stop();
|
|
|
|
|
m_stopped = true;
|
2021-12-12 16:54:10 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Log a message to the "messages" entry. The message is also printed to standard output (followed
|
|
|
|
|
* by a newline).
|
|
|
|
|
*
|
|
|
|
|
* @param message message
|
|
|
|
|
*/
|
|
|
|
|
public static synchronized void log(String message) {
|
|
|
|
|
if (m_messageLog == null) {
|
|
|
|
|
start();
|
|
|
|
|
}
|
|
|
|
|
m_messageLog.append(message);
|
|
|
|
|
System.out.println(message);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the managed data log (for custom logging). Starts the data log manager if not already
|
|
|
|
|
* started.
|
|
|
|
|
*
|
|
|
|
|
* @return data log
|
|
|
|
|
*/
|
|
|
|
|
public static synchronized DataLog getLog() {
|
|
|
|
|
if (m_log == null) {
|
|
|
|
|
start();
|
|
|
|
|
}
|
|
|
|
|
return m_log;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the log directory.
|
|
|
|
|
*
|
|
|
|
|
* @return log directory, or empty string if logging not yet started
|
|
|
|
|
*/
|
|
|
|
|
public static synchronized String getLogDir() {
|
|
|
|
|
if (m_logDir == null) {
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
return m_logDir;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Enable or disable logging of NetworkTables data. Note that unlike the network interface for
|
|
|
|
|
* NetworkTables, this will capture every value change. Defaults to enabled.
|
|
|
|
|
*
|
|
|
|
|
* @param enabled true to enable, false to disable
|
|
|
|
|
*/
|
|
|
|
|
public static synchronized void logNetworkTables(boolean enabled) {
|
|
|
|
|
boolean wasEnabled = m_ntLoggerEnabled;
|
|
|
|
|
m_ntLoggerEnabled = enabled;
|
|
|
|
|
if (m_log == null) {
|
|
|
|
|
start();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (enabled && !wasEnabled) {
|
|
|
|
|
startNtLog();
|
|
|
|
|
} else if (!enabled && wasEnabled) {
|
|
|
|
|
stopNtLog();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-13 01:13:06 -04:00
|
|
|
/**
|
|
|
|
|
* Enable or disable logging of the console output. Defaults to enabled.
|
|
|
|
|
*
|
|
|
|
|
* @param enabled true to enable, false to disable
|
|
|
|
|
*/
|
|
|
|
|
public static synchronized void logConsoleOutput(boolean enabled) {
|
|
|
|
|
boolean wasEnabled = m_consoleLoggerEnabled;
|
|
|
|
|
m_consoleLoggerEnabled = enabled;
|
|
|
|
|
if (m_log == null) {
|
|
|
|
|
start();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (enabled && !wasEnabled) {
|
|
|
|
|
startConsoleLog();
|
|
|
|
|
} else if (!enabled && wasEnabled) {
|
|
|
|
|
stopConsoleLog();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-12-12 16:54:10 -08:00
|
|
|
private static String makeLogDir(String dir) {
|
|
|
|
|
if (!dir.isEmpty()) {
|
|
|
|
|
return dir;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (RobotBase.isReal()) {
|
|
|
|
|
try {
|
|
|
|
|
// prefer a mounted USB drive if one is accessible
|
|
|
|
|
Path usbDir = Paths.get("/u").toRealPath();
|
|
|
|
|
if (Files.isWritable(usbDir)) {
|
2023-11-30 23:13:51 -08:00
|
|
|
if (!new File("/u/logs").mkdir()) {
|
|
|
|
|
// ignored
|
|
|
|
|
}
|
2025-02-07 12:37:23 -08:00
|
|
|
HAL.reportUsage("DataLogManager", "USB");
|
2023-11-30 23:13:51 -08:00
|
|
|
return "/u/logs";
|
2021-12-12 16:54:10 -08:00
|
|
|
}
|
|
|
|
|
} catch (IOException ex) {
|
|
|
|
|
// ignored
|
|
|
|
|
}
|
2025-06-02 16:42:56 -07:00
|
|
|
if (!new File("/home/systemcore/logs").mkdir()) {
|
2023-11-14 12:20:51 -08:00
|
|
|
// ignored
|
|
|
|
|
}
|
2025-02-07 12:37:23 -08:00
|
|
|
HAL.reportUsage("DataLogManager", "Onboard");
|
2025-06-02 16:42:56 -07:00
|
|
|
return "/home/systemcore/logs";
|
2021-12-12 16:54:10 -08:00
|
|
|
}
|
2023-12-11 20:21:06 -08:00
|
|
|
String logDir = Filesystem.getOperatingDirectory().getAbsolutePath() + "/logs";
|
|
|
|
|
if (!new File(logDir).mkdir()) {
|
|
|
|
|
// ignored
|
|
|
|
|
}
|
|
|
|
|
return logDir;
|
2021-12-12 16:54:10 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static String makeLogFilename(String filenameOverride) {
|
|
|
|
|
if (!filenameOverride.isEmpty()) {
|
|
|
|
|
return filenameOverride;
|
|
|
|
|
}
|
|
|
|
|
Random rnd = new Random();
|
|
|
|
|
StringBuilder filename = new StringBuilder();
|
|
|
|
|
filename.append("FRC_TBD_");
|
|
|
|
|
for (int i = 0; i < 4; i++) {
|
|
|
|
|
filename.append(String.format("%04x", rnd.nextInt(0x10000)));
|
|
|
|
|
}
|
|
|
|
|
filename.append(".wpilog");
|
|
|
|
|
return filename.toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void startNtLog() {
|
|
|
|
|
NetworkTableInstance inst = NetworkTableInstance.getDefault();
|
|
|
|
|
m_ntEntryLogger = inst.startEntryDataLog(m_log, "", "NT:");
|
|
|
|
|
m_ntConnLogger = inst.startConnectionDataLog(m_log, "NTConnection");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void stopNtLog() {
|
|
|
|
|
NetworkTableInstance.stopEntryDataLog(m_ntEntryLogger);
|
|
|
|
|
NetworkTableInstance.stopConnectionDataLog(m_ntConnLogger);
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-13 01:13:06 -04:00
|
|
|
private static void startConsoleLog() {
|
|
|
|
|
if (RobotBase.isReal()) {
|
2025-06-02 16:42:56 -07:00
|
|
|
m_consoleLogger = new FileLogger("/home/systemcore/FRC_UserProgram.log", m_log, "console");
|
2024-09-13 01:13:06 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void stopConsoleLog() {
|
|
|
|
|
if (RobotBase.isReal()) {
|
|
|
|
|
m_consoleLogger.close();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-12-12 16:54:10 -08:00
|
|
|
private static void logMain() {
|
|
|
|
|
// based on free disk space, scan for "old" FRC_*.wpilog files and remove
|
|
|
|
|
{
|
|
|
|
|
File logDir = new File(m_logDir);
|
2024-04-21 23:34:05 -04:00
|
|
|
long freeSpace = logDir.getUsableSpace();
|
2021-12-12 16:54:10 -08:00
|
|
|
if (freeSpace < kFreeSpaceThreshold) {
|
|
|
|
|
// Delete oldest FRC_*.wpilog files (ignore FRC_TBD_*.wpilog as we just created one)
|
|
|
|
|
File[] files =
|
|
|
|
|
logDir.listFiles(
|
|
|
|
|
(dir, name) ->
|
|
|
|
|
name.startsWith("FRC_")
|
|
|
|
|
&& name.endsWith(".wpilog")
|
|
|
|
|
&& !name.startsWith("FRC_TBD_"));
|
|
|
|
|
if (files != null) {
|
|
|
|
|
Arrays.sort(files, Comparator.comparingLong(File::lastModified));
|
|
|
|
|
int count = files.length;
|
|
|
|
|
for (File file : files) {
|
|
|
|
|
--count;
|
|
|
|
|
if (count < kFileCountThreshold) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
long length = file.length();
|
|
|
|
|
if (file.delete()) {
|
2023-06-08 23:02:21 -04:00
|
|
|
DriverStation.reportWarning("DataLogManager: Deleted " + file.getName(), false);
|
2021-12-12 16:54:10 -08:00
|
|
|
freeSpace += length;
|
|
|
|
|
if (freeSpace >= kFreeSpaceThreshold) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
System.err.println("DataLogManager: could not delete " + file);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-06-08 23:02:21 -04:00
|
|
|
} else if (freeSpace < 2 * kFreeSpaceThreshold) {
|
|
|
|
|
DriverStation.reportWarning(
|
|
|
|
|
"DataLogManager: Log storage device has "
|
|
|
|
|
+ freeSpace / 1000000
|
|
|
|
|
+ " MB of free space remaining! Logs will get deleted below "
|
|
|
|
|
+ kFreeSpaceThreshold / 1000000
|
2024-04-21 23:34:05 -04:00
|
|
|
+ " MB of free space. "
|
2023-06-08 23:02:21 -04:00
|
|
|
+ "Consider deleting logs off the storage device.",
|
|
|
|
|
false);
|
2021-12-12 16:54:10 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int timeoutCount = 0;
|
|
|
|
|
boolean paused = false;
|
|
|
|
|
int dsAttachCount = 0;
|
|
|
|
|
int fmsAttachCount = 0;
|
|
|
|
|
boolean dsRenamed = m_filenameOverride;
|
|
|
|
|
boolean fmsRenamed = m_filenameOverride;
|
|
|
|
|
int sysTimeCount = 0;
|
|
|
|
|
IntegerLogEntry sysTimeEntry =
|
|
|
|
|
new IntegerLogEntry(
|
|
|
|
|
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
|
|
|
Event newDataEvent = new Event();
|
|
|
|
|
DriverStation.provideRefreshedDataEventHandle(newDataEvent.getHandle());
|
2021-12-12 16:54:10 -08:00
|
|
|
while (!Thread.interrupted()) {
|
[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
|
|
|
boolean timedOut;
|
|
|
|
|
try {
|
|
|
|
|
timedOut = WPIUtilJNI.waitForObjectTimeout(newDataEvent.getHandle(), 0.25);
|
|
|
|
|
} catch (InterruptedException e) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
2021-12-12 16:54:10 -08:00
|
|
|
if (Thread.interrupted()) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
[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
|
|
|
if (timedOut) {
|
2021-12-12 16:54:10 -08:00
|
|
|
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-09-30 09:24:06 -07:00
|
|
|
if (dsAttachCount > 50) { // 1 second
|
|
|
|
|
if (RobotController.isSystemTimeValid()) {
|
|
|
|
|
LocalDateTime now = LocalDateTime.now(m_utc);
|
2021-12-12 16:54:10 -08:00
|
|
|
m_log.setFilename("FRC_" + m_timeFormatter.format(now) + ".wpilog");
|
|
|
|
|
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
|
|
|
|
|
DriverStation.MatchType matchType = DriverStation.getMatchType();
|
|
|
|
|
if (matchType != DriverStation.MatchType.None) {
|
|
|
|
|
// rename per match info
|
2024-06-05 00:09:10 -04:00
|
|
|
char matchTypeChar =
|
|
|
|
|
switch (matchType) {
|
|
|
|
|
case Practice -> 'P';
|
|
|
|
|
case Qualification -> 'Q';
|
|
|
|
|
case Elimination -> 'E';
|
|
|
|
|
default -> '_';
|
|
|
|
|
};
|
2021-12-12 16:54:10 -08:00
|
|
|
m_log.setFilename(
|
|
|
|
|
"FRC_"
|
|
|
|
|
+ m_timeFormatter.format(LocalDateTime.now(m_utc))
|
|
|
|
|
+ "_"
|
|
|
|
|
+ DriverStation.getEventName()
|
|
|
|
|
+ "_"
|
|
|
|
|
+ matchTypeChar
|
|
|
|
|
+ DriverStation.getMatchNumber()
|
|
|
|
|
+ ".wpilog");
|
|
|
|
|
fmsRenamed = true;
|
|
|
|
|
dsRenamed = true; // don't override FMS rename
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Write system time every ~5 seconds
|
|
|
|
|
sysTimeCount++;
|
|
|
|
|
if (sysTimeCount >= 250) {
|
|
|
|
|
sysTimeCount = 0;
|
2023-09-30 09:24:06 -07:00
|
|
|
if (RobotController.isSystemTimeValid()) {
|
|
|
|
|
sysTimeEntry.append(WPIUtilJNI.getSystemTime(), WPIUtilJNI.now());
|
|
|
|
|
}
|
2021-12-12 16:54:10 -08:00
|
|
|
}
|
|
|
|
|
}
|
[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
|
|
|
newDataEvent.close();
|
2021-12-12 16:54:10 -08:00
|
|
|
}
|
|
|
|
|
}
|