diff --git a/src/org/openlcb/implementations/BitProducerConsumer.java b/src/org/openlcb/implementations/BitProducerConsumer.java index 357cc762..5753a893 100644 --- a/src/org/openlcb/implementations/BitProducerConsumer.java +++ b/src/org/openlcb/implementations/BitProducerConsumer.java @@ -27,6 +27,7 @@ public class BitProducerConsumer extends MessageDecoder { private final int flags; public final static EventID nullEvent = new EventID(new byte[]{0, 0, 0, 0, 0, 0, 0, 0}); + //private final static Logger log = Logger.getLogger(VersionedValue.class.getCanonicalName()); /// Flag bit to set default value. (set: true; clear: false). public final static int DEFAULT_TRUE = 1; @@ -83,6 +84,21 @@ public VersionedValue getValue() { return value; } + /** + * Resets the producer/consumer to its default state. This will not change the actual state (also not trigger listeners), but will start reporting unknown state to the network, and enables sending a new query message to the network using @link sendQuery(), assuming the flags are set up for that. + */ + public void resetToDefault() { + value.setVersionToDefault(); + } + + /** + * Sends out query messages to the bus. Useful to be called after resetToDefault(). + */ + public void sendQuery() { + sendMessage(new IdentifyProducersMessage(iface.getNodeId(), eventOn)); + sendMessage(new IdentifyConsumersMessage(iface.getNodeId(), eventOn)); + } + /** * Sends out an event message * @param the message type to send. @@ -98,7 +114,7 @@ void sendMessage(T msg) { * default value passed in. */ public boolean isValueAtDefault() { - return (value.getVersion() == value.DEFAULT_VERSION); + return (value.isVersionAtDefault()); } private EventState getOnEventState() { @@ -135,8 +151,7 @@ private void sendIdentifiedMessages(boolean queryState) { getOffEventState())); } if (queryState) { - sendMessage(new IdentifyProducersMessage(iface.getNodeId(), eventOn)); - sendMessage(new IdentifyConsumersMessage(iface.getNodeId(), eventOn)); + sendQuery(); } } diff --git a/src/org/openlcb/implementations/VersionedValue.java b/src/org/openlcb/implementations/VersionedValue.java index e089743f..b3132c7d 100644 --- a/src/org/openlcb/implementations/VersionedValue.java +++ b/src/org/openlcb/implementations/VersionedValue.java @@ -1,5 +1,7 @@ package org.openlcb.implementations; +import java.util.logging.Logger; + /** * Created by bracz on 12/30/15. */ @@ -8,7 +10,9 @@ public class VersionedValue { int version; int nextVersion; java.beans.PropertyChangeSupport pcs = new java.beans.PropertyChangeSupport(this); - public static int DEFAULT_VERSION = 1; + static int DEFAULT_VERSION = 1; + private int defaultVersion = DEFAULT_VERSION; + private final static Logger log = Logger.getLogger(VersionedValue.class.getCanonicalName()); public VersionedValue(T t) { version = DEFAULT_VERSION; @@ -16,6 +20,9 @@ public VersionedValue(T t) { data = t; } + /** + * @return a strictly monotonically increasing version number to be used for setting the value. + */ public int getNewVersion() { int newVersion; synchronized (this) { @@ -24,17 +31,54 @@ public int getNewVersion() { return newVersion; } + /** + * Resets the state of the listener to construction state, without changing the value. The + * next 'set' will force updates to be called, and isVersionAtDefault will return true. + */ + public synchronized void setVersionToDefault() { + defaultVersion = version; + } + + /** + * @return true if we are currently at the default version (either just after construction or + * due to ra eset to default call). + */ + public synchronized boolean isVersionAtDefault() { + return version == defaultVersion; + } + + /** + * Sets the current value; possibly calling listeners. Handles versioning internally. + * @param t new value of data stored. + */ public void set(T t) { + int version; synchronized(this) { - int version = getNewVersion(); - set(version, t); + version = getNewVersion(); } + set(version, t); } + /** + * Sets the value using a pre-requested version. The set has no effect if the version has + * already been exceeded. + * @param atVersion proposed new version number + * @param t new value of data stored + * @return true if data was updated. False if the version is already outdated and no change + * was made. + */ public boolean setWithForceNotify(int atVersion, T t) { return setInternal(atVersion, t, true); } + /** + * Sets the value using a pre-requested version. The set has no effect if the version has + * already been exceeded. + * @param atVersion proposed new version number + * @param t new value of data stored + * @return true if data was updated. False if the version is already outdated and no change + * was made. + */ public boolean set(int atVersion, T t) { return setInternal(atVersion, t, false); } @@ -49,11 +93,11 @@ private boolean setInternal(int atVersion, T t, boolean forceNotify) { if (nextVersion <= atVersion) { nextVersion = atVersion + 1; } - if (data.equals(t) && oldVersion != DEFAULT_VERSION && !forceNotify) { + if (data.equals(t) && oldVersion != defaultVersion && !forceNotify) { return true; } old = data; - if (oldVersion == DEFAULT_VERSION || forceNotify) { + if (oldVersion == defaultVersion || forceNotify) { old = null; } data = t; @@ -62,10 +106,16 @@ private boolean setInternal(int atVersion, T t, boolean forceNotify) { return true; } + /** + * @return data stored at the current version. + */ public T getLatestData() { return data; } + /** + * @return current version number. + */ public int getVersion() { return version; } diff --git a/test/org/openlcb/implementations/BitProducerConsumerTest.java b/test/org/openlcb/implementations/BitProducerConsumerTest.java index 08e193a8..f1b987fa 100644 --- a/test/org/openlcb/implementations/BitProducerConsumerTest.java +++ b/test/org/openlcb/implementations/BitProducerConsumerTest.java @@ -474,6 +474,93 @@ public void testOneEventNull() throws Exception { } + public void testSendQuery() { + createWithDefaults(); + + pc.sendQuery(); + expectFrame(":X19914333N0504030201000708;"); + expectFrame(":X198F4333N0504030201000708;"); + expectNoFrames(); + } + + public void testResetToDefault() { + createWithDefaults(); + MockVersionedValueListener listener = new MockVersionedValueListener<>(pc + .getValue()); + + // baseline: at default. + sendFrameAndExpectResult( // + ":X198F4444N0504030201000709;", + ":X194C7333N0504030201000709;"); + sendFrameAndExpectResult( // + ":X198F4444N0504030201000708;", + ":X194C7333N0504030201000708;"); + + verifyNoMoreInteractions(listener.stub); + + // Sets to off. Callback comes. + sendFrame(":X195B4444N0504030201000709;"); + + verify(listener.stub).update(false); + verifyNoMoreInteractions(listener.stub); + reset(listener.stub); + // queries return definite state + sendFrameAndExpectResult( // + ":X198F4444N0504030201000709;", + ":X194C4333N0504030201000709;"); + sendFrameAndExpectResult( // + ":X198F4444N0504030201000708;", + ":X194C5333N0504030201000708;"); + // fun starts here + pc.resetToDefault(); + // queries return unknown + sendFrameAndExpectResult( // + ":X198F4444N0504030201000709;", + ":X194C7333N0504030201000709;"); + sendFrameAndExpectResult( // + ":X198F4444N0504030201000708;", + ":X194C7333N0504030201000708;"); + + verifyNoMoreInteractions(listener.stub); + reset(listener.stub); + + // Sets to off with a PCER + sendFrame(":X195B4444N0504030201000709;"); + // queries return definite state + sendFrameAndExpectResult( // + ":X198F4444N0504030201000709;", + ":X194C4333N0504030201000709;"); + sendFrameAndExpectResult( // + ":X198F4444N0504030201000708;", + ":X194C5333N0504030201000708;"); + + verify(listener.stub).update(false); + verifyNoMoreInteractions(listener.stub); + reset(listener.stub); + + pc.resetToDefault(); + // queries return unknown + sendFrameAndExpectResult( // + ":X198F4444N0504030201000709;", + ":X194C7333N0504030201000709;"); + sendFrameAndExpectResult( // + ":X198F4444N0504030201000708;", + ":X194C7333N0504030201000708;"); + // Sets to off with a consumer identified. This usually happens when sendQuery() is + // invoked and the layout responds. We will get a callback too. + sendFrame(":X19545333N0504030201000708;"); + verify(listener.stub).update(false); + verifyNoMoreInteractions(listener.stub); + reset(listener.stub); + // queries return definite state + sendFrameAndExpectResult( // + ":X198F4444N0504030201000709;", + ":X194C4333N0504030201000709;"); + sendFrameAndExpectResult( // + ":X198F4444N0504030201000708;", + ":X194C5333N0504030201000708;"); + } + @Override protected void tearDown() throws Exception { expectNoFrames(); diff --git a/test/org/openlcb/implementations/VersionedValueTest.java b/test/org/openlcb/implementations/VersionedValueTest.java index 943f56de..13be91b1 100644 --- a/test/org/openlcb/implementations/VersionedValueTest.java +++ b/test/org/openlcb/implementations/VersionedValueTest.java @@ -15,12 +15,20 @@ public class VersionedValueTest extends TestCase { public void testDefaultAndSet() throws Exception { assertEquals("default value", Integer.valueOf(42), v.getLatestData()); assertEquals(v.DEFAULT_VERSION, v.getVersion()); + assertTrue(v.isVersionAtDefault()); + assertEquals(42, (int)v.getLatestData()); v.set(13); assertEquals("set value", Integer.valueOf(13), v.getLatestData()); assert(v.DEFAULT_VERSION < v.getVersion()); - } + assertFalse(v.isVersionAtDefault()); + assertEquals(13, (int)v.getLatestData()); + v.setVersionToDefault(); + assert(v.DEFAULT_VERSION < v.getVersion()); + assertTrue(v.isVersionAtDefault()); + assertEquals(13, (int)v.getLatestData()); + } public void testSetWithForceNotify() throws Exception { v.set(33); // gets rid of the default value condition @@ -84,6 +92,33 @@ public void testSet() throws Exception { verifyNoMoreInteractions(l2.stub); verifyNoMoreInteractions(l1.stub); reset(l1.stub, l2.stub); + + // skips duplicate notification + l1.setFromOwner(33); + verifyNoMoreInteractions(l2.stub); + verifyNoMoreInteractions(l1.stub); + reset(l1.stub, l2.stub); + + // After reset to default we will get an update notification. + v.setVersionToDefault(); + l1.setFromOwner(33); + verify(l2.stub).update(33); + verifyNoMoreInteractions(l1.stub); + reset(l1.stub, l2.stub); + + // After reset to default we will get both update notifications. + v.setVersionToDefault(); + v.set(33); + verify(l1.stub).update(33); + verify(l2.stub).update(33); + reset(l1.stub, l2.stub); + + // After reset to default we will get an update notification. + v.setVersionToDefault(); + l1.setFromOwner(42); + verify(l2.stub).update(42); + verifyNoMoreInteractions(l1.stub); + reset(l1.stub, l2.stub); } public void testOutOfOrder() throws Exception {