/*
* Copyright 2013-2016 Cel Skeggs
*
* This file is part of the CCRE, the Common Chicken Runtime Engine.
*
* The CCRE is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* The CCRE is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with the CCRE. If not, see <http://www.gnu.org/licenses/>.
*/
package ccre.timers;
import java.util.ArrayList;
import ccre.channel.BooleanCell;
import ccre.channel.BooleanIO;
import ccre.channel.BooleanInput;
import ccre.channel.BooleanOutput;
import ccre.channel.EventCell;
import ccre.channel.EventInput;
import ccre.channel.EventOutput;
import ccre.channel.FloatInput;
import ccre.scheduler.Scheduler;
import ccre.time.Time;
import ccre.util.Utils;
/**
* An ExpirationTimer acts sort of like an alarm clock. You can schedule a
* series of alarms with certain delays, and then start the timer. When each
* delay passes, the timer will trigger the event associated with the delay. The
* timer can be fed, which restarts the timer (can be used to implement
* WatchDogs, hence the name). The timer can also be stopped, which resets the
* timer and prevents it from running until it is started again.
*
* @author skeggsc
*/
public final class ExpirationTimer {
private final String tag;
/**
* Creates an ExpirationTimer.
*/
public ExpirationTimer() {
this(Utils.getMethodCaller(1).toString());
}
/**
* Creates an ExpirationTimer with a descriptive tag for the time that it
* consumes.
*
* @param tag the scheduling tag
*/
public ExpirationTimer(String tag) {
this.tag = tag;
}
/**
* The list of tasks, in an arbitrary order.
*/
private final ArrayList<Task> tasks = new ArrayList<Task>();
private final BooleanCell isStarted = new BooleanCell();
private EventOutput cancel;
/**
* Schedule an EventOutput to be triggered at a specific delay.
*
* @param delay the delay (in milliseconds) to trigger at.
* @param cnsm the event to fire.
* @throws IllegalStateException if the timer is already running.
*/
public synchronized void schedule(long delay, EventOutput cnsm) throws IllegalStateException {
schedule(FloatInput.always(delay / 1000f), cnsm);
}
/**
* Schedule an EventOutput to be triggered at a dynamic delay.
*
* @param delay the dynamic delay (in seconds) to trigger at.
* @param cnsm the event to fire.
* @throws IllegalStateException if the timer is already running.
*/
public synchronized void schedule(FloatInput delay, EventOutput cnsm) throws IllegalStateException {
if (isStarted.get()) {
throw new IllegalStateException("Timer is running!");
}
tasks.add(new Task(delay, cnsm));
}
/**
* Return an event that will be triggered at the specified delay.
*
* @param delay the delay (in milliseconds) to trigger at.
* @return the event that will be fired.
* @throws IllegalStateException if the timer is already running.
*/
public EventInput schedule(long delay) throws IllegalStateException {
EventCell evt = new EventCell();
schedule(delay, evt);
return evt;
}
/**
* Return an event that will be triggered at a dynamic delay.
*
* @param delay the dynamic delay (in second) to trigger at.
* @return the event that will be fired.
* @throws IllegalStateException if the timer is already running.
*/
public EventInput schedule(FloatInput delay) throws IllegalStateException {
EventCell evt = new EventCell();
schedule(delay, evt);
return evt;
}
/**
* Start the timer running.
*
* @throws IllegalStateException if the timer was already running.
*/
public synchronized void start() throws IllegalStateException {
if (isStarted.get()) {
throw new IllegalStateException("Timer is running!");
}
isStarted.safeSet(true);
feed();
}
/**
* Start or restart the timer running.
*/
public synchronized void startOrFeed() {
if (isStarted.get()) {
feed();
} else {
start();
}
}
/**
* Reset the timer. This will act as if the timer had just been started, in
* terms of which events are fired when.
*
* @throws IllegalStateException if the timer was not started.
*/
public synchronized void feed() throws IllegalStateException {
if (!isStarted.get()) {
throw new IllegalStateException("Timer is not running!");
}
if (cancel != null) {
cancel.event();
cancel = null;
}
cancel = EventOutput.ignored;
long base = Time.currentTimeNanos();
for (Task t : tasks) {
cancel = cancel.combine(Scheduler.scheduleInterruptibleAt(this.tag, base + t.getDelayNanos(), t.cnsm));
}
}
/**
* Stop the timer. This will prevent the timer from running until start is
* called again.
*
* @throws IllegalStateException if the timer was not started.
*/
public synchronized void stop() throws IllegalStateException {
if (!isStarted.get()) {
throw new IllegalStateException("Timer is not running!");
}
if (cancel != null) {
cancel.event();
cancel = null;
}
isStarted.safeSet(false);
}
/**
* Get an event that, when fired, will start the timer. This will not throw
* an IllegalStateException if the timer is already running.
*
* @return the event to start the timer.
*/
public EventOutput getStartEvent() {
return () -> {
if (!isStarted.get()) {
start();
}
};
}
/**
* Get an event that, when fired, will start or feed the timer, like
* startOrFeed().
*
* @return the event to start or feed the timer.
*/
public EventOutput getStartOrFeedEvent() {
return () -> startOrFeed();
}
/**
* Get an event that, when fired, will feed the timer. This will not throw
* an IllegalStateException if the timer is not running.
*
* @return the event to feed the timer.
*/
public EventOutput getFeedEvent() {
return () -> {
if (isStarted.get()) {
feed();
}
};
}
/**
* Get an event that, when fired, will stop the timer. This will not throw
* an IllegalStateException if the timer is not running.
*
* @return the event to stop the timer.
*/
public EventOutput getStopEvent() {
return () -> {
if (isStarted.get()) {
stop();
}
};
}
/**
* When the specified event occurs, start the timer. See getStartEvent() for
* details.
*
* @param src When to start the timer.
* @see #getStartEvent()
*/
public void startWhen(EventInput src) {
src.send(getStartEvent());
}
/**
* When the specified event occurs, feed the timer. See getFeedEvent() for
* details.
*
* @param src When to feed the timer.
* @see #getFeedEvent()
*/
public void feedWhen(EventInput src) {
src.send(getFeedEvent());
}
/**
* When the specified event occurs, start or feed the timer. See
* getStartOrFeedEvent() for details.
*
* @param src When to start or feed the timer.
* @see #getStartOrFeedEvent()
*/
public void startOrFeedWhen(EventInput src) {
src.send(getStartOrFeedEvent());
}
/**
* When the specified event occurs, stop the timer. See getStopEvent() for
* details.
*
* @param src When to stop the timer.
* @see #getStopEvent()
*/
public void stopWhen(EventInput src) {
src.send(getStopEvent());
}
/**
* Control this timer with the given BooleanIO. This will start or stop the
* timer when the input changes. This will not throw an
* IllegalStateException if the timer is in the wrong state or log a
* warning.
*
* Warning: the use of this method in conjunction with other control methods
* may lead to unexpected results!
*
* @param when when this boolean is true, the timer will be running, and
* when this boolean is false, the timer will be stopped.
*/
public void runWhen(BooleanInput when) {
when.send(this.getRunningControl());
}
/**
* Get a BooleanIO that represents whether or not this timer is running.
* This will start or stop the timer when the value is changed, and will
* appear to be the value representing the new state afterward. This will
* not throw an IllegalStateException if the timer is in the wrong state or
* log a warning.
*
* @return a BooleanIO to monitor and control the ExpirationTimer.
*/
public BooleanIO getRunning() {
return BooleanIO.compose(getRunningStatus(), getRunningControl());
}
/**
* Get a BooleanOutput that can be written to in order to control whether or
* not this timer is running. This will start or stop the timer when the
* outputted value changes. This will not throw an IllegalStateException if
* the timer is in the wrong state or log a warning.
*
* @return a BooleanOutput to control the ExpirationTimer.
*/
public BooleanOutput getRunningControl() {
return value -> {
if (value) {
if (!isStarted.get()) {
start();
}
} else {
if (isStarted.get()) {
stop();
}
}
};
}
/**
* Get a BooleanInput representing whether or not the timer is running.
*
* @return an input representing if the timer is running.
*/
public BooleanInput getRunningStatus() {
return isStarted;
}
/**
* Check if the timer is running.
*
* @return if the timer is running.
*/
public boolean isRunning() {
return isStarted.get();
}
/**
* A task that is scheduled for a specific delay after the timer starts.
*/
private static class Task {
/**
* The event to fire.
*/
public final EventOutput cnsm;
/**
* The source of tuning for the delay.
*/
public final FloatInput tuning;
/**
* Create a new task with a tunable delay.
*
* @param delay The delay after which the task is fired, in seconds.
* @param cnsm The EventOutput fired by this Task.
*/
Task(FloatInput delay, EventOutput cnsm) {
this.cnsm = cnsm;
this.tuning = delay;
}
long getDelayNanos() {
return (long) (tuning.get() * Time.NANOSECONDS_PER_SECOND);
}
}
/**
* End the ExpirationTimer's thread as soon as possible.
*/
public synchronized void terminate() {
if (isRunning()) {
stop();
}
}
}