Listeners
Listeners are always executed in the order that they are hooked, so the order in which they're hooked may be important.
Keep in mind these and nearly everything else in this Scheduler API section only run while the Scheduler itself is running.
Overview
okay, now that that's out of the way...
Listeners... the pièce de résistance of the Scheduler API!
They allow you to tersely subscribe to events to do something when said event happens.
If we were to strip all the fancy abstraction off of a listener that's been hooked to the Scheduler, we would end up with something like this:
- Java
- Kotlin
- Brainf***
while (true) { // The loop is the scheduler
if (condition) { // The if is the listener
// do something
}
}
while (true) { // The loop is the scheduler
if (condition) { // The if is the listener
// do something
}
}
+[>condition[do something[-]]<]
Pretty simple, right? Well, that's exactly what a listener is. It just checks for a condition, and does something if it's true. Except it's more versatile than just that.
For example, let's say we want to alert our driver when the lift is too high, and slow the robot down...
- Java
- Kotlin
// Creates the listener by passing in the condition to listen to
new Listener(() -> lift.getHeight() > 1000)
// Runs only once when the condition becomes true
.onRise(() -> telemetry.addLine("Lift is too high, we're slowing the bot down!"))
// Constantly called while the condition is true
.whileHigh(() -> drive.setPowerMultiplier(.5));
Scheduler.launch(this) // Listener only works when the Scheduler is running, needs to be in a LinearOpMode
// Creates the listener by passing in the condition to listen to
Listener { lift.height > 1000 }
// Runs only once when the condition becomes true
.onRise { telemetry.addLine("Lift is too high, we're slowing the bot down!") }
// Constantly called while the condition is true
.whileHigh { drive.powerMultiplier = .5 }
Scheduler.launch(this) // Listener only works when the Scheduler is running, needs to be in a LinearOpMode
onRise
, onFall
, whileHigh
, and whileLow
all return the same Listener object, so you can
chain them together as seen above, instead of explicitly stating the object name every time.
Every time the Scheduler loops, it'll evaluate the passed in lambda, and fire all the events subscribed to the corresponding state.
You might have noticed the onRise
and onFall
, and may be a little confused right now...
Well here's a signal diagram to help:
You can see four distinct words:
- Low: The condition is false
- Rise: The condition went from false to true
- High: The condition is true
- Fall: The condition went from true to false
Similarly, the Listener class has four subscription methods:
whileHigh - (or whileTrue)
whileLow -- (or whileFalse)
onRise ---- (or onJustTrue)
onFall ---- (or onJustFalse)
You're (probably) a smart (enough) cookie; you can probably figure out what they mean. If not, it's fine! Just look at the method documentation for more detailed examples!
The concept of hooking🪝
This isn't super important to know, so you could skip it if you really wanted—I just wanted to explain it real quick as it comes up a bit.
When you first create a Listener, nothing happens. It's not evaluated and updated every loop by the Scheduler; it just sits there and looks pretty.
It's only when you subscribe to the listener (or run Listener.hook
) that it starts being evaluated and updated (i.e.
it gets hooked).
- Java
- Kotlin
Listener listener = new Listener(() -> true); // Not hooked (not in Scheduler's list of listeners)
listener.whileHigh(() -> ...); // Now it's hooked automagically
listener.hook(); // Also hooks it w/out subscribing to any events (No diff made here b/c it's already hooked)
listener.whileHigh(() -> ...); // It's already hooked, so nothing changes
val listener = Listener { true } // Not hooked (not in Scheduler's list of listeners)
listener.whileHigh { ... } // Now it's hooked automagically
listener.hook() // Also hooks it w/out subscribing to any events (No diff made here b/c it's already hooked)
listener.whileHigh { ... } // It's already hooked, so nothing changes
This can actually be used to your advantage with listener.hook by pre-hooking a listener before some other to run the former listener first.
I mean, that is a very niche situation, so do with it what you will Ig lol.
Listener methods
Construction
Constructs a new Listener without hooking it (see: hooking).
- Java
- Kotlin
long startTime = System.currentTimeMillis();
// Creates a new Listener that checks if 5 seconds have passed
Listener timer = new Listener(() -> System.currentTimeMillis() - startTime > 5000);
// Creates a listener which always evaluates to true, so it'll always be high
Listener alwaysTrue = new Listener(() -> true);
val startTime = System.currentTimeMillis()
// Creates a new Listener that checks if 5 seconds have passed
val timer = Listener { System.currentTimeMillis() - startTime > 5000 }
// Creates a listener which always evaluates to true, so it'll always be high
val alwaysTrue = Listener { true }
listener.onRise()
Runs the given callback whenever the condition goes from false to true (i.e. it's on the rising edge)
boolean wasHigh = false;
while (true) {
boolean isHigh = condition();
if (isHigh && !wasHigh) {
// Do thing
}
wasHigh = isHigh;
}
- Java
- Kotlin
Listener gamepadA = new Listener(() -> gamepad1.a);
// Runs only once when the gamepad1.a goes from false to true
// i.e. when gamepad1.a was JUST pressed
gamepadA.onRise(() -> telemetry.addLine("A was pressed!"));
val gamepadA = Listener { gamepad1.a }
// Runs only once when the gamepad1.a goes from false to true
// i.e. when gamepad1.a was JUST pressed
gamepadA.onRise { telemetry.addLine("A was pressed!") }
listener.onFall()
Runs the given callback whenever the condition goes from true to false (i.e. it's on the falling edge)
boolean wasHigh = false;
while (true) {
boolean isHigh = condition();
if (!isHigh && wasHigh) {
// Do thing
}
wasHigh = isHigh;
}
- Java
- Kotlin
Listener gamepadA = new Listener(() -> gamepad1.a);
// Runs only once when the gamepad1.a goes from true to false
// i.e. when the A button was JUST released
gamepadA.onFall(() -> telemetry.addLine("A button released!"))
val gamepadA = Listener { gamepad1.a }
// Runs only once when the gamepad1.a goes from true to false
// i.e. when the A button was JUST released
gamepadA.onFall { telemetry.addLine("A button released!") }
listener.whileHigh()
Runs the given callback while the condition is true.
while (true) {
if (condition()) {
// Do thing
}
}
- Java
- Kotlin
Listener gamepadA = new Listener(() -> gamepad1.a);
// Runs every loop while the A button is pressed
gamepadA.whileHigh(() -> telemetry.addLine("A button is being held!"));
val gamepadA = Listener { gamepad1.a }
// Runs every loop while the A button is pressed
gamepadA.whileHigh { telemetry.addLine("A button is being held!") }
listener.whileLow()
Runs the given callback while the condition is false.
while (true) {
if (!condition()) {
// Do thing
}
}
- Java
- Kotlin
Listener gamepadA = new Listener(() -> gamepad1.a);
// Runs every loop while the A button is not pressed
gamepadA.whileLow(() -> telemetry.addLine("A button is not being held!"));
val gamepadA = Listener { gamepad1.a }
// Runs every loop while the A button is not pressed
gamepadA.whileLow { telemetry.addLine("A button is not being held!") }
listener.hook()
Hooks the Listener to the Scheduler, does nothing if it's already hooked.
Only reason I'd imagine you'd want to do this is if you did Scheduler.reset and want to hook the Listener back up. I don't know why you'd reset the Scheduler in the first place, but you go, girl!!!
Actually, I guess you can use it for hooking up a listener before another certain listener in advance, to keep things running in the right order, but that's a very nice case...
Anyway, here's an example:
- Java
- Kotlin
Listener listener = new Listener(() -> ...);
listener.hook(); // Hooks the listener to the Scheduler
Scheduler.reset(); // Resets the Scheduler
listener.hook(); // Hooks the listener back up
listener.hook(); // Does nothing since it's already hooked
val listener = Listener { ... }
listener.hook() // Hooks the listener to the Scheduler
Scheduler.reset() // Resets the Scheduler
listener.hook() // Hooks the listener back up
listener.hook() // Does nothing since it's already hooked
listener.destroy()
Destroys the Listener, removing it from the Scheduler, and clearing its actions. Leaves the condition alone, though.
Again, not entirely sure why you'd do this, but if you have a good enough reason to do it, you can probably figure out how to use it yourself. Knock yourself out!!!
Listener method aliases
Supported aliases for the subscription methods are:
onJustTrue(callback: Runnable) = onRise(callback)
onJustFalse(callback: Runnable) = onFall(callback)
whileTrue(callback: Runnable) = whileHigh(callback)
whileFalse(callback: Runnable) = whileLow(callback)
Static Listener methods
Listener.always()
A static instance of a Listener that's always true, useful for when you want to run something every loop, but don't want to (or can't) use the beforeEach or afterEach blocks for whatever reason.
Note that you can only pass in a method to run every loop; you can't use onRise
, onFall
or whileLow
with this
Listener; It implicitly calls whileHigh
Keep in mind that this is only hooked the first time it's used (unless you explicitly hook it, of course)
- Java
- Kotlin
Listener.always(() -> telemetry.addLine("This will run every loop!"));
Listener.always { telemetry.addLine("This will run every loop!") }
Listener builders
For convenience, you can combine multiple listeners/conditions into one using a variety of logical operators.
The syntax is as follows:
listener1.whateveroperator(listener2)
or listener.whateveroperator(() -> someBoolean)
- Java
- Kotlin
Listener gamepadA = new Listener(() -> gamepad1.a);
Listener gamepadB = new Listener(() -> gamepad1.b);
// True if both A and B are pressed
Listener gamepadA_and_B = gamepadA.and(gamepadB);
// True if either A or X are pressed
Listener gamepadA_or_X = gamepadA.or(() -> gamepad1.x);
// True if A is pressed and B is not pressed
Listener gamepadA_and_notB = gamepadA.and(gamepadB.not());
// True if A is pressed and either B or X are pressed
Listener gamepadA_or_B_or_X = gamepadA.or(gamepadB).or(() -> gamepad1.x);
// True if A is pressed and B is not pressed, or if B is pressed and A is not pressed
Listener gamepadA_xor_B = gamepadA.xor(gamepadB);
/* operator funs plus, div, and not are overridden for and, or, and not! */
/* The rest are infix functions */
val gamepadA = Listener { gamepad1.a }
val gamepadB = Listener { gamepad1.b }
// True if both A and B are pressed
val `gamepadA & B` = gamepadA + gamepadB
// True if either A or X are pressed
val `gamepadA | X` = gamepadA / { gamepad1.x }
// True if A is pressed and B is not pressed
val `gamepadA & not B = gamepadA + !gamepadB
// True if A is pressed and either B or X are pressed
val `gamepadA | B & X` = gamepadA / gamepadB / { gamepad1.x }
// True if A is pressed and B is not pressed, or if B is pressed and A is not pressed
val `gamepadA ^ B` = gamepadA xor gamepadB
If you're using Kotlin, please read the note in the code above about using parentheses to make sure stuff works as expected.
// works as expected
(gamepadA + gamepadB).onRise { ... }
// also fine
(gamepadA / { !gamepad1.x }).onRise { ... }
// performs the `onRise` on `gamepadB` only, THEN performs the `and` on the gamepadA with gamepadB
// aka it's probably not what you intend lol
gamepadA + gamepadB.onRise { ... }
Currently supported functions/operators are:
listener.and( otherListener )
listener.or( otherListener )
listener.xor( otherListener )
listener.nand( otherListener )
listener.nor( otherListener )
listener.xnor( otherListener )
listener.not( otherListener )
and
listener.and( () -> someBoolean )
listener.or( () -> someBoolean )
listener.xor( () -> someBoolean )
listener.nand( () -> someBoolean )
listener.nor( () -> someBoolean )
listener.xnor( () -> someBoolean )
listener.not( () -> someBoolean )