/* * 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; } }