Skip to content
shueja edited this page Feb 8, 2024 · 12 revisions

Monologue

1. n. A one-sided conversation.

Monologue is a Java annotation-based logging library for FRC. With Monologue, extensive telemetry and on-robot logging can be added to your robot code with minimal code footprint and design restrictions.

Setup Overview

  1. Install Monologue.
  2. Choose a root object, make it implement monologue.Logged and call Monologue.setupLogging(...). See below for details.
  3. Call Monologue.updateAll() periodically. If this is not called, logging entries will appear without values.
  4. Add logging annotations to fields and methods you want to capture. During Monologue.setupLogging(...), Monologue will recursively search for annotations through the root object and any of its class-scope variables that also implement Logged.
  5. Add imperative logging calls within Logged classes for variables that can't or shouldn't be put in class-scope.

1. Installation

For teams already familiar with Oblog, the setup process for Monologue is very similar.

The first step to using Monologue is to add it to your robot project as a dependency. Monologue is distributed using jitpack.

To add the dependency, we only need to add a couple lines to your project’s build.gradle. First, add the following:

repositories {
  maven { url 'https://jitpack.io' }
}

Secondly, add the following to the dependencies list:

implementation 'com.github.shueja:Monologue:RELEASE_TAG'

where RELEASE_TAG is the latest release version tag (e.g. v1.0.0).

2. Setting Up the Logger

Choose a class to be the root of the logging tree. Monologue will log any annotated fields in this class, in Logged-implementing objects that are fields of this class, and so on recursively to build a "tree" of logged fields.

Robot.java is a good choice for a base class, though for command-based teams, RobotContainer.java is a good alternative. Be sure to set up the logger after all contained Logged classes have been constructed, typically at the end of robotInit() or the RobotContainer constructor.

IMPORTANT: Your base class needs to implement monologue.Logged, so that Monologue can create a level in the logging structure for it.

A basic configuration for a simple TimedRobot project would be like this:

package frc.robot;

import monologue.Monologue;
import monologue.Logged;

public class Robot extends TimedRobot implements Logged {
  @Override
  public void robotInit() {
      ... 
      boolean fileOnly = false;
      boolean lazyLogging = false;
      Monologue.setupMonologue(this, "Robot", fileOnly, lazyLogging);
  }
  
  @Override
  public void robotPeriodic() {
     // setFileOnly is used to shut off NetworkTables broadcasting for most logging calls.
     // Basing this condition on the connected state of the FMS is a suggestion only.
     Monologue.setFileOnly(DriverStation.isFMSConnected());
     // This method needs to be called periodically, or no logging annotations will process properly.
     Monologue.updateAll();
  }
}

The first argument to Monologue.setupLogging() is the root Logged class, which is Robot, or this. The second argument is the path to use for the root container. In this case, values being logged would show up in NetworkTables and DataLog under /Robot/variableName. See section 4

IMPORTANT: Forward slashes as path separators is permitted in the rootPath. However, do not start your rootPath with more than one leading forward slash. Monologue will accept /Robot or Robot as the same path (and will add a slash if necessary to log under /Robot). However, additional leading slashes lead to client-dependent display behavior and should generally be avoided.

3. Monologue.updateAll()

After you have set up the logger, add a periodic call to Monologue.updateAll(), as in the above example.

Monologue to capture all your logged values periodically, publish them to NetworkTables, and add them to the on-robot data logging. Next, let's set up some values to log.

4. Logging Annotations

Monologue offers two annotations (@Log.NT, @Log.File), which can be applied to variables of supported types, or methods that return the supported types.

@Log used alone is an alias for @Log.NT. For clarity, this section will use @Log.NT.

Note that any method you apply the annotation to will be run every periodic loop. To use these, simply annotate the field or method with the desired annotation. The class containing the annotated field must implement Logged, even if it is your root container. Once a class has been declared Logged, Monologue will automatically search it for annotated fields and getters to capture - as long as it is reachable from the specified root through a direct sequence of Logged parent classes (the logger will recursively search down the tree of all Logged fields from the root container and place the values in the NT/DataLog tree according to the class structure).

Annotatable Data Types

  • Primitives: int, long, float, double, boolean
  • Primitive arrays: int[], long[], float[], double[], boolean[]
  • String and String[] (note that logging Strings is significantly more performance-heavy than logging primitives)
  • WPILib struct-serializable types: for example, geometry types (Pose2d, etc), other simple structured data (SwerveModuleState, etc). For the full list of struct-serializable types, see the implementing classes of StructSerializable in WPILib's Javadocs.
  • Arrays of the above struct-serializable types.
  • Sendables such as Field2d and Mechanism2d. (Note that these will always publish to NetworkTables, and ignore the file-only and lazy-logging features of Monologue. This is due to limitations in the Sendable API.)

Logging Levels

Each annotated field can be set up with an optional logging level. These determine how Monologue handles the field based on the global fileOnly flag.

fileOnly=false @Log.NT @Log.File fileOnly=true @Log.NT @Log.File
DEFAULT NT, file file file file
NOT_FILE_ONLY NT, file file none none
OVERRIDE_FILE_ONLY NT, file file NT, file file

LogLevel.NOT_FILE_ONLY

When used with a @Log.File annotation, the field will only appear in the datalog, and only while fileOnly is false. When used with a @Log.NT annotation, the field will appear on NetworkTables and in the datalog, but only while fileOnly is false. When fileOnly is true, the field will not be logged at all.

LogLevel.DEFAULT

If a level is not specified, this level will be used. When used with a @Log.File annotation, the field will appear in the datalog at all times, and will not appear on NetworkTables. When used with a @Log.NT annotation, the field will appear in the datalog at all times. Additionally, the field will appear on NetworkTables, only while fileOnly is false.

LogLevel.OVERRIDE_FILE_ONLY

When used with a @Log.File annotation, the field will appear in the datalog at all times, and will not appear on NetworkTables. When used with a @Log.NT annotation, the field will appear in the datalog at all times and on NetworkTables at all times, regardless of the value of fileOnly.

Use OVERRIDE_FILE_ONLY for any values you want to communicate over NetworkTables while fileOnly is false. This includes driver dashboards and custom coprocessor communication (though Monologue is not recommended for building NetworkTables communication protocols to coprocessors).

5. Imperative logging within Logged classes

By making a class implement Logged, you not only mark it for Monologue to include in the logging tree, you also gain access to methods that allow you to one-shot log additional fields to the right place in the logging tree.

To imperatively log an additional value, call this.log(String key, T value, LogLevel level). The level parameter is optional, and Monologue will use LogLevel.DEFAULT if not specified. The key will be relative to the Logged class's place in the tree. Therefore, if you are in a class that is logged as /Robot/drivetrain, and call log("error", 2.4), an entry will be in the logging tree under /Robot/drivetrain/error, with a value of 2.4.

There are three major caveats with this feature.

  1. log calls made before Monologue.setupMonologue is called will be silently dropped. This includes calls made in the constructor of the Logged class.
  2. Sendables cannot be logged using these calls, but all other data types above are supported.
  3. Using a key that conflicts with the name of an annotated field or method will likely cause conflicting data recorded in the datalog, and will possibly cause NetworkTables errors. Making log calls to the same key from different places in the class will not cause this issue.

Lazy Logging

To prevent datalog files becoming bloated with multiple records of a value that hasn't changed since the last update, Monologue offers a lazy logging feature. This can only be configured when setting up the logger. When lazy logging is enabled, Monologue will compare the current value-to-be-logged with the value from the previous updateAll() call. If the values are equal in their contents, Monologue will not write another record to the log or to NT.

Please note that comparisons of arrays are most efficient if the array does not change length from one updateAll() call to another.