/* * 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.channel; import ccre.timers.PauseTimer; import ccre.util.Utils; import ccre.verifier.FlowPhase; import ccre.verifier.SetupPhase; /** * A FloatInput is a way to get the current state of a float input, and to * subscribe to notifications of changes in the float input's value. * * A FloatInput also acts as an UpdatingInput that updates when the value * changes, and never updates when the value doesn't change. * * By convention, most float inputs and outputs have states that range from * -1.0f to 1.0f. * * @author skeggsc */ public interface FloatInput extends UpdatingInput { /** * Creates a FloatInput that is always the specified value. * * @param value the value to always have. * @return the FloatInput representing that value. */ @SetupPhase public static FloatInput always(final float value) { return new FloatInput() { @Override public float get() { return value; } @Override public CancelOutput onUpdate(EventOutput notify) { if (notify == null) { throw new NullPointerException(); } return CancelOutput.nothing; } }; } /** * A FloatInput that is always zero. * * Equivalent to <code>FloatInput.always(0)</code>. * * @see #always(float) */ public static final FloatInput zero = always(0); /** * Gets the current value of this float input. * * @return The current value. */ @FlowPhase public float get(); /** * Subscribe to changes in this float input's value. The float output will * be modified whenever the value of this input changes. * * By convention, most float inputs and outputs have states that range from * -1.0f to 1.0f. * * If available, the current value of the input will be written at this * time. * * If the same listener is added multiple times, it has the same effect as * if it was added once. * * @param output The float output to notify when the value changes. * @return a CancelOutput that deregisters the registered EventOutput. DO * NOT FIRE THIS RETURNED EVENT MORE THAN ONCE: UNDEFINED BEHAVIOR MAY * RESULT. */ @SetupPhase public default CancelOutput send(FloatOutput output) { output.safeSet(get()); return onUpdate(() -> output.set(get())); } /** * Provides a FloatInput whose value is the value of this FloatInput plus * the value of <code>other</code>. * * @param other the other FloatInput to include. * @return the combined FloatInput. */ @SetupPhase public default FloatInput plus(FloatInput other) { return FloatOperation.addition.of(this, other); } /** * Provides a FloatInput whose value is the value of this FloatInput minus * the value of <code>other</code>. * * @param other the other FloatInput to include. * @return the combined FloatInput. */ @SetupPhase public default FloatInput minus(FloatInput other) { return FloatOperation.subtraction.of(this, other); } /** * Provides a FloatInput whose value is the value of <code>other</code> * minus the value of this FloatInput. * * @param other the other FloatInput to include. * @return the combined FloatInput. */ @SetupPhase public default FloatInput minusRev(FloatInput other) { return FloatOperation.subtraction.of(other, this); } /** * Provides a FloatInput whose value is the value of this FloatInput * multiplied by the value of <code>other</code>. * * @param other the other FloatInput to include. * @return the combined FloatInput. */ @SetupPhase public default FloatInput multipliedBy(FloatInput other) { return FloatOperation.multiplication.of(this, other); } /** * Provides a FloatInput whose value is the value of this FloatInput divided * by the value of <code>other</code>. * * @param other the other FloatInput to include. * @return the combined FloatInput. */ @SetupPhase public default FloatInput dividedBy(FloatInput other) { return FloatOperation.division.of(this, other); } /** * Provides a FloatInput whose value is the value of <code>other</code> * divided by the value of this FloatInput. * * @param other the other FloatInput to include. * @return the combined FloatInput. */ @SetupPhase public default FloatInput dividedByRev(FloatInput other) { return FloatOperation.division.of(other, this); } /** * Provides a FloatInput whose value is the value of this FloatInput divided * by the value of <code>other</code>. * * @param other the other FloatInput to include. * @return the combined FloatInput. */ @SetupPhase public default FloatInput modulo(FloatInput other) { return FloatOperation.modulation.of(this, other); } /** * Provides a FloatInput whose value is the value of <code>other</code> * modulo the value of this FloatInput. * * @param other the other FloatInput to include. * @return the combined FloatInput. */ @SetupPhase public default FloatInput moduloRev(FloatInput other) { return FloatOperation.modulation.of(other, this); } /** * Provides a FloatInput whose value is the value of this FloatInput plus * <code>other</code>. * * @param other the other value to include. * @return the combined FloatInput. */ @SetupPhase public default FloatInput plus(float other) { return FloatOperation.addition.of(this, other); } /** * Provides a FloatInput whose value is the value of this FloatInput minus * <code>other</code>. * * @param other the other value to include. * @return the combined FloatInput. */ @SetupPhase public default FloatInput minus(float other) { return FloatOperation.subtraction.of(this, other); } /** * Provides a FloatInput whose value is <code>other</code> minus the value * of this FloatInput. * * @param other the other value to include. * @return the combined FloatInput. */ @SetupPhase public default FloatInput minusRev(float other) { return FloatOperation.subtraction.of(other, this); } /** * Provides a FloatInput whose value is the value of this FloatInput * multiplied by <code>other</code>. * * @param other the other value to include. * @return the combined FloatInput. */ @SetupPhase public default FloatInput multipliedBy(float other) { return FloatOperation.multiplication.of(this, other); } /** * Provides a FloatInput whose value is the value of this FloatInput divided * by <code>other</code>. * * @param other the other value to include. * @return the combined FloatInput. */ @SetupPhase public default FloatInput dividedBy(float other) { return FloatOperation.division.of(this, other); } /** * Provides a FloatInput whose value is <code>other</code> divided by the * value of this FloatInput. * * @param other the other value to include. * @return the combined FloatInput. */ @SetupPhase public default FloatInput dividedByRev(float other) { return FloatOperation.division.of(other, this); } /** * Provides a FloatInput whose value is the value of this FloatInput modulo * <code>other</code>. * * @param other the other value to include. * @return the combined FloatInput. */ @SetupPhase public default FloatInput modulo(float other) { return FloatOperation.modulation.of(this, other); } /** * Provides a FloatInput whose value is <code>other</code> modulo the value * of this FloatInput. * * @param other the other value to include. * @return the combined FloatInput. */ @SetupPhase public default FloatInput moduloRev(float other) { return FloatOperation.modulation.of(other, this); } /** * Provides a BooleanInput that is true iff the value of this FloatInput is * at least <code>minimum</code>. * * @param minimum the lower bound. * @return the comparison result as a BooleanInput. */ @SetupPhase public default BooleanInput atLeast(float minimum) { if (Float.isNaN(minimum)) { throw new IllegalArgumentException("Cannot have NaN boundary in atLeast!"); } return new DerivedBooleanInput(this) { @Override protected boolean apply() { return FloatInput.this.get() >= minimum; } }; } /** * Provides a BooleanInput that is true iff the value of this FloatInput is * at least the value of <code>minimum</code>. * * @param minimum the lower bound. * @return the comparison result as a BooleanInput. */ @SetupPhase public default BooleanInput atLeast(FloatInput minimum) { return new DerivedBooleanInput(this, minimum) { @Override protected boolean apply() { return FloatInput.this.get() >= minimum.get(); } }; } /** * Provides a BooleanInput that is true iff the value of this FloatInput is * at most <code>maximum</code>. * * @param maximum the upper bound. * @return the comparison result as a BooleanInput. */ @SetupPhase public default BooleanInput atMost(final float maximum) { if (Float.isNaN(maximum)) { throw new IllegalArgumentException("Cannot have NaN boundary in atMost!"); } return new DerivedBooleanInput(this) { @Override protected boolean apply() { return FloatInput.this.get() <= maximum; } }; } /** * Provides a BooleanInput that is true iff the value of this FloatInput is * at most the value of <code>maximum</code>. * * @param maximum the upper bound. * @return the comparison result as a BooleanInput. */ @SetupPhase public default BooleanInput atMost(final FloatInput maximum) { return new DerivedBooleanInput(this, maximum) { @Override protected boolean apply() { return FloatInput.this.get() <= maximum.get(); } }; } /** * Provides a BooleanInput that is true iff the value of this FloatInput is * outside the range of <code>minimum</code> to <code>maximum</code>. If it * is equal to <code>minimum</code> or <code>maximum</code>, that counts as * within the range and so the value will be false. * * @param minimum the lower bound. * @param maximum the upper bound. * @return the comparison result as a BooleanInput. */ @SetupPhase public default BooleanInput outsideRange(final float minimum, final float maximum) { if (Float.isNaN(minimum) || Float.isNaN(maximum)) { throw new IllegalArgumentException("Cannot have NaN boundary in outsideRange!"); } return new DerivedBooleanInput(this) { @Override protected boolean apply() { float value = FloatInput.this.get(); return value < minimum || value > maximum; } }; } /** * Provides a BooleanInput that is true iff the value of this FloatInput is * outside the range of the value of <code>minimum</code> to the value of * <code>maximum</code>. If it is equal to the value of <code>minimum</code> * or the value of <code>maximum</code>, that counts as within the range and * so the value will be false. * * @param minimum the lower bound. * @param maximum the upper bound. * @return the comparison result as a BooleanInput. */ @SetupPhase public default BooleanInput outsideRange(final FloatInput minimum, final FloatInput maximum) { return new DerivedBooleanInput(this) { @Override protected boolean apply() { float value = FloatInput.this.get(); return value < minimum.get() || value > maximum.get(); } }; } /** * Provides a BooleanInput that is true iff the value of this FloatInput is * inside the range of <code>minimum</code> to <code>maximum</code>. If it * is equal to <code>minimum</code> or <code>maximum</code>, that counts as * within the range and so the value will be true. * * @param minimum the lower bound. * @param maximum the upper bound. * @return the comparison result as a BooleanInput. */ @SetupPhase public default BooleanInput inRange(final float minimum, final float maximum) { if (Float.isNaN(minimum) || Float.isNaN(maximum)) { throw new IllegalArgumentException("Cannot have NaN boundary in inRange!"); } return new DerivedBooleanInput(this) { @Override public boolean apply() { float val = FloatInput.this.get(); return val >= minimum && val <= maximum; } }; } /** * Provides a BooleanInput that is true iff the value of this FloatInput is * inside the range of the value of <code>minimum</code> to the value of * <code>maximum</code>. If it is equal to the value of <code>minimum</code> * or the value of <code>maximum</code>, that counts as within the range and * so the value will be true. * * @param minimum the lower bound. * @param maximum the upper bound. * @return the comparison result as a BooleanInput. */ @SetupPhase public default BooleanInput inRange(final FloatInput minimum, final FloatInput maximum) { return new DerivedBooleanInput(this, minimum, maximum) { @Override public boolean apply() { float val = FloatInput.this.get(); return val >= minimum.get() && val <= maximum.get(); } }; } /** * Provides a FloatInput whose value is this FloatInput's value, but * negated. * * @return the negated version of this FloatInput. */ @SetupPhase public default FloatInput negated() { return FloatFilter.negate.wrap(this); } /** * Provides an EventInput that is fired whenever this FloatInput changes by * any amount. * * @return the derived EventInput. */ @SetupPhase public default EventInput onChange() { return new DerivedEventInput(this) { @Override protected boolean shouldProduce() { return true; } }; } /** * Returns an EventInput that fires whenever the value changes by at least * the specified amount, relative to the value the last time it changed. * * Note that this means that if the value changes by eight at once and the * specified value is four, the input will only fire once. * * @param magnitude the nonnegative and finite number that acts as the * threshold for whether or not the event should fire. * @return the EventInput to fire. * @throws IllegalArgumentException if magnitude is negative, infinite, or * NaN. */ @SetupPhase public default EventInput onChangeBy(float magnitude) throws IllegalArgumentException { if (!Float.isFinite(magnitude) || magnitude < 0) { throw new IllegalArgumentException("delta must be nonnegative and finite, but was: " + magnitude); } final float deltaAbs = Math.abs(magnitude); return new DerivedEventInput(this) { float last = get(); @Override protected boolean shouldProduce() { float value = get(); if (Math.abs(last - value) >= deltaAbs) { last = value; return true; } return false; } }; } /** * Provides a version of this FloatInput with a deadzone: if the value is * within <code>deadzone</code> of zero, then the result will be zero. * Otherwise, the value will be unchanged. * * @param deadzone the size of the deadzone to apply. * @return the deadzoned version of this FloatInput. */ @SetupPhase public default FloatInput deadzone(float deadzone) { return FloatFilter.deadzone(deadzone).wrap(this); } /** * Provides a translated and scaled version of this FloatInput such that the * result will be zero if this FloatInput is equal to <code>zeroV</code>, * the result will be one if this FloatInput is equal to <code>oneV</code>, * and otherwise the result is interpolated linearly between those points. * * @param zeroV the value that should be converted to zero. * @param oneV the value that should be converted to one. * @return the translated and scaled version of this FloatInput. * @throws IllegalArgumentException if either of zeroV or oneV are infinite * or NaN, or they are the same, or are far apart enough that their * difference is infinite or NaN. */ @SetupPhase public default FloatInput normalize(float zeroV, float oneV) throws IllegalArgumentException { if (!Float.isFinite(zeroV) || !Float.isFinite(oneV)) { throw new IllegalArgumentException("Infinite or NaN bound to normalize: " + zeroV + ", " + oneV); } if (zeroV == oneV) { throw new IllegalArgumentException("Equal zero and one bounds to normalize: " + zeroV); } final float range = oneV - zeroV; if (!Float.isFinite(range)) { throw new IllegalArgumentException("normalize range is large enough to provide invalid results: " + zeroV + ", " + oneV); } FloatInput original = this; return new DerivedFloatInput(original) { @Override protected float apply() { return (original.get() - zeroV) / range; } }; } /** * Provides a translated and scaled version of this FloatInput such that the * result will be zero if this FloatInput is equal to <code>zeroV</code>, * the result will be one if this FloatInput is equal to the value of * <code>oneV</code>, and otherwise the result is interpolated linearly * between those points. * * @param zeroV the value that should be converted to zero. * @param oneV the input for the value that should be converted to one. * @return the translated and scaled version of this FloatInput. * @throws IllegalArgumentException if zeroV is infinite or NaN. */ @SetupPhase public default FloatInput normalize(final float zeroV, final FloatInput oneV) { if (oneV == null) { throw new NullPointerException(); } if (!Float.isFinite(zeroV)) { throw new IllegalArgumentException("Infinite or NaN zero bound to normalize: " + zeroV); } FloatInput original = this; return new DerivedFloatInput(original, oneV) { @Override protected float apply() { float deltaN = oneV.get() - zeroV; if (deltaN == 0) { return Float.NaN;// as opposed to either infinity or // negative infinity } return (original.get() - zeroV) / deltaN; } }; } /** * Provides a translated and scaled version of this FloatInput such that the * result will be zero if this FloatInput is equal to the value of * <code>zeroV</code>, the result will be one if this FloatInput is equal to * <code>oneV</code>, and otherwise the result is interpolated linearly * between those points. * * @param zeroV the input for the value that should be converted to zero. * @param oneV the value that should be converted to one. * @return the translated and scaled version of this FloatInput. * @throws IllegalArgumentException if oneV is infinite or NaN. */ @SetupPhase public default FloatInput normalize(final FloatInput zeroV, final float oneV) { if (zeroV == null) { throw new NullPointerException(); } if (!Float.isFinite(oneV)) { throw new IllegalArgumentException("Infinite or NaN one bound to normalize: " + oneV); } FloatInput original = this; return new DerivedFloatInput(original, zeroV) { @Override protected float apply() { float zeroN = zeroV.get(), deltaN = oneV - zeroN; if (deltaN == 0) { return Float.NaN;// as opposed to either infinity or // negative infinity } return (original.get() - zeroN) / deltaN; } }; } /** * Provides a translated and scaled version of this FloatInput such that the * result will be zero if this FloatInput is equal to the value of * <code>zeroV</code>, the result will be one if this FloatInput is equal to * the value of <code>oneV</code>, and otherwise the result is interpolated * linearly between those points. * * @param zeroV the input for the value that should be converted to zero. * @param oneV the input for the value that should be converted to one. * @return the translated and scaled version of this FloatInput. */ @SetupPhase public default FloatInput normalize(final FloatInput zeroV, final FloatInput oneV) { if (zeroV == null || oneV == null) { throw new NullPointerException(); } FloatInput original = this; return new DerivedFloatInput(original, zeroV, oneV) { @Override protected float apply() { float zeroN = zeroV.get(), deltaN = oneV.get() - zeroN; if (deltaN == 0) { // as opposed to either infinity or negative infinity return Float.NaN; } return (original.get() - zeroN) / deltaN; } }; } /** * Provides a version of this FloatInput with ramping applied. * * @param limit the maximum delta value per time when * <code>updateWhen</code> is fired. * @param updateWhen when the ramping should update. * @return a ramped version of this FloatInput. */ @SetupPhase public default FloatInput withRamping(final float limit, EventInput updateWhen) { if (updateWhen == null) { throw new NullPointerException(); } FloatCell temp = new FloatCell(); updateWhen.send(this.createRampingEvent(limit, temp)); return temp; } /** * Provides a version of this FloatInput with ramping applied. * * @param limit the maximum delta value per time when * <code>updateWhen</code> is fired. * @param updateWhen when the ramping should update. * @return a ramped version of this FloatInput. */ @SetupPhase public default FloatInput withRamping(final FloatInput limit, EventInput updateWhen) { if (limit == null || updateWhen == null) { throw new NullPointerException(); } FloatCell temp = new FloatCell(); updateWhen.send(this.createRampingEvent(limit, temp)); return temp; } /** * Provides an event that ramps the value of this FloatInput, and sends the * result of the ramping to <code>target</code>. * * @param limit the maximum delta value per time that the event is fired. * @param target the output to control with this ramping. * @return an event that continues ramping. */ @SetupPhase public default EventOutput createRampingEvent(float limit, FloatOutput target) { if (Float.isNaN(limit)) { throw new IllegalArgumentException("Ramping rate cannot be NaN!"); } return createRampingEvent(FloatInput.always(limit), target); } /** * Provides an event that ramps the value of this FloatInput, and sends the * result of the ramping to <code>target</code>. * * @param limit the maximum delta value per time that the event is fired. * @param target the output to control with this ramping. * @return an event that continues ramping. */ @SetupPhase public default EventOutput createRampingEvent(final FloatInput limit, final FloatOutput target) { if (target == null || limit == null) { throw new NullPointerException(); } return new EventOutput() { private float last = get(); @Override public void event() { if (Float.isNaN(last)) { last = get(); } else { last = Utils.updateRamping(last, get(), limit.get()); } target.set(last); } }; } /** * Provides the derivative of this FloatInput as another FloatInput. This * will only update when the current value of this FloatInput changes, and * will be based on the change and on the amount of time that it took. * * @return the derivative of this FloatInput. * @deprecated since this only updates when the value changes, it will * (almost) never actually reach zero! */ @Deprecated @SetupPhase public default FloatInput derivative() { FloatCell out = new FloatCell(); FloatOutput deriv = out.viaDerivative(); onUpdate(() -> deriv.set(get())); return out; } /** * Provides the derivative of this FloatInput as another FloatInput. This * will only update when the current value of this FloatInput changes, and * will be based on the change and on the amount of time that it took. * * <code>millis</code> is the number of milliseconds after which to assume * that the motors have stopped, if no value is received. This must be at * least a bit longer than the update period of the speed sensor, so usually * needs to be higher than 20 milliseconds. 25 milliseconds is a decent * default. * * @param millis the assume-stopped delay. * @return the derivative of this FloatInput. */ @SetupPhase public default FloatInput derivative(int millis) { FloatCell out = new FloatCell(); FloatOutput deriv = out.viaDerivative(); PauseTimer t = new PauseTimer("derivative", FloatInput.always(millis / 1000f)); EventOutput update = t.combine(deriv.eventSet(this)); t.triggerAtEnd(update); onUpdate(update); return out; } /** * Provides a version of this FloatInput whose value only changes when * <code>allow</code> is true. When <code>allow</code> changes to true, the * value is immediately updated and continues to update, and when * <code>allow</code> changes to false, the value is locked. * * @param allow when updating should be allowed. * @return the lockable version of this FloatInput. */ @SetupPhase public default FloatInput filterUpdates(BooleanInput allow) { final FloatInput original = this; return new DerivedFloatInput(this, allow) { private float lastValue = original.get(); @Override public float apply() { if (allow.get()) { lastValue = original.get(); } return lastValue; } }; } /** * Provides a version of this FloatInput whose value only changes when * <code>deny</code> is false. When <code>deny</code> changes to false, the * value is immediately updated and continues to update, and when * <code>deny</code> changes to true, the value is locked. * * @param deny when updating should be disallowed. * @return the lockable version of this FloatInput. */ @SetupPhase public default FloatInput filterUpdatesNot(BooleanInput deny) { final FloatInput original = this; return new DerivedFloatInput(this, deny) { private float lastValue = original.get(); @Override public float apply() { if (!deny.get()) { lastValue = original.get(); } return lastValue; } }; } /** * Provides a FloatInput that has the same value as this FloatInput, or its * negation. If <code>negate</code> is true, it is negated, and if * <code>negate</code> is false, it is not negated. * * @param negate whether or not the input should be negated * @return the possibly negated version of this FloatInput */ @SetupPhase public default FloatInput negatedIf(BooleanInput negate) { return negate.toFloat(this, this.negated()); } /** * Provides a FloatInput whose value is this FloatInput's value, but always * positive, or in other words an absolute value. * * @return the absolute value version of this FloatInput. */ @SetupPhase public default FloatInput absolute() { return FloatFilter.absolute.wrap(this); } // TODO: integrals! }