/* * Copyright 2015-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.ctrl; import ccre.channel.BooleanInput; import ccre.channel.DerivedBooleanInput; import ccre.channel.DerivedFloatInput; import ccre.channel.EventCell; import ccre.channel.EventInput; import ccre.channel.EventOutput; import ccre.channel.FloatInput; import ccre.channel.UpdatingInput; import ccre.log.LogLevel; import ccre.log.Logger; import ccre.verifier.FlowPhase; import ccre.verifier.IgnoredPhase; import ccre.verifier.SetupPhase; /** * A finite-state machine. This has a number of named, predefined states, which * can be switched between, and can affect the functionality of other parts of * the code. * * Users can be notified when a state is exited or entered, and can switch * between states either regarding or disregarding the current state. They can * also, of course, determine the current state. * * @author skeggsc */ public class StateMachine { private int currentState; private final int numberOfStates; private final EventCell onExit = new EventCell(); private final EventCell onEnter = new EventCell(); private final String[] stateNames; /** * Create a new StateMachine with a named defaultState and a list of state * names. The names cannot be null or duplicates. * * @param defaultState the state to initially be in. * @param names the names of the states. */ public StateMachine(String defaultState, String... names) { checkNamesConsistency(names); this.stateNames = names; numberOfStates = names.length; setState(defaultState); } /** * Create a new StateMachine with an indexed defaultState and a list of * state names. * * @param defaultState the state to initially be in, as an index in the list * of names. * @param names the names of the states. */ public StateMachine(int defaultState, String... names) { checkNamesConsistency(names); this.stateNames = names; numberOfStates = names.length; setState(defaultState); } /** * @return the number of states */ public int getNumberOfStates() { return numberOfStates; } @SetupPhase private static void checkNamesConsistency(String... names) { for (int i = 0; i < names.length; i++) { String name = names[i]; if (name == null) { throw new NullPointerException(); } for (int j = i + 1; j < names.length; j++) { if (name.equals(names[j])) { throw new IllegalArgumentException("Duplicate state name: " + names[i]); } } } } @IgnoredPhase private int indexOfName(String state) { for (int i = 0; i < getNumberOfStates(); i++) { if (state.equals(stateNames[i])) { return i; } } throw new IllegalArgumentException("State name not found: " + state); } /** * Set the state of this machine to the named state. * * @param state the state to change to. */ @FlowPhase public void setState(String state) { setState(indexOfName(state)); } /** * Set the state of this machine to the indexed state. * * @param state the state to change to, as an index in the list of state * names. */ @FlowPhase public void setState(int state) { if (state < 0 || state >= getNumberOfStates()) { throw new IllegalArgumentException("Invalid state ID: " + state); } if (state == currentState) { return; } onExit.safeEvent(); currentState = state; onEnter.safeEvent(); } /** * Change to the named state when the event occurs. * * @param state the state to change to. * @param when when to change state. */ public void setStateWhen(String state, EventInput when) { setStateWhen(indexOfName(state), when); } /** * Change to the indexed state when the event occurs. * * @param state the state to change to, as an index in the list of state * names. * @param when when to change state. */ public void setStateWhen(int state, EventInput when) { when.send(getStateSetEvent(state)); } /** * Get an event that will change the state to the named state. * * @param state the state to change to. * @return the event that changes state. */ public EventOutput getStateSetEvent(String state) { return getStateSetEvent(indexOfName(state)); } /** * Get an event that will change the state to the indexed state. * * @param state the state to change to, as an index in the list of state * names. * @return the event that changes state. */ public EventOutput getStateSetEvent(final int state) { if (state < 0 || state >= getNumberOfStates()) { throw new IllegalArgumentException("Invalid state ID: " + state); } return () -> { if (state != currentState) { onExit.safeEvent(); currentState = state; onEnter.safeEvent(); } }; } /** * Get the current state. * * @return the index of the current state. */ public int getState() { return currentState; } /** * Get the name of the current state. * * @return the name of the current state. */ @FlowPhase public String getStateName() { return stateNames[currentState]; } /** * Get the name of the indexed state. * * @param state the state to look up, as an index in the list of state * names. * @return the name of the indexed state. */ public String getStateName(int state) { if (state < 0 || state >= getNumberOfStates()) { throw new IllegalArgumentException("Invalid state ID: " + state); } return stateNames[state]; } /** * Check if the machine is in the named state. * * @param state the state to check. * @return if this machine is in that state. */ public boolean isState(String state) { return isState(indexOfName(state)); } /** * Check if the machine is in the indexed state. * * @param state the state to check, as an index in the list of state names. * @return if this machine is in that state. */ public boolean isState(int state) { if (state < 0 || state >= numberOfStates) { throw new IllegalArgumentException("State out of range: " + state); } return currentState == state; } /** * Return an input representing if the machine is in the named state. * * @param state the state to check. * @return an input for if this machine is in that state. */ public BooleanInput getIsState(String state) { return getIsState(indexOfName(state)); } /** * Return an input representing if the machine is in the indexed state. * * @param state the state to check, as an index in the list of state names. * @return an input for if this machine is in that state. */ public BooleanInput getIsState(int state) { if (state < 0 || state >= getNumberOfStates()) { throw new IllegalArgumentException("Invalid state ID: " + state); } return new DerivedBooleanInput(onEnter) { @Override protected boolean apply() { return currentState == state; } }; } /** * Return an event that moves the machine to the target state if it is in * the source state. * * @param fromState the source state. * @param toState the target state. * @return the event to conditionally change the machine's state. */ public EventOutput getStateTransitionEvent(String fromState, String toState) { return getStateTransitionEvent(indexOfName(fromState), indexOfName(toState)); } /** * Return an event that moves the machine to the target state if it is in * the source state. * * @param fromState the source state, as an index in the list of state * names. * @param toState the target state, as an index in the list of state names. * @return the event to conditionally change the machine's state. */ public EventOutput getStateTransitionEvent(int fromState, int toState) { return getStateSetEvent(toState).filter(getIsState(fromState)); } /** * When the event occurs, move this machine to the target state if it is in * the source state. * * @param fromState the source state. * @param toState the target state. * @param when when to change state. */ public void transitionStateWhen(String fromState, String toState, EventInput when) { transitionStateWhen(indexOfName(fromState), indexOfName(toState), when); } /** * When the event occurs, move this machine to the target state if it is in * the source state. * * @param fromState the source state, as an index in the list of state * names. * @param toState the target state, as an index in the list of state names. * @param when when to change state. */ public void transitionStateWhen(int fromState, int toState, EventInput when) { when.send(getStateTransitionEvent(fromState, toState)); } /** * Whenever the state changes, log a message constructed from the prefix * concatenated with the name of the current state. * * No space is inserted automatically - include that in the prefix. * * @param level the logging level at which to log the message. * @param prefix the prefix of the message to log. */ public void autologTransitions(final LogLevel level, final String prefix) { onEnter.send(new EventOutput() { @Override public void event() { Logger.log(level, prefix + getStateName()); } }); } /** * Get an event that will fire whenever a new state is entered. * * @return the event input. */ public EventInput getStateEnterEvent() { return onEnter; } /** * Fire output whenever a new state is entered. * * @param output the event to fire. */ public void onStateEnter(EventOutput output) { onEnter.send(output); } /** * Get an event that will fire when the named state is entered. * * @param state the state to monitor. * @return the event input. */ public EventInput onEnterState(String state) { return onEnterState(indexOfName(state)); } /** * Get an event that will fire when the indexed state is entered. * * @param state the state to monitor, as an index in the list of state * names. * @return the event input. */ public EventInput onEnterState(int state) { final EventCell out = new EventCell(); onEnterState(state, out); return out; } /** * Fire output when the named state is entered. * * @param state the state to monitor. * @param output the event to fire. */ public void onEnterState(String state, final EventOutput output) { onEnterState(indexOfName(state), output); } /** * Fire output when the indexed state is entered. * * @param state the state to monitor, as an index in the list of state * names. * @param output the event to fire. */ public void onEnterState(int state, final EventOutput output) { onEnter.send(output.filter(getIsState(state))); } /** * Get an event that will fire whenever a state is exited, and before the * next state is entered. * * @return the event input. */ public EventInput getStateExitEvent() { return onExit; } /** * Fire output whenever a state is exited. * * @param output the output to fire. */ public void onStateExit(EventOutput output) { onExit.send(output); } /** * Get an event that will fire when the named state is exited. * * @param state the state to monitor. * @return the event input. */ public EventInput onExitState(String state) { return onExitState(indexOfName(state)); } /** * Get an event that will fire when the indexed state is exited. * * @param state the state to monitor, as an index in the list of state * names. * @return the event input. */ public EventInput onExitState(int state) { final EventCell out = new EventCell(); onExitState(state, out); return out; } /** * Fire output when the named state is exited. * * @param state the state to monitor. * @param output the event to fire. */ public void onExitState(String state, final EventOutput output) { onExitState(indexOfName(state), output); } /** * Fire output when the indexed state is exited. * * @param state the state to monitor, as an index in the list of state * names. * @param output the event to fire. */ public void onExitState(int state, final EventOutput output) { onExit.send(output.filter(getIsState(state))); } /** * Provides an EventInput dynamically selected from a set of EventInputs * based on the current state. You must pass exactly one EventInput per * state, in the order specified in the constructor. * * @param inputs the EventInputs to select from. * @return the selected EventInput. */ public EventInput selectByState(EventInput... inputs) { if (inputs.length != numberOfStates) { throw new IllegalArgumentException("Wrong number of states in call to selectByState!"); } EventCell occur = new EventCell(); for (int i = 0; i < inputs.length; i++) { final int state = i; inputs[i].send(() -> { if (currentState == state) { occur.event(); } }); } return occur; } /** * Provides a BooleanInput dynamically selected from a set of BooleanInputs * based on the current state. You must pass exactly one BooleanInput per * state, in the order specified in the constructor. * * @param inputs the BooleanInputs to select from. * @return the selected BooleanInput. */ public BooleanInput selectByState(BooleanInput... inputs) { if (inputs.length != numberOfStates) { throw new IllegalArgumentException("Wrong number of states in call to selectByState!"); } UpdatingInput[] ins = new UpdatingInput[inputs.length + 1]; System.arraycopy(inputs, 0, ins, 1, inputs.length); ins[0] = this.onEnter; return new DerivedBooleanInput(ins) { @Override protected boolean apply() { return inputs[currentState].get(); } }; } /** * Provides an FloatInput dynamically selected from a set of FloatInputs * based on the current state. You must pass exactly one FloatInput per * state, in the order specified in the constructor. * * @param inputs the FloatInputs to select from. * @return the selected FloatInput. */ public FloatInput selectByState(FloatInput... inputs) { if (inputs.length != numberOfStates) { throw new IllegalArgumentException("Wrong number of states in call to selectByState!"); } UpdatingInput[] ins = new UpdatingInput[inputs.length + 1]; System.arraycopy(inputs, 0, ins, 1, inputs.length); ins[0] = this.onEnter; return new DerivedFloatInput(ins) { @Override protected float apply() { return inputs[currentState].get(); } }; } }