/*
* 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 java.io.Serializable;
import ccre.log.Logger;
import ccre.time.Time;
import ccre.verifier.FlowPhase;
import ccre.verifier.SetupPhase;
/**
* A FloatOutput is an interface for anything that can be set to an analog
* value.
*
* By convention, most float inputs and outputs have states that range from
* -1.0f to 1.0f.
*
* @see FloatInput
* @author skeggsc
*/
public interface FloatOutput extends Serializable {
/**
* A FloatOutput that goes nowhere. All data sent here is ignored.
*/
FloatOutput ignored = new FloatOutput() {
@Override
public void set(float newValue) {
}
// Important to the functioning of static BooleanOutput.combine
@Override
public FloatOutput combine(FloatOutput other) {
if (other == null) {
throw new NullPointerException();
}
return other;
};
};
/**
* Sets the float value of this output.
*
* By convention, most float inputs and outputs have states that range from
* -1.0f to 1.0f.
*
* If any exception occurs during the propagation of the changes, it will be
* passed on by <code>set</code>.
*
* @param value the new value to send to this output.
* @see #safeSet(float) for a version that catches any errors that occur.
*/
@FlowPhase
public void set(float value);
/**
* Sets the float value of this output.
*
* By convention, most float inputs and outputs have states that range from
* -1.0f to 1.0f.
*
* If any exception occurs during the propagation of the changes,
* <code>safeSet</code> will catch and log it as a
* {@link ccre.log.LogLevel#SEVERE} error.
*
* @param value the new value to send to this output.
* @see #set(float) for a version that throws any errors that occur.
*/
@FlowPhase
public default void safeSet(float value) {
try {
set(value);
} catch (Throwable ex) {
Logger.severe("Error during channel propagation", ex);
}
}
/**
* Provides an EventOutput that sets the value of this FloatOutput to
* <code>value</code>.
*
* @param value the value to use.
* @return an event that sets the value.
*/
@SetupPhase
public default EventOutput eventSet(float value) {
return () -> set(value);
}
/**
* Provides an EventOutput that sets the value of this FloatOutput to the
* value of <code>value</code>.
*
* @param value the input to read the new value from.
* @return an event that sets the value.
*/
@SetupPhase
public default EventOutput eventSet(FloatInput value) {
if (value == null) {
throw new NullPointerException();
}
return () -> set(value.get());
}
/**
* Sets the value of this FloatOutput to <code>value</code> when
* <code>when</code> is fired.
*
* @param value the value to set.
* @param when when to set the value.
*/
@SetupPhase
public default void setWhen(float value, EventInput when) {
if (when == null) {
throw new NullPointerException();
}
when.send(eventSet(value));
}
/**
* Sets the value of this FloatOutput to the value of <code>value</code>
* when <code>when</code> is fired.
*
* @param value the input to read the new value from.
* @param when when to set the value.
*/
@SetupPhase
public default void setWhen(FloatInput value, EventInput when) {
if (value == null || when == null) {
throw new NullPointerException();
}
when.send(eventSet(value));
}
/**
* Provides a FloatOutput that controls both this FloatOutput and
* <code>other</code>. When the new FloatOutput is set to any number, both
* this FloatOutput and <code>other</code> will be set to that value.
*
* If any error occurs during propagation of changes to either EventOutput,
* the other target will still be modified. If both throw exceptions, then
* one of the exceptions will be added as a suppressed exception to the
* other.
*
* @param other the EventOutput to combine this EventOutput with.
* @return the combined EventOutput.
*/
@SetupPhase
public default FloatOutput combine(FloatOutput other) {
if (other == null) {
throw new NullPointerException();
}
FloatOutput original = this;
return value -> {
try {
original.set(value);
} catch (Throwable thr) {
try {
other.set(value);
} catch (Throwable thr2) {
thr.addSuppressed(thr2);
}
throw thr;
}
other.set(value);
};
}
/**
* Combines any number of FloatOutputs into a single FloatOutput, such that
* when the returned FloatOutput is set to a value, all of the FloatOutputs
* are set to that value.
*
* If any error occurs during propagation of values to any FloatOutput, the
* other outputs will still be modified. If multiple outputs throw
* exceptions, then one of them will be chosen arbitrarily and all of the
* others will be added as suppressed exceptions either to the thrown
* exception or to other suppressed expressions.
*
* @param outputs the FloatOutputs to include.
* @return the combined version of the FloatOutputs.
*/
@SetupPhase
public static FloatOutput combine(FloatOutput... outputs) {
// This works without including 'ignored' in the actual data structure
// by having 'ignored' drop itself during combine.
FloatOutput out = ignored;
for (FloatOutput o : outputs) {
out = out.combine(o);
}
return out;
}
/**
* Provide a negated version of this FloatOutput, such that every value is
* negated before being propagated to this FloatOutput.
*
* @return the negated version of this FloatOutput.
*/
@SetupPhase
public default FloatOutput negate() {
return FloatFilter.negate.wrap(this);
}
/**
* Provides a version of this FloatOutput with a deadzone: if a set value is
* within <code>deadzone</code> of zero, then this FloatInput will be set to
* zero. Otherwise, this FloatInput will be set to the original value.
*
* @param deadzone the size of the deadzone to apply.
* @return the deadzoned version of this FloatOutput.
*/
@SetupPhase
public default FloatOutput outputDeadzone(float deadzone) {
return FloatFilter.deadzone(deadzone).wrap(this);
}
/**
* Provides a version of this FloatOutput 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 FloatOutput.
*/
@SetupPhase
public default FloatOutput addRamping(final float limit, EventInput updateWhen) {
if (updateWhen == null) {
throw new NullPointerException();
}
FloatCell temp = new FloatCell();
updateWhen.send(temp.createRampingEvent(limit, this));
return temp;
}
/**
* Sets this FloatOutput to the derivative of the provided FloatOutput. The
* values sent to this FloatOutput will be based on the change in value of
* the provided FloatOutput and the amount of time that it took for the
* value to change.
*
* @return the FloatOutput to take the derivative of.
*/
@SetupPhase
public default FloatOutput viaDerivative() {
FloatOutput original = this;
return new FloatOutput() {
// not zero because then FakeTime might break...
private static final long UNINITIALIZED = -1;
private long lastUpdateNanos = UNINITIALIZED;
private float lastValue = Float.NaN;
@Override
public synchronized void set(float value) {
long timeNanos = Time.currentTimeNanos();
if (lastUpdateNanos == UNINITIALIZED) {
lastValue = value;
lastUpdateNanos = timeNanos;
return;
}
if (lastUpdateNanos == timeNanos) {
return; // extremely unlikely... but just in case.
}
float f = Time.NANOSECONDS_PER_SECOND * (value - lastValue) / (timeNanos - lastUpdateNanos);
lastValue = value;
lastUpdateNanos = timeNanos;
original.set(f);
}
};
}
/**
* Provides a version of this FloatOutput that only propagates changes if
* <code>allow</code> is currently true.
*
* When <code>allow</code> changes to false, the output is locked, and when
* <code>allow</code> changes to true, the output is unlocked and this
* BooleanOutput is set to its last received value.
*
* @param allow when to allow changing of the result.
* @return the lockable version of this FloatOutput.
*/
@SetupPhase
public default FloatOutput filter(BooleanInput allow) {
FloatOutput original = this;
return new FloatOutput() {
private boolean anyValue;
private float lastValue;
{
allow.onPress().send(() -> {
if (anyValue) {
original.set(lastValue);
}
});
}
@Override
public void set(float value) {
lastValue = value;
anyValue = true;
if (allow.get()) {
original.set(value);
}
}
};
}
/**
* Provides a version of this FloatOutput that only propagates changes if
* <code>deny</code> is currently false.
*
* When <code>deny</code> changes to true, the output is locked, and when
* <code>deny</code> changes to false, the output is unlocked and this
* BooleanOutput is set to its last received value.
*
* @param deny when to deny changing of the result.
* @return the lockable version of this FloatOutput.
*/
@SetupPhase
public default FloatOutput filterNot(BooleanInput deny) {
return this.filter(deny.not());
}
/**
* Provides a BooleanOutput that controls this FloatOutput by choosing
* between a value for true and a value for false.
*
* @param off the value for this FloatOutput when the BooleanOutput is
* false.
* @param on the value for this FloatOutput when the BooleanOutput is true.
* @return the BooleanOutput that controls this FloatOutput.
*/
@SetupPhase
public default BooleanOutput fromBoolean(final float off, final float on) {
return fromBoolean(FloatInput.always(off), FloatInput.always(on));
}
/**
* Provides a BooleanOutput that controls this FloatOutput by choosing
* between a value for true and a value for false.
*
* @param off the value for this FloatOutput when the BooleanOutput is
* false.
* @param on the input representing the value for this FloatOutput when the
* BooleanOutput is true.
* @return the BooleanOutput that controls this FloatOutput.
*/
@SetupPhase
public default BooleanOutput fromBoolean(float off, FloatInput on) {
return fromBoolean(FloatInput.always(off), on);
}
/**
* Provides a BooleanOutput that controls this FloatOutput by choosing
* between a value for true and a value for false.
*
* @param off the input representing the value for this FloatOutput when the
* BooleanOutput is false.
* @param on the value for this FloatOutput when the BooleanOutput is true.
* @return the BooleanOutput that controls this FloatOutput.
*/
@SetupPhase
public default BooleanOutput fromBoolean(FloatInput off, float on) {
return fromBoolean(off, FloatInput.always(on));
}
/**
* Provides a BooleanOutput that controls this FloatOutput by choosing
* between a value for true and a value for false.
*
* @param off the input representing the value for this FloatOutput when the
* BooleanOutput is false.
* @param on the input representing the value for this FloatOutput when the
* BooleanOutput is true.
* @return the BooleanOutput that controls this FloatOutput.
*/
@SetupPhase
public default BooleanOutput fromBoolean(FloatInput off, FloatInput on) {
return new BooleanOutput() {
private boolean lastValue, anyValue = false;
{
off.onUpdate(() -> {
if (anyValue && !lastValue) {
update();
}
});
on.onUpdate(() -> {
if (anyValue && lastValue) {
update();
}
});
}
@Override
public synchronized void set(boolean value) {
if (value != lastValue || !anyValue) {
lastValue = value;
anyValue = true;
update();
}
}
@FlowPhase
private void update() {
FloatOutput.this.set(lastValue ? on.get() : off.get());
}
};
}
/**
* Provides a FloatOutput that controls this FloatOutput by choosing between
* an unchanged and a negated version of the values sent to this
* FloatOutput.
*
* @param negate whether or not the output should be negated
* @return the possibly negated version of this FloatOutput
*/
@SetupPhase
public default FloatOutput negateIf(BooleanInput negate) {
final FloatOutput aThis = this;
return new FloatOutput() {
private boolean anyValue;
private float lastValue;
{
negate.send((negate2) -> {
if (anyValue) {
aThis.set(negate2 ? -lastValue : lastValue);
}
});
}
@Override
public void set(float value) {
lastValue = value;
anyValue = true;
aThis.set(negate.get() ? -value : value);
}
};
}
/**
* Returns a FloatIO version of this value. If it is already a FloatIO, it
* will be returned directly. If it is not, a new FloatCell will be created
* around this FloatOutput.
*
* @param default_value the initial value for the returned FloatIO, which
* will also be sent to this FloatOutput immediately
* @return a new IO
*/
@SetupPhase
public default FloatIO cell(float default_value) {
FloatIO fio = new FloatCell(default_value);
fio.send(this);
return fio;
}
}