From 691e5165d6b88f31daad0ec6a4d3028663a2b564 Mon Sep 17 00:00:00 2001 From: mxtmx Date: Sat, 24 Jan 2026 02:01:09 -0800 Subject: [PATCH 1/2] Add Phases documentation --- SUMMARY.md | 2 + automations/phasemanager.md | 220 ++++++++++++++++++++++++++++++++++++ concepts/phases.md | 34 ++++++ 3 files changed, 256 insertions(+) create mode 100644 automations/phasemanager.md create mode 100644 concepts/phases.md diff --git a/SUMMARY.md b/SUMMARY.md index 3e2dd2e..1d20e3e 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -10,6 +10,7 @@ * [Spatial Awareness](concepts/spatial-awareness.md) * [Time Awareness](concepts/time-awareness.md) +* [Phases](concepts/phases.md) * [Retries](concepts/retries.md) ## Automations @@ -20,6 +21,7 @@ * [CompositeZone](automations/zones/compositezone.md) * [TimerEx](automations/timerex.md) * [RetryCommand](automations/retrycommand.md) +* [PhaseManager](automations/phasemanager.md) ## Dev Tools diff --git a/automations/phasemanager.md b/automations/phasemanager.md new file mode 100644 index 0000000..1e31c0a --- /dev/null +++ b/automations/phasemanager.md @@ -0,0 +1,220 @@ +--- +description: package com.skeletonarmy.marrow.phases +--- + +# PhaseManager + +## Overview + +`PhaseManager` tracks the current phase of the match and provides time information. It allows you to define custom match phases and automatically transitions through them based on elapsed time. + +Use `PhaseManager` to implement time-aware autonomous and teleop strategies. Register listeners to trigger different command groups when phases change, or query the current phase and remaining time to make dynamic decisions. + +Check out the ["Phases" page](../concepts/phases.md) to learn how to use `PhaseManager` for phase-aware behaviors. + +*** + +## Creating Phases + +A `Phase` represents a segment of the match with a name, duration, and optional time unit. Durations default to seconds but can use any `TimeUnit`: + +```java +// Seconds (default) +Phase autonomousPhase = new Phase("Autonomous", 28); +Phase parkPhase = new Phase("Park", 2); + +// Custom time units +Phase phase = new Phase("Quick", 500, TimeUnit.MILLISECONDS); +``` + +**Phase Equality**: Phases are compared **by name only**. Two phases with the same name are considered equal, even if they have different durations or time units. This affects both `equals()` and `hashCode()` behavior. + +*** + +## Usage + +To use `PhaseManager`, you need to: + +1. **Initialize** it at the start of your OpMode with your phases: + ```java + Phase autonomousPhase = new Phase("Autonomous", 28); + Phase parkPhase = new Phase("Park", 2); + PhaseManager.init(this, autonomousPhase, parkPhase); + ``` + +2. **Update** it once per loop iteration: + ```java + PhaseManager.update(); + ``` + +3. **Query** the current phase and time as needed: + ```java + if (PhaseManager.isCurrentPhase("Park")) { + driveToParking(); + } + + if (PhaseManager.getTimeRemaining() < 5) { + // Do something with final seconds + } + ``` + +The timer starts automatically when the match begins (when the robot state becomes `RUNNING`). + +*** + +## Common Methods + +### Phase Methods + +**`getName()`** + +Get the name of a phase: + +```java +Phase phase = new Phase("Endgame", 20); +String name = phase.getName(); // "Endgame" +``` + +**`getDuration()` / `getUnit()`** + +Get the duration and time unit as originally specified: + +```java +Phase phase = new Phase("Quick", 500, TimeUnit.MILLISECONDS); +double duration = phase.getDuration(); // 500 +TimeUnit unit = phase.getUnit(); // TimeUnit.MILLISECONDS +``` + +**`getDurationSeconds()`** + +Get the duration converted to seconds (useful when phases use different time units): + +```java +Phase phase = new Phase("Quick", 500, TimeUnit.MILLISECONDS); +double seconds = phase.getDurationSeconds(); // 0.5 +``` + +**`toString()`** + +Get the phase name as a string: + +```java +Phase phase = new Phase("Endgame", 20); +String str = phase.toString(); // "Endgame" +``` + +### PhaseManager Methods + +**`isCurrentPhase(String name)` / `isCurrentPhase(Phase phase)`** + +Check which phase you're in: + +```java +if (PhaseManager.isCurrentPhase("Park")) { + // Execute parking routine +} +``` + +**`getCurrentPhase()`** + +Get the current phase object: + +```java +Phase current = PhaseManager.getCurrentPhase(); +``` + +**`getElapsedTime()`** + +Total elapsed time since match start (in seconds): + +```java +double elapsed = PhaseManager.getElapsedTime(); +``` + +**`getTimeRemaining()` / `getPhaseTimeRemaining()`** + +Time remaining in the match or current phase (in seconds): + +```java +double matchRemaining = PhaseManager.getTimeRemaining(); +double phaseRemaining = PhaseManager.getPhaseTimeRemaining(); +``` + +**`getTotalMatchDuration()`** + +Total match duration (sum of all phase durations): + +```java +double total = PhaseManager.getTotalMatchDuration(); +``` + +**`addPhaseListener(PhaseListener)` / `removePhaseListener(PhaseListener)`** + +Listen to phase transitions. Register a `PhaseListener` that will be called whenever the robot enters a new phase: + +```java +PhaseManager.addPhaseListener(newPhase -> { + if (newPhase.getName().equals("Endgame")) { + // ENTERING ENDGAME! + } +}); + +// Remove specific listener +PhaseManager.removePhaseListener(myListener); +``` + +**`clearPhaseListeners()`** + +Remove all registered phase listeners at once: + +```java +PhaseManager.clearPhaseListeners(); +``` + +*** + +## Example: Phase-Based Behavior + +```java +@TeleOp +public class MyTeleOp extends LinearOpMode { + @Override + public void runOpMode() { + Phase teleopPhase = new Phase("Teleop", 100); + Phase endgamePhase = new Phase("Endgame", 20); + + PhaseManager.init(this, teleopPhase, endgamePhase); + + PhaseManager.addPhaseListener(phase -> { + if (phase.getName().equals("Endgame")) { + telemetry.addLine("ENTERING ENDGAME!"); + } + }); + + waitForStart(); + + while (opModeIsActive()) { + PhaseManager.update(); + + if (PhaseManager.getTimeRemaining() < 5) { + telemetry.addLine("FINAL 5 SECONDS"); + } + + telemetry.addData("Phase", PhaseManager.getCurrentPhase().getName()); + telemetry.addData("Time Remaining", String.format("%.1f", PhaseManager.getTimeRemaining())); + telemetry.update(); + } + } +} +``` + +*** + +## Notes + +- **Phase ordering matters**: Phases are transitioned in the order you provide to `init()`. Make sure your durations align with your actual match strategy. +- **Match start detection**: The timer starts when the robot state becomes `RUNNING` (i.e., when `waitForStart()` completes). +- **Phase listeners are resilient**: Exceptions in phase listeners won't break your match. Errors are logged to telemetry. +- **Phases are compared by name**: Two `Phase` objects are equal if they have the same name, regardless of duration or time unit. This means `equals()` and `hashCode()` are based on the phase name only. +- **PhaseManager is stateful**: `PhaseManager` is a static manager. Don't instantiate it directly; call static methods instead. +- **Phases are transitioned in order**: The match progresses through phases sequentially. The last phase continues until the match ends, even if elapsed time exceeds its duration. diff --git a/concepts/phases.md b/concepts/phases.md new file mode 100644 index 0000000..274cc60 --- /dev/null +++ b/concepts/phases.md @@ -0,0 +1,34 @@ +--- +description: How to leverage phases for time-aware strategies +--- + +# Phases + +In FTC competitions, matches have distinct phases. For example, in autonomous, you have the driving phase and then a park phase. In teleop, you have the main driving phase and the endgame. Each phase has different objectives and time constraints. + +Without phase awareness, your robot treats the entire match as one continuous period. This leads to: + +* Spending too much time on low-value tasks when you should be securing endgame points. +* Attempting complex maneuvers when you should be parking for guaranteed points. +* Losing track of how much time you have left for the current objective. + +**Match phases** solve this problem by dividing your match into logical segments with their own durations. Your code can react to phase transitions, changing strategy dynamically as the match progresses. + +*** + +## Real-World Scenarios + +* Your autonomous runs scoring cycles for 28 seconds, then parks in the final 2 seconds. +* Your teleop executes aggressive scoring for 100 seconds, then switches to endgame behavior (hang, climb, etc.) in the final 20 seconds. +* A subsystem knows it's in endgame and prioritizes hang mechanism deployment over other tasks. +* Your command scheduler switches to a different command group when entering the final phase. + +*** + +## Using Marrow + +To make this easier, Marrow provides [`PhaseManager`](../automations/phasemanager.md). + +Instead of manually tracking time and implementing phase logic yourself, [`PhaseManager`](../automations/phasemanager.md) automatically transitions your match through custom-defined phases and provides methods to query the current phase, remaining time, and phase transitions. + +For a full breakdown of how to use it in your own code, see the [`PhaseManager` reference page](../automations/phasemanager.md). From 75ae271ddd123b8088501da79eb5e021b733c08c Mon Sep 17 00:00:00 2001 From: mxtmx Date: Tue, 27 Jan 2026 17:22:55 -0800 Subject: [PATCH 2/2] Update PhaseManager docs for instance-based usage --- automations/phasemanager.md | 69 ++++++++++++++++++++++--------------- concepts/phases.md | 2 +- 2 files changed, 42 insertions(+), 29 deletions(-) diff --git a/automations/phasemanager.md b/automations/phasemanager.md index 1e31c0a..1790a00 100644 --- a/automations/phasemanager.md +++ b/automations/phasemanager.md @@ -6,7 +6,7 @@ description: package com.skeletonarmy.marrow.phases ## Overview -`PhaseManager` tracks the current phase of the match and provides time information. It allows you to define custom match phases and automatically transitions through them based on elapsed time. +`PhaseManager` tracks the current phase of the match and provides time information. Create an instance with your custom match phases, and it automatically transitions through them based on elapsed time. Use `PhaseManager` to implement time-aware autonomous and teleop strategies. Register listeners to trigger different command groups when phases change, or query the current phase and remaining time to make dynamic decisions. @@ -27,33 +27,37 @@ Phase parkPhase = new Phase("Park", 2); Phase phase = new Phase("Quick", 500, TimeUnit.MILLISECONDS); ``` -**Phase Equality**: Phases are compared **by name only**. Two phases with the same name are considered equal, even if they have different durations or time units. This affects both `equals()` and `hashCode()` behavior. - *** ## Usage To use `PhaseManager`, you need to: -1. **Initialize** it at the start of your OpMode with your phases: +1. **Create an instance** in your OpMode's `init()` with your phases: ```java Phase autonomousPhase = new Phase("Autonomous", 28); Phase parkPhase = new Phase("Park", 2); - PhaseManager.init(this, autonomousPhase, parkPhase); + PhaseManager phaseManager = new PhaseManager(this, autonomousPhase, parkPhase); ``` 2. **Update** it once per loop iteration: ```java - PhaseManager.update(); + phaseManager.update(); ``` 3. **Query** the current phase and time as needed: ```java - if (PhaseManager.isCurrentPhase("Park")) { + // Check by phase reference + if (phaseManager.isCurrentPhase(parkPhase)) { + driveToParking(); + } + + // Or check by name + if (phaseManager.isCurrentPhase("Park")) { driveToParking(); } - if (PhaseManager.getTimeRemaining() < 5) { + if (phaseManager.getTimeRemaining() < 5) { // Do something with final seconds } ``` @@ -110,7 +114,13 @@ String str = phase.toString(); // "Endgame" Check which phase you're in: ```java -if (PhaseManager.isCurrentPhase("Park")) { +// Check by name +if (phaseManager.isCurrentPhase("Park")) { + // Execute parking routine +} + +// Check by phase reference +if (phaseManager.isCurrentPhase(parkPhase)) { // Execute parking routine } ``` @@ -120,7 +130,7 @@ if (PhaseManager.isCurrentPhase("Park")) { Get the current phase object: ```java -Phase current = PhaseManager.getCurrentPhase(); +Phase current = phaseManager.getCurrentPhase(); ``` **`getElapsedTime()`** @@ -128,7 +138,7 @@ Phase current = PhaseManager.getCurrentPhase(); Total elapsed time since match start (in seconds): ```java -double elapsed = PhaseManager.getElapsedTime(); +double elapsed = phaseManager.getElapsedTime(); ``` **`getTimeRemaining()` / `getPhaseTimeRemaining()`** @@ -136,8 +146,8 @@ double elapsed = PhaseManager.getElapsedTime(); Time remaining in the match or current phase (in seconds): ```java -double matchRemaining = PhaseManager.getTimeRemaining(); -double phaseRemaining = PhaseManager.getPhaseTimeRemaining(); +double matchRemaining = phaseManager.getTimeRemaining(); +double phaseRemaining = phaseManager.getPhaseTimeRemaining(); ``` **`getTotalMatchDuration()`** @@ -145,7 +155,7 @@ double phaseRemaining = PhaseManager.getPhaseTimeRemaining(); Total match duration (sum of all phase durations): ```java -double total = PhaseManager.getTotalMatchDuration(); +double total = phaseManager.getTotalMatchDuration(); ``` **`addPhaseListener(PhaseListener)` / `removePhaseListener(PhaseListener)`** @@ -153,14 +163,14 @@ double total = PhaseManager.getTotalMatchDuration(); Listen to phase transitions. Register a `PhaseListener` that will be called whenever the robot enters a new phase: ```java -PhaseManager.addPhaseListener(newPhase -> { +phaseManager.addPhaseListener(newPhase -> { if (newPhase.getName().equals("Endgame")) { // ENTERING ENDGAME! } }); // Remove specific listener -PhaseManager.removePhaseListener(myListener); +phaseManager.removePhaseListener(myListener); ``` **`clearPhaseListeners()`** @@ -168,7 +178,7 @@ PhaseManager.removePhaseListener(myListener); Remove all registered phase listeners at once: ```java -PhaseManager.clearPhaseListeners(); +phaseManager.clearPhaseListeners(); ``` *** @@ -178,14 +188,18 @@ PhaseManager.clearPhaseListeners(); ```java @TeleOp public class MyTeleOp extends LinearOpMode { + private PhaseManager phaseManager; + private Phase teleopPhase; + private Phase endgamePhase; + @Override public void runOpMode() { - Phase teleopPhase = new Phase("Teleop", 100); - Phase endgamePhase = new Phase("Endgame", 20); + teleopPhase = new Phase("Teleop", 100); + endgamePhase = new Phase("Endgame", 20); - PhaseManager.init(this, teleopPhase, endgamePhase); + phaseManager = new PhaseManager(this, teleopPhase, endgamePhase); - PhaseManager.addPhaseListener(phase -> { + phaseManager.addPhaseListener(phase -> { if (phase.getName().equals("Endgame")) { telemetry.addLine("ENTERING ENDGAME!"); } @@ -194,14 +208,14 @@ public class MyTeleOp extends LinearOpMode { waitForStart(); while (opModeIsActive()) { - PhaseManager.update(); + phaseManager.update(); - if (PhaseManager.getTimeRemaining() < 5) { + if (phaseManager.getTimeRemaining() < 5) { telemetry.addLine("FINAL 5 SECONDS"); } - telemetry.addData("Phase", PhaseManager.getCurrentPhase().getName()); - telemetry.addData("Time Remaining", String.format("%.1f", PhaseManager.getTimeRemaining())); + telemetry.addData("Phase", phaseManager.getCurrentPhase().getName()); + telemetry.addData("Time Remaining", String.format("%.1f", phaseManager.getTimeRemaining())); telemetry.update(); } } @@ -212,9 +226,8 @@ public class MyTeleOp extends LinearOpMode { ## Notes -- **Phase ordering matters**: Phases are transitioned in the order you provide to `init()`. Make sure your durations align with your actual match strategy. +- **Phase ordering matters**: Phases are transitioned in the order you provide to the constructor. Make sure your durations align with your actual match strategy. - **Match start detection**: The timer starts when the robot state becomes `RUNNING` (i.e., when `waitForStart()` completes). - **Phase listeners are resilient**: Exceptions in phase listeners won't break your match. Errors are logged to telemetry. -- **Phases are compared by name**: Two `Phase` objects are equal if they have the same name, regardless of duration or time unit. This means `equals()` and `hashCode()` are based on the phase name only. -- **PhaseManager is stateful**: `PhaseManager` is a static manager. Don't instantiate it directly; call static methods instead. +- **Instance-based design**: `PhaseManager` is an instance-based class. Create an instance using `new PhaseManager(opMode, phases...)` and call methods on that instance. - **Phases are transitioned in order**: The match progresses through phases sequentially. The last phase continues until the match ends, even if elapsed time exceeds its duration. diff --git a/concepts/phases.md b/concepts/phases.md index 274cc60..49aad22 100644 --- a/concepts/phases.md +++ b/concepts/phases.md @@ -29,6 +29,6 @@ Without phase awareness, your robot treats the entire match as one continuous pe To make this easier, Marrow provides [`PhaseManager`](../automations/phasemanager.md). -Instead of manually tracking time and implementing phase logic yourself, [`PhaseManager`](../automations/phasemanager.md) automatically transitions your match through custom-defined phases and provides methods to query the current phase, remaining time, and phase transitions. +Instead of manually tracking time and implementing phase logic yourself, you create a `PhaseManager` instance that automatically transitions your match through custom-defined phases and provides methods to query the current phase, remaining time, and phase transitions. For a full breakdown of how to use it in your own code, see the [`PhaseManager` reference page](../automations/phasemanager.md).