/* * 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.cluck; import java.io.IOException; import java.io.ObjectInputStream; import java.io.OutputStream; import java.io.Serializable; import java.util.concurrent.CopyOnWriteArrayList; import ccre.channel.BooleanCell; import ccre.channel.BooleanIO; import ccre.channel.BooleanInput; import ccre.channel.BooleanOutput; import ccre.channel.CancelOutput; import ccre.channel.EventCell; import ccre.channel.EventIO; import ccre.channel.EventInput; import ccre.channel.EventOutput; import ccre.channel.FloatCell; import ccre.channel.FloatIO; import ccre.channel.FloatInput; import ccre.channel.FloatOutput; import ccre.cluck.rpc.RemoteProcedure; import ccre.cluck.rpc.SimpleProcedure; import ccre.log.LogLevel; import ccre.log.Logger; import ccre.log.LoggingTarget; import ccre.rconf.RConf; import ccre.rconf.RConf.Entry; import ccre.rconf.RConfable; import ccre.util.UniqueIds; import ccre.util.Utils; import ccre.verifier.SetupPhase; /** * A file that handles publishing and subscribing of basic channels. * * @author skeggsc */ public class CluckPublisher { // TODO: publishR? For detachable publishes? // TODO: reorder the members of this class? /** * Start a search process on the specified network. This will tell the * listener each time a new remote is discovered, and the EventOutput * returned from this method will cause the network to be rechecked for new * elements. * * Note that unlike the previous implementation, this implementation will * not send a checking message until the EventOutput is fired. * * @param node the network to search. * @param listener the listener to tell about new nodes. * @return the EventOutput that rechecks the network. */ @SetupPhase public static EventOutput setupSearching(final CluckNode node, final CluckRemoteListener listener) { final String local = UniqueIds.global.nextHexId("search-"); new CluckSubscriber(node) { @Override protected void receive(String source, byte[] data) { if (data.length == 2 && data[0] == CluckConstants.RMT_PING) { listener.handle(source, data[1]); } } @Override protected void receiveBroadcast(String source, byte[] data) { } }.attach(local); return () -> node.broadcast(local, new byte[] { CluckConstants.RMT_PING }, null); } /** * Publish an EventOutput on the network. * * @param node The node to publish on. * @param name The name for the EventOutput. * @param consumer The EventOutput. */ @SetupPhase public static void publish(final CluckNode node, String name, final EventOutput consumer) { if (consumer == null) { throw new NullPointerException(); } new CluckRMTSubscriber(node, CluckConstants.RMT_EVENTOUTP) { @Override protected void receiveValid(String source, byte[] data) { consumer.event(); } }.attach(name); } /** * Subscribe to an EventOutput from the network at the specified path. * * @param node The node to subscribe from. * @param path The path to subscribe to. * @return the EventOutput. */ @SetupPhase public static EventOutput subscribeEO(final CluckNode node, final String path) { if (node == null || path == null) { throw new NullPointerException(); } return () -> node.transmit(path, null, new byte[] { CluckConstants.RMT_EVENTOUTP }); } /** * Publish an EventInput on the network. * * @param node The node to publish on. * @param name The name for the EventInput. * @param input The EventInput. */ @SetupPhase public static void publish(final CluckNode node, final String name, EventInput input) { final CopyOnWriteArrayList<String> remotes = new CopyOnWriteArrayList<String>(); input.send(() -> { for (String remote : remotes) { node.transmit(remote, name, new byte[] { CluckConstants.RMT_EVENTINPUTRESP }); } }); new CluckSubscriber(node) { @Override protected void receive(String src, byte[] data) { if (data.length != 0 && (data[0] == CluckConstants.RMT_NEGATIVE_ACK || data[0] == CluckConstants.RMT_LEGACY_EVENTINPUT_UNSUB)) { if (remotes.remove(src)) { Logger.warning("Connection cancelled to " + src + " on " + name); } else { Logger.warning("Received cancellation to nonexistent " + src + " on " + name); } } else if (requireRMT(src, data, CluckConstants.RMT_EVENTINPUT) && !remotes.contains(src)) { remotes.add(src); } } @Override protected void receiveBroadcast(String source, byte[] data) { defaultBroadcastHandle(source, data, CluckConstants.RMT_EVENTINPUT); } }.attach(name); } /** * Subscribe to an EventInput from the network at the specified path. * * @param node The node to subscribe from. * @param path The path to subscribe to. * @return the EventInput. */ @SetupPhase public static EventInput subscribeEI(final CluckNode node, final String path) { if (node == null || path == null) { throw new NullPointerException(); } final SubscribedEventInput result = new SubscribedEventInput(node, path); new EventInputReceiver(node, result, path).attach(); return result; } /** * Publish a LoggingTarget on the network. * * @param node The node to publish on. * @param name The name for the LoggingTarget. * @param lt The LoggingTarget. */ @SetupPhase public static void publish(final CluckNode node, String name, final LoggingTarget lt) { if (lt == null) { throw new NullPointerException(); } new CluckRMTSubscriber(node, CluckConstants.RMT_LOGTARGET, 10) { @Override protected void receiveValid(String source, byte[] data) { int len1 = Utils.bytesToInt(data, 2); int len2 = Utils.bytesToInt(data, 6); if (len1 + len2 + 10 != data.length) { Logger.warning("Bad data length to Logging Target!"); return; } String message = Utils.fromBytes(data, 10, len1); String extended = len2 == 0 ? null : Utils.fromBytes(data, 10 + len1, len2); lt.log(LogLevel.fromByte(data[1]), message, extended); } }.attach(name); } /** * Subscribe to a LoggingTarget from the network at the specified path, with * only sending data for at least a minimum logging level. * * Note that, if you send an empty string as an 'extra' to the logging * target, it will be replaced with null. * * @param node The node to subscribe from. * @param path The path to subscribe to. * @return the LoggingTarget. */ @SetupPhase public static LoggingTarget subscribeLT(final CluckNode node, final String path) { if (node == null || path == null) { throw new NullPointerException(); } return new SubscribedLoggingTarget(node, path); } /** * Publish a BooleanInput on the network. This will send values to clients * when they connect. * * @param node The node to publish on. * @param name The name for the BooleanInput. * @param input The BooleanInput. */ @SetupPhase public static void publish(final CluckNode node, final String name, final BooleanInput input) { final CopyOnWriteArrayList<String> remotes = new CopyOnWriteArrayList<String>(); input.send(new BooleanInputPublishListener(name, remotes, node)); new CluckSubscriber(node) { @Override protected void receive(String src, byte[] data) { if (data.length != 0 && (data[0] == CluckConstants.RMT_NEGATIVE_ACK || data[0] == CluckConstants.RMT_LEGACY_BOOLINPUT_UNSUB)) { if (remotes.remove(src)) { Logger.warning("Connection cancelled to " + src + " on " + name); } else { Logger.warning("Received cancellation to nonexistent " + src + " on " + name); } } else if (requireRMT(src, data, CluckConstants.RMT_BOOLINPUT)) { remotes.add(src); node.transmit(src, name, new byte[] { CluckConstants.RMT_BOOLINPUTRESP, input.get() ? (byte) 1 : 0 }); } } @Override protected void receiveBroadcast(String source, byte[] data) { defaultBroadcastHandle(source, data, CluckConstants.RMT_BOOLINPUT); } }.attach(name); } /** * Subscribe to a BooleanInput from the network at the specified path. * * @param node The node to subscribe from. * @param path The path to subscribe to. * @param subscribeByDefault Should this request the value from the remote * by default, as opposed to waiting until this is needed. If this is false, * then readValue() won't work until you run addTarget(). * @return the BooleanInput. */ @SetupPhase public static BooleanInput subscribeBI(CluckNode node, String path, boolean subscribeByDefault) { if (node == null || path == null) { throw new NullPointerException(); } final SubscribedBooleanInput result = new SubscribedBooleanInput(node, path, subscribeByDefault); new BooleanInputReceiver(node, result, path).attach(); return result; } /** * Publish a BooleanOutput on the network. * * @param node The node to publish on. * @param name The name for the BooleanOutput. * @param output The BooleanOutput. */ @SetupPhase public static void publish(final CluckNode node, String name, final BooleanOutput output) { if (output == null) { throw new NullPointerException(); } new CluckRMTSubscriber(node, CluckConstants.RMT_BOOLOUTP, 2) { @Override protected void receiveValid(String source, byte[] data) { output.set(data[1] != 0); } }.attach(name); } /** * Subscribe to a BooleanOutput from the network at the specified path. * * @param node The node to subscribe from. * @param path The path to subscribe to. * @return the BooleanOutput. */ @SetupPhase public static BooleanOutput subscribeBO(final CluckNode node, final String path) { if (node == null || path == null) { throw new NullPointerException(); } return (b) -> node.transmit(path, null, new byte[] { CluckConstants.RMT_BOOLOUTP, b ? (byte) 1 : 0 }); } /** * Publish a FloatInput on the network. This will send values to clients * when they connect. * * @param node The node to publish on. * @param name The name for the FloatInput. * @param input The FloatInput. */ @SetupPhase public static void publish(final CluckNode node, final String name, final FloatInput input) { final CopyOnWriteArrayList<String> remotes = new CopyOnWriteArrayList<String>(); input.send(new FloatInputPublishListener(remotes, name, node)); new CluckSubscriber(node) { @Override protected void receive(String src, byte[] data) { if (data.length != 0 && (data[0] == CluckConstants.RMT_NEGATIVE_ACK || data[0] == CluckConstants.RMT_LEGACY_FLOATINPUT_UNSUB)) { if (remotes.remove(src)) { Logger.warning("Connection cancelled to " + src + " on " + name); } else { Logger.warning("Received cancellation to nonexistent " + src + " on " + name); } } else if (requireRMT(src, data, CluckConstants.RMT_FLOATINPUT)) { remotes.add(src); int iver = Float.floatToIntBits(input.get()); node.transmit(src, name, new byte[] { CluckConstants.RMT_FLOATINPUTRESP, (byte) (iver >> 24), (byte) (iver >> 16), (byte) (iver >> 8), (byte) iver }); } } @Override protected void receiveBroadcast(String source, byte[] data) { defaultBroadcastHandle(source, data, CluckConstants.RMT_FLOATINPUT); } }.attach(name); } /** * Subscribe to a FloatInput from the network at the specified path. * * @param node The node to subscribe from. * @param path The path to subscribe to. * @param subscribeByDefault Should this request the value from the remote * by default, as opposed to waiting until this is needed. If this is false, * then <code>get()</code> won't work until you run <code>send()</code> or * <code>onUpdate</code>. * @return the FloatInput. */ @SetupPhase public static FloatInput subscribeFI(final CluckNode node, final String path, final boolean subscribeByDefault) { if (node == null || path == null) { throw new NullPointerException(); } final SubscribedFloatInput result = new SubscribedFloatInput(node, path, subscribeByDefault); new FloatInputReceiver(node, result, path).attach(); return result; } /** * Publish a FloatOutput on the network. * * @param node The node to publish on. * @param name The name for the FloatOutput. * @param out The FloatOutput. */ @SetupPhase public static void publish(final CluckNode node, String name, final FloatOutput out) { if (out == null) { throw new NullPointerException(); } new CluckRMTSubscriber(node, CluckConstants.RMT_FLOATOUTP, 5) { @Override protected void receiveValid(String source, byte[] data) { out.set(Utils.bytesToFloat(data, 1)); } }.attach(name); } /** * Subscribe to a FloatOutput from the network at the specified path. * * @param node The node to subscribe from. * @param path The path to subscribe to. * @return the FloatOutput. */ @SetupPhase public static FloatOutput subscribeFO(CluckNode node, String path) { if (node == null || path == null) { throw new NullPointerException(); } return (f) -> { int iver = Float.floatToIntBits(f); node.transmit(path, null, new byte[] { CluckConstants.RMT_FLOATOUTP, (byte) (iver >> 24), (byte) (iver >> 16), (byte) (iver >> 8), (byte) iver }); }; } /** * Publish a FloatIO on the network. * * @param node The node to publish on. * @param name The name for the FloatIO. * @param stat The FloatIO. */ @SetupPhase public static void publish(CluckNode node, String name, FloatIO stat) { publish(node, name + ".input", stat.asInput()); publish(node, name + ".output", stat.asOutput()); } /** * Subscribe to a FloatIO from the network at the specified path. * * @param node The node to subscribe from. * @param path The path to subscribe to. * @param subscribeByDefault Should this request the value from the remote * by default, as opposed to waiting until this is needed. If this is false, * then <code>get()</code> won't work until you run <code>send()</code> or * <code>onUpdate</code>. * @return the FloatIO. */ @SetupPhase public static FloatIO subscribeFIO(CluckNode node, String path, boolean subscribeByDefault) { return FloatIO.compose(subscribeFI(node, path + ".input", subscribeByDefault), subscribeFO(node, path + ".output")); } /** * Publish a BooleanIO on the network. * * @param node The node to publish on. * @param name The name for the BooleanIO. * @param stat The BooleanIO to publish. */ @SetupPhase public static void publish(CluckNode node, String name, BooleanIO stat) { publish(node, name + ".input", stat.asInput()); publish(node, name + ".output", stat.asOutput()); } /** * Subscribe to a BooleanIO from the network at the specified path. * * @param node The node to subscribe from. * @param path The path to subscribe to. * @param subscribeByDefault Should this request the value from the remote * by default, as opposed to waiting until this is needed. If this is false, * then <code>get()</code> won't work until you run <code>send()</code> or * <code>onUpdate</code>. * @return the BooleanIO. */ @SetupPhase public static BooleanIO subscribeBIO(CluckNode node, String path, boolean subscribeByDefault) { return BooleanIO.compose(subscribeBI(node, path + ".input", subscribeByDefault), subscribeBO(node, path + ".output")); } /** * Publish an EventIO on the network. * * @param node The node to publish on. * @param name The name for the EventIO. * @param stat The EventIO to publish. */ @SetupPhase public static void publish(CluckNode node, String name, EventIO stat) { publish(node, name + ".input", stat.asInput()); publish(node, name + ".output", stat.asOutput()); } /** * Subscribe to a EventIO from the network at the specified path. * * @param node The node to subscribe from. * @param path The path to subscribe to. * @return the EventIO. */ @SetupPhase public static EventIO subscribeEIO(CluckNode node, String path) { return EventIO.compose(subscribeEI(node, path + ".input"), subscribeEO(node, path + ".output")); } /** * Publish an OutputStream on the network. * * @param node The node to publish on. * @param name The name for the OutputStream. * @param out The OutputStream. */ @SetupPhase public static void publish(final CluckNode node, String name, final OutputStream out) { if (node == null || name == null || out == null) { throw new NullPointerException(); } new CluckRMTSubscriber(node, CluckConstants.RMT_OUTSTREAM) { @Override protected void receiveValid(String source, byte[] data) { if (data.length > 1) { try { out.write(data, 1, data.length - 1); } catch (IOException ex) { Logger.warning("IO Exception during network transfer!", ex); } } } }.attach(name); } /** * Subscribe to an OutputStream from the network at the specified path. * * @param node The node to subscribe from. * @param path The path to subscribe to. * @return the OutputStream. */ @SetupPhase public static OutputStream subscribeOS(final CluckNode node, final String path) { if (node == null || path == null) { throw new NullPointerException(); } return new SubscribedOutputStream(node, path); } /** * Publish an OutputStream from the network. This returns an OutputStream * that goes to any OutputStreams subscribing to this. * * @param node The node to publish on. * @param name The name for the OutputStream. * @return the OutputStream that goes to the network. */ @SetupPhase public static OutputStream publishOS(final CluckNode node, final String name) { if (node == null || name == null) { throw new NullPointerException(); } final CopyOnWriteArrayList<String> remotes = new CopyOnWriteArrayList<String>(); new CluckSubscriber(node) { @Override protected void receive(String src, byte[] data) { if (data.length != 0 && data[0] == CluckConstants.RMT_NEGATIVE_ACK) { if (remotes.remove(src)) { Logger.warning("Connection cancelled to " + src + " on " + name); } else { Logger.warning("Received cancellation to nonexistent " + src + " on " + name); } } else if (requireRMT(src, data, CluckConstants.RMT_INPUTSTREAM) && !remotes.contains(src)) { remotes.add(src); } } @Override protected void receiveBroadcast(String source, byte[] data) { defaultBroadcastHandle(source, data, CluckConstants.RMT_INPUTSTREAM); } }.attach(name); return new OutputStream() { @Override public void write(int b) throws IOException { for (String remote : remotes) { node.transmit(remote, name, new byte[] { CluckConstants.RMT_OUTSTREAM, (byte) b }); } } @Override public void write(byte[] b, int off, int len) throws IOException { if (len > 0) { byte[] bt = new byte[len + 1]; bt[0] = CluckConstants.RMT_OUTSTREAM; System.arraycopy(b, off, bt, 1, len); for (String remote : remotes) { node.transmit(remote, name, bt); } } }; }; } /** * Subscribe from an OutputStream on the network at the specified path. This * asks the OutputStream at the named output to stream its data to us. * * @param node The node to subscribe from. * @param path The path to subscribe to. * @param output The OutputStream to write to. */ @SetupPhase public static void subscribe(final CluckNode node, final String path, OutputStream output) { if (node == null || path == null || output == null) { throw new NullPointerException(); } String linkName = UniqueIds.global.nextHexId("srcOS"); node.transmit(path, linkName, new byte[] { CluckConstants.RMT_INPUTSTREAM }); new CluckRMTSubscriber(node, CluckConstants.RMT_OUTSTREAM) { @Override protected void receiveValid(String src, byte[] data) { if (!path.equals(src)) { Logger.warning("Bad source to " + linkName + ": " + src + " instead of " + path); } else if (data.length > 1) { try { output.write(data, 1, data.length - 1); } catch (IOException ex) { Logger.warning("IO Exception during network transfer!", ex); } } } @Override protected void receiveBroadcast(String source, byte[] data) { if (data.length == 1 && data[0] == CluckConstants.RMT_NOTIFY) { node.transmit(path, linkName, new byte[] { CluckConstants.RMT_INPUTSTREAM }); } } }.attach(linkName); } /** * Publish an RConfable device on the network. * * @param node The node to publish on. * @param name The name for the RConfable. * @param device The RConfable. */ @SetupPhase public static void publishRConf(CluckNode node, String name, final RConfable device) { node.getRPCManager().publish(name + "-rpcq", new RemoteProcedure() { @Override public void invoke(byte[] in, OutputStream out) { try { RConf.Entry[] data; try { data = device.queryRConf(); } catch (InterruptedException e1) { Thread.currentThread().interrupt(); return; } short count = (short) data.length; if (count != data.length) { Logger.warning("Too many fields in RConf query response!"); count = Short.MAX_VALUE; } try { out.write(new byte[] { (byte) (count >> 8), (byte) count }); for (int i = 0; i < count; i++) { // plus one for the type header int len = data[i].contents.length + 1; out.write(new byte[] { (byte) (len >> 24), (byte) (len >> 16), (byte) (len >> 8), (byte) len, data[i].type }); out.write(data[i].contents); } } catch (IOException e) { Logger.warning("IOException during response to RConf query!", e); } } finally { try { out.close(); } catch (IOException e) { Logger.warning("IOException during RConf close!", e); } } } }); node.getRPCManager().publish(name + "-rpcs", new RemoteProcedure() { @Override public void invoke(byte[] in, OutputStream out) { try { if (in.length < 2) { Logger.warning("Invalid message to RConf signal node!"); return; } byte[] data = new byte[in.length - 2]; System.arraycopy(in, 2, data, 0, data.length); try { boolean success; try { success = device.signalRConf(((in[0] & 0xFF) << 8) | (in[1] & 0xFF), data); } catch (InterruptedException ex) { throw ex; } catch (Throwable thr) { Logger.severe("RConf signal failed", thr); success = false; } try { out.write(success ? (byte) 1 : (byte) 0); } catch (IOException e) { Logger.warning("IOException during response to RConf signal!", e); } } catch (InterruptedException e1) { Thread.currentThread().interrupt(); } } finally { try { out.close(); } catch (IOException e) { Logger.warning("IOException during response to RConf signal!", e); } } } }); } /** * Subscribe to an RConfable device from the network at the specified path. * * @param node The node to subscribe from. * @param path The path to subscribe to. * @param timeout The maximum wait time for the RPC calls. * @return the RConfable. */ @SetupPhase public static RConfable subscribeRConf(CluckNode node, String path, final int timeout) { final RemoteProcedure query = node.getRPCManager().subscribe(path + "-rpcq", timeout); final RemoteProcedure signal = node.getRPCManager().subscribe(path + "-rpcs", timeout); return new SubscribedRConfable(query, timeout, signal); } private CluckPublisher() { } private static final class SubscribedRConfable implements RConfable, Serializable { private static final long serialVersionUID = 6975429475672969797L; private final RemoteProcedure query; private final int timeout; private final RemoteProcedure signal; private SubscribedRConfable(RemoteProcedure query, int timeout, RemoteProcedure signal) { this.query = query; this.timeout = timeout; this.signal = signal; } @Override public boolean signalRConf(int field, byte[] data) throws InterruptedException { byte[] ndata = new byte[data.length + 2]; if (field != (field & 0xFFFF)) { Logger.warning("Out of range field in RConf query response!"); return false; } ndata[0] = (byte) (field >> 8); ndata[1] = (byte) field; System.arraycopy(data, 0, ndata, 2, data.length); byte[] result = SimpleProcedure.invoke(signal, ndata, timeout); if (result == SimpleProcedure.TIMED_OUT) { return false; } else if (result.length >= 1 && result[0] == 0) { return false; } return true; } @Override public Entry[] queryRConf() throws InterruptedException { byte[] data = SimpleProcedure.invoke(query, new byte[0], timeout); if (data == SimpleProcedure.TIMED_OUT) { return null; } if (data.length < 2) { Logger.warning("Too-short (1) RConf query response!"); return null; } Entry[] out = new Entry[((data[0] & 0xFF) << 8) | (data[1] & 0xFF)]; int ptr = 2; for (int i = 0; i < out.length; i++) { if (data.length - ptr < 4) { Logger.warning("Too-short (2) RConf query response!"); return null; } int len = ((data[ptr] & 0xFF) << 24) | ((data[ptr + 1] & 0xFF) << 16) | ((data[ptr + 2] & 0xFF) << 8) | (data[ptr + 3] & 0xFF); byte type = data[ptr + 4]; byte[] part = new byte[len - 1]; ptr += 5; if (data.length - ptr < part.length) { Logger.warning("Too-short (3) RConf query response!"); return null; } System.arraycopy(data, ptr, part, 0, part.length); ptr += part.length; out[i] = new RConf.Entry(type, part); } if (ptr != data.length) { Logger.warning("Too-long RConf query response!"); return null; } return out; } } private static final class FloatInputPublishListener implements FloatOutput { private static final long serialVersionUID = 1432024738866192130L; private final CopyOnWriteArrayList<String> remotes; private final String name; private final CluckNode node; private FloatInputPublishListener(CopyOnWriteArrayList<String> remotes, String name, CluckNode node) { this.remotes = remotes; this.name = name; this.node = node; } @Override public void set(float value) { for (String remote : remotes) { int iver = Float.floatToIntBits(value); node.transmit(remote, name, new byte[] { CluckConstants.RMT_FLOATINPUTRESP, (byte) (iver >> 24), (byte) (iver >> 16), (byte) (iver >> 8), (byte) iver }); } } } private static final class BooleanInputPublishListener implements BooleanOutput { private static final long serialVersionUID = -7563622859541688219L; private final String name; private final CopyOnWriteArrayList<String> remotes; private final CluckNode node; private BooleanInputPublishListener(String name, CopyOnWriteArrayList<String> remotes, CluckNode node) { this.name = name; this.remotes = remotes; this.node = node; } @Override public void set(boolean value) { for (String remote : remotes) { node.transmit(remote, name, new byte[] { CluckConstants.RMT_BOOLINPUTRESP, value ? (byte) 1 : 0 }); } } } private static class SubscribedLoggingTarget implements LoggingTarget, Serializable { private static final long serialVersionUID = 5342629979840268661L; private final CluckNode node; private final String path; SubscribedLoggingTarget(CluckNode node, String path) { this.node = node; this.path = path; } @Override public void log(LogLevel level, String message, Throwable throwable) { log(level, message, Utils.toStringThrowable(throwable)); } private static long lastReportedRemoteLoggingError = 0; @Override public void log(LogLevel level, String message, String extended) { try { byte[] msg = message.getBytes("UTF-8"); byte[] ext = extended == null ? new byte[0] : extended.getBytes("UTF-8"); byte[] out = new byte[10 + msg.length + ext.length]; out[0] = CluckConstants.RMT_LOGTARGET; out[1] = LogLevel.toByte(level); int lm = msg.length; out[2] = (byte) (lm >> 24); out[3] = (byte) (lm >> 16); out[4] = (byte) (lm >> 8); out[5] = (byte) (lm); int le = ext.length; out[6] = (byte) (le >> 24); out[7] = (byte) (le >> 16); out[8] = (byte) (le >> 8); out[9] = (byte) (le); System.arraycopy(msg, 0, out, 10, msg.length); System.arraycopy(ext, 0, out, 10 + msg.length, ext.length); node.transmit(path, null, out); } catch (Throwable thr) { // We use System.currentTimeMillis() instead of // Time.currentTimeMillis() because this is only to prevent // message spam. if (System.currentTimeMillis() - lastReportedRemoteLoggingError > 500) { Logger.severe("[LOCAL] Error during remote log", thr); lastReportedRemoteLoggingError = System.currentTimeMillis(); } } } } private static class SubscribedFloatInput extends FloatCell { private static final long serialVersionUID = 1031666017588055705L; private boolean sent; private final CluckNode node; private final String path; private transient String linkName; private final boolean canUnsubscribe; SubscribedFloatInput(CluckNode node, String path, boolean subscribeByDefault) { super(Float.NaN); this.sent = subscribeByDefault; this.node = node; this.path = path; generateLinkName(); this.canUnsubscribe = !subscribeByDefault; if (subscribeByDefault) { node.transmit(path, linkName, new byte[] { CluckConstants.RMT_FLOATINPUT }); } } @Override public synchronized CancelOutput onUpdate(EventOutput out) { CancelOutput base = super.onUpdate(out); if (!sent) { sent = true; node.transmit(path, linkName, new byte[] { CluckConstants.RMT_FLOATINPUT }); } return base.combine(() -> { synchronized (SubscribedFloatInput.this) { if (canUnsubscribe && sent && !this.hasListeners()) { sent = false; node.transmit(path, linkName, new byte[] { CluckConstants.RMT_NEGATIVE_ACK }); } } }); } private synchronized boolean shouldResend() { return sent; } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); generateLinkName(); new FloatInputReceiver(node, this, path).attach(); } @SetupPhase private void generateLinkName() { linkName = UniqueIds.global.nextHexId("srcFI"); } public String getLinkName() { return linkName; } } private static class FloatInputReceiver extends CluckRMTSubscriber { private final SubscribedFloatInput result; private final String path; private final String linkName; FloatInputReceiver(CluckNode node, SubscribedFloatInput result, String path) { super(node, CluckConstants.RMT_FLOATINPUTRESP, 5); this.result = result; this.path = path; this.linkName = result.getLinkName(); } @Override protected void receiveValid(String src, byte[] data) { if (!path.equals(src)) { Logger.warning("Bad source to " + linkName + ": " + src + " instead of " + path); } else { result.set(Utils.bytesToFloat(data, 1)); } } @Override protected void receiveBroadcast(String source, byte[] data) { if (data.length == 1 && data[0] == CluckConstants.RMT_NOTIFY) { if (result.shouldResend()) { node.transmit(path, linkName, new byte[] { CluckConstants.RMT_FLOATINPUT }); } } } @SetupPhase public void attach() { attach(linkName); } } private static class SubscribedBooleanInput extends BooleanCell { private static final long serialVersionUID = 6685907502662588221L; private boolean sent; private final CluckNode node; private final String path; private transient String linkName; private final boolean canUnsubscribe; SubscribedBooleanInput(CluckNode node, String path, boolean subscribeByDefault) { this.sent = subscribeByDefault; this.node = node; this.path = path; this.canUnsubscribe = !subscribeByDefault; generateLinkName(); if (subscribeByDefault) { node.transmit(path, linkName, new byte[] { CluckConstants.RMT_BOOLINPUT }); } } @Override public synchronized CancelOutput onUpdate(EventOutput cns) { CancelOutput base = super.onUpdate(cns); if (!sent) { sent = true; node.transmit(path, linkName, new byte[] { CluckConstants.RMT_BOOLINPUT }); } return base.combine(() -> { synchronized (SubscribedBooleanInput.this) { if (canUnsubscribe && sent && !this.hasListeners()) { sent = false; node.transmit(path, linkName, new byte[] { CluckConstants.RMT_NEGATIVE_ACK }); } } }); } private synchronized boolean shouldResend() { return sent; } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); generateLinkName(); new BooleanInputReceiver(node, this, path).attach(); } @SetupPhase private void generateLinkName() { linkName = UniqueIds.global.nextHexId("srcBI"); } public String getLinkName() { return linkName; } } private static class BooleanInputReceiver extends CluckRMTSubscriber { private final SubscribedBooleanInput result; private final String path; private final String linkName; BooleanInputReceiver(CluckNode node, SubscribedBooleanInput result, String path) { super(node, CluckConstants.RMT_BOOLINPUTRESP, 2); this.result = result; this.path = path; this.linkName = result.getLinkName(); } @Override protected void receiveValid(String src, byte[] data) { if (!path.equals(src)) { Logger.warning("Bad source to " + linkName + ": " + src + " instead of " + path); } else { result.set(data[1] != 0); } } @Override protected void receiveBroadcast(String source, byte[] data) { if (data.length == 1 && data[0] == CluckConstants.RMT_NOTIFY) { if (result.shouldResend()) { node.transmit(path, linkName, new byte[] { CluckConstants.RMT_BOOLINPUT }); } } } @SetupPhase public void attach() { attach(linkName); } } private static class SubscribedEventInput extends EventCell { private static final long serialVersionUID = -4051785233205840392L; private boolean sent; private final CluckNode node; private final String path; private transient String linkName; SubscribedEventInput(CluckNode node, String path) { this.node = node; this.path = path; generateLinkName(); } @Override public synchronized CancelOutput onUpdate(EventOutput cns) { CancelOutput base = super.onUpdate(cns); if (!sent) { sent = true; node.transmit(path, linkName, new byte[] { CluckConstants.RMT_EVENTINPUT }); } return base.combine(() -> { synchronized (SubscribedEventInput.this) { if (sent && !this.hasListeners()) { sent = false; node.transmit(path, linkName, new byte[] { CluckConstants.RMT_NEGATIVE_ACK }); } } }); } private synchronized boolean shouldResend() { return sent; } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); generateLinkName(); new EventInputReceiver(node, this, path).attach(); } @SetupPhase private void generateLinkName() { this.linkName = UniqueIds.global.nextHexId("srcES"); } public String getLinkName() { return linkName; } } private static class EventInputReceiver extends CluckRMTSubscriber { private final SubscribedEventInput result; private final String path; private final String linkName; EventInputReceiver(CluckNode node, SubscribedEventInput result, String path) { super(node, CluckConstants.RMT_EVENTINPUTRESP); this.result = result; this.path = path; this.linkName = result.getLinkName(); } @Override protected void receiveValid(String src, byte[] data) { if (!path.equals(src)) { Logger.warning("Bad source to " + linkName + ": " + src + " instead of " + path); } else { result.event(); } } @Override protected void receiveBroadcast(String source, byte[] data) { if (data.length == 1 && data[0] == CluckConstants.RMT_NOTIFY) { if (result.shouldResend()) { node.transmit(path, linkName, new byte[] { CluckConstants.RMT_EVENTINPUT }); } } } @SetupPhase public void attach() { this.attach(linkName); } } private static class SubscribedOutputStream extends OutputStream implements Serializable { private static final long serialVersionUID = -9002013295388072459L; private final CluckNode node; private final String path; SubscribedOutputStream(CluckNode node, String path) { this.node = node; this.path = path; } @Override public void write(int b) throws IOException { node.transmit(path, null, new byte[] { CluckConstants.RMT_OUTSTREAM, (byte) b }); } @Override public void write(byte b[], int off, int len) throws IOException { byte[] newbyteout = new byte[len + 1]; newbyteout[0] = CluckConstants.RMT_OUTSTREAM; System.arraycopy(b, off, newbyteout, 1, len); node.transmit(path, null, newbyteout); } } }