/*
* 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.binding;
import java.util.HashMap;
import ccre.channel.CancelOutput;
import ccre.channel.EventInput;
import ccre.channel.FloatOutput;
import ccre.cluck.Cluck;
import ccre.log.Logger;
import ccre.rconf.RConf;
import ccre.rconf.RConf.Entry;
import ccre.rconf.RConfable;
import ccre.storage.Storage;
import ccre.storage.StorageSegment;
import ccre.verifier.SetupPhase;
/**
* A CluckControlBinder connects together a ControlBindingDataSource (such as a
* set of Joysticks) to a ControlBindingDataSink (such as your program's
* controls.) It allows for configuration of the linkage over Cluck and allows
* for saving the control binding configuration.
*
* @author skeggsc
*/
public class CluckControlBinder implements RConfable {
private final ControlBindingDataSource sourceSet;
private final ControlBindingDataSink sinkSet;
// From sink to source.
private final HashMap<String, String> boolLinkage = new HashMap<String, String>();
private final HashMap<String, String> floatLinkage = new HashMap<String, String>();
private final HashMap<String, Boolean> floatInverts = new HashMap<String, Boolean>();
private final HashMap<String, CancelOutput> boolUnbinds = new HashMap<String, CancelOutput>();
private final HashMap<String, CancelOutput> floatUnbinds = new HashMap<String, CancelOutput>();
private final String name;
private boolean dirty = false;
private final StorageSegment storage;
/**
* Create a new CluckControlBinder, published with the specified name, that
* binds together the provided source and sink.
*
* If anything is already available on the sink, this will also attempt to
* load the saved configuration.
*
* @param name the name of this binding, used for the StorageSegment name
* and the Cluck link.
* @param source the data source to bind.
* @param sink the data sink to bind.
*/
public CluckControlBinder(String name, ControlBindingDataSource source, ControlBindingDataSink sink) {
this.name = name;
this.sourceSet = source;
this.sinkSet = sink;
storage = Storage.openStorage("Control Bindings: " + name);
if (sink.listBooleans().length != 0 || sink.listFloats().length != 0) {
load();
}
}
/**
* Provide a ControlBindingCreator for the given source, which is
* configurable over Cluck under the specified name.
*
* This will load any saved configuration when the load event is produced.
* This is recommended to be produced exactly once, at the end of
* initialization. The provided RConf interface includes buttons for saving
* and loading.
*
* @param name the name for the CluckControlBinder created as part of this.
* @param source the data source that controls can be assigned from.
* @param load when to load the configuration for this CluckControlBinder.
* @return the ControlBindingCreator that a program can use to provide its
* controls that it wants bound.
*/
@SetupPhase
public static ControlBindingCreator makeCreator(String name, ControlBindingDataSource source, EventInput load) {
ControlBindingDataSinkBuildable sink = new ControlBindingDataSinkBuildable();
final CluckControlBinder binder = new CluckControlBinder(name, source, sink);
binder.publish();
if (load == null) {
throw new IllegalArgumentException("makeCreator expects a 'load' event because, otherwise, it doesn't actually know when to load the settings!");
} else {
load.send(() -> binder.load());
}
return sink;
}
/**
* Publish the RConf interface for this binder under the name
* "[NAME] Control Bindings".
*
* For example, if the name of this CluckControlBinder were "Drive Code",
* the RConf interface would be available under
* "Drive Code Control Bindings".
*
* This is equivalent to <code>publish(name + " Control Bindings");</code>
*
* @see #publish(String)
*/
@SetupPhase
public void publish() {
publish(name + " Control Bindings");
}
/**
* Publish the RConf interface for this binder under the specified link
* name.
*
* The RConf interface includes saving and loading buttons, along with
* buttons to bind any of the control sinks to the currently activated
* control source.
*
* See the published RConf interface for more details.
*
* @param name the link name for this RConf interface.
*/
@SetupPhase
public void publish(String name) {
Cluck.publishRConf(name, this);
}
@Override
public Entry[] queryRConf() throws InterruptedException {
String[] boolSinks = sinkSet.listBooleans();
String[] floatSinks = sinkSet.listFloats();
Entry[] ents = new Entry[floatSinks.length + boolSinks.length + 6 + (floatSinks.length == 0 ? 0 : 1) + (boolSinks.length == 0 ? 0 : 1)];
ents[0] = RConf.title(name);
ents[1] = RConf.string("Click a binding while holding the new button or axis");
ents[2] = RConf.string("Click without holding anything to clear");
ents[3] = dirty ? RConf.button("Save Configuration") : RConf.string("Save Configuration");
ents[4] = dirty ? RConf.button("Load Configuration") : RConf.string("Load Configuration");
ents[5] = RConf.autoRefresh(10000);
int n = 6;
if (boolSinks.length != 0) {
ents[n++] = RConf.title("Buttons:");
for (String sink : boolSinks) {
String source = boolLinkage.get(sink);
ents[n++] = RConf.button(sink + ": " + (source == null ? "unbound" : source));
}
}
if (floatSinks.length != 0) {
ents[n++] = RConf.title("Axes:");
for (String sink : floatSinks) {
String source = floatLinkage.get(sink);
ents[n++] = RConf.button(sink + ": " + (source == null ? "unbound" : source + (floatInverts.get(sink) ? " (inverted)" : "")));
}
}
if (ents.length != n) {
throw new RuntimeException("Oops! Mismatch of RConf array length in CluckControlBinder.");
}
return ents;
}
@Override
public boolean signalRConf(int field, byte[] data) throws InterruptedException {
if (field == 3 && dirty) {
save();
return true;
}
if (field == 4 && dirty) {
try {
load();
} catch (Throwable e) {
Logger.severe("Error while updating controls", e);
return false;
}
return true;
}
String[] boolSinks = sinkSet.listBooleans();
String[] floatSinks = sinkSet.listFloats();
int n = 6;
if (boolSinks.length != 0) {
n++;
for (String sink : boolSinks) {
if (field == n++) {
String source = getActiveBoolSource();
rebindBoolean(sink, source);
dirty = true;
return true;
}
}
}
if (floatSinks.length != 0) {
n++;
for (String sink : floatSinks) {
if (field == n++) {
String source = getActiveFloatSource();
boolean invert = getFloatSourceNegative(source);
rebindFloat(sink, source, invert);
dirty = true;
return true;
}
}
}
return false;
}
@SetupPhase
private void rebindBoolean(String sink, String source) {
CancelOutput unbind = boolUnbinds.get(sink);
if (unbind != null) {
unbind.cancel();
}
if (source == null) {
boolLinkage.remove(sink);
boolUnbinds.remove(sink);
} else {
unbind = sourceSet.getBoolean(source).send(sinkSet.getBoolean(sink));
boolLinkage.put(sink, source);
boolUnbinds.put(sink, unbind);
}
}
@SetupPhase
private void rebindFloat(String sink, String source, boolean invert) {
CancelOutput unbind = floatUnbinds.get(sink);
if (unbind != null) {
unbind.cancel();
}
if (source == null) {
floatInverts.remove(sink);
floatLinkage.remove(sink);
floatUnbinds.remove(sink);
} else {
FloatOutput o = sinkSet.getFloat(sink);
unbind = sourceSet.getFloat(source).send(invert ? o.negate() : o);
floatInverts.put(sink, invert);
floatLinkage.put(sink, source);
floatUnbinds.put(sink, unbind);
}
}
@SetupPhase
private String getActiveBoolSource() {
String found = null;
for (String bin : sourceSet.listBooleans()) {
if (sourceSet.getBoolean(bin).get()) {
if (found != null) {
Logger.warning("More than one active boolean source is pressed: at least '" + found + "' and '" + bin + "'");
return null;
}
found = bin;
}
}
return found;
}
@SetupPhase
private String getActiveFloatSource() {
String found = null;
for (String fin : sourceSet.listFloats()) {
if (Math.abs(sourceSet.getFloat(fin).get()) >= 0.8f) {
if (found != null) {
Logger.warning("More than one active float source is pressed: at least '" + found + "' and '" + fin + "'");
return null;
}
found = fin;
}
}
return found;
}
@SetupPhase
private boolean getFloatSourceNegative(String source) {
return source == null ? false : sourceSet.getFloat(source).get() < 0;
}
@SetupPhase
private void load() {
Logger.config("Loading control bindings for " + this.name);
for (String boolSink : sinkSet.listBooleans()) {
String source = storage.getStringForKey("z" + boolSink);
if (source != null && sourceSet.getBoolean(source) == null) {
Logger.warning("Invalid control binding boolean source: " + source);
} else {
rebindBoolean(boolSink, source);
}
}
for (String floatSink : sinkSet.listFloats()) {
String source = storage.getStringForKey("f" + floatSink);
boolean invert = Boolean.parseBoolean(storage.getStringForKey("!f" + floatSink));
if (source != null && sourceSet.getFloat(source) == null) {
Logger.warning("Invalid control binding float source: " + source);
} else {
rebindFloat(floatSink, source, invert);
}
}
Logger.config("Loaded " + (boolLinkage.size() + floatLinkage.size()) + " of " + (sinkSet.listBooleans().length + sinkSet.listFloats().length) + " control bindings for " + this.name);
dirty = false;
}
@SetupPhase
private void save() {
for (String boolSink : sinkSet.listBooleans()) {
storage.setStringForKey("z" + boolSink, boolLinkage.get(boolSink));
}
for (String floatSink : sinkSet.listFloats()) {
storage.setStringForKey("f" + floatSink, floatLinkage.get(floatSink));
storage.setStringForKey("!f" + floatSink, floatInverts.getOrDefault(floatSink, false).toString());
}
storage.flush();
dirty = false;
}
}