001package jmri.jmrix.openlcb; 002 003import java.util.TimerTask; 004import javax.annotation.CheckReturnValue; 005import javax.annotation.Nonnull; 006import javax.annotation.OverridingMethodsMustInvokeSuper; 007 008import jmri.NamedBean; 009import jmri.Sensor; 010import jmri.implementation.AbstractSensor; 011import jmri.jmrix.can.CanSystemConnectionMemo; 012 013import org.openlcb.EventID; 014import org.openlcb.OlcbInterface; 015import org.openlcb.implementations.BitProducerConsumer; 016import org.openlcb.implementations.EventTable; 017import org.openlcb.implementations.VersionedValueListener; 018 019/** 020 * Extend jmri.AbstractSensor for OpenLCB controls. 021 * 022 * @author Bob Jacobsen Copyright (C) 2008, 2010, 2011 023 */ 024public final class OlcbSensor extends AbstractSensor { 025 026 static final int ON_TIME = 500; // time that sensor is active after being tripped 027 028 OlcbAddress addrActive; // go to active state 029 OlcbAddress addrInactive; // go to inactive state 030 private final OlcbInterface iface; 031 private final CanSystemConnectionMemo memo; 032 033 VersionedValueListener<Boolean> sensorListener; 034 BitProducerConsumer pc; 035 EventTable.EventTableEntryHolder activeEventTableEntryHolder = null; 036 EventTable.EventTableEntryHolder inactiveEventTableEntryHolder = null; 037 private static final boolean DEFAULT_IS_AUTHORITATIVE = true; 038 private static final boolean DEFAULT_LISTEN = true; 039 private static final int PC_DEFAULT_FLAGS = BitProducerConsumer.DEFAULT_FLAGS & 040 (~BitProducerConsumer.LISTEN_INVALID_STATE); 041 042 private TimerTask timerTask; 043 044 public OlcbSensor(String prefix, String address, CanSystemConnectionMemo memo) { 045 super(prefix + "S" + address); 046 this.memo = memo; 047 if (memo != null) { // greatly simplify testing 048 this.iface = memo.get(OlcbInterface.class); 049 } else { 050 this.iface = null; 051 } 052 init(address); 053 } 054 055 /** 056 * Common initialization for both constructors. 057 * <p> 058 * 059 */ 060 private void init(String address) { 061 // build local addresses 062 OlcbAddress a = new OlcbAddress(address, memo); 063 OlcbAddress[] v = a.split(memo); 064 if (v == null) { 065 log.error("Did not find usable system name: {}", address); 066 return; 067 } 068 switch (v.length) { 069 case 1: 070 // momentary sensor 071 addrActive = v[0]; 072 addrInactive = null; 073 break; 074 case 2: 075 addrActive = v[0]; 076 addrInactive = v[1]; 077 break; 078 default: 079 log.error("Can't parse OpenLCB Sensor system name: {}", address); 080 } 081 082 } 083 084 /** 085 * Helper function that will be invoked after construction once the properties have been 086 * loaded. Used specifically for preventing double initialization when loading sensors from 087 * XML. 088 */ 089 void finishLoad() { 090 int flags = PC_DEFAULT_FLAGS; 091 flags = OlcbUtils.overridePCFlagsFromProperties(this, flags); 092 log.debug("Sensor Flags: default {} overridden {} listen bit {}", PC_DEFAULT_FLAGS, flags, 093 BitProducerConsumer.LISTEN_EVENT_IDENTIFIED); 094 disposePc(); 095 if (addrInactive == null) { 096 pc = new BitProducerConsumer(iface, addrActive.toEventID(), BitProducerConsumer.nullEvent, flags); 097 098 sensorListener = new VersionedValueListener<Boolean>(pc.getValue()) { 099 @Override 100 public void update(Boolean value) { 101 setOwnState(value ? Sensor.ACTIVE : Sensor.INACTIVE); 102 if (value) { 103 setTimeout(); 104 } 105 } 106 }; 107 } else { 108 pc = new BitProducerConsumer(iface, addrActive.toEventID(), 109 addrInactive.toEventID(), flags); 110 sensorListener = new VersionedValueListener<Boolean>(pc.getValue()) { 111 @Override 112 public void update(Boolean value) { 113 setOwnState(value ? Sensor.ACTIVE : Sensor.INACTIVE); 114 } 115 }; 116 } 117 activeEventTableEntryHolder = iface.getEventTable().addEvent(addrActive.toEventID(), getEventName(true)); 118 if (addrInactive != null) { 119 inactiveEventTableEntryHolder = iface.getEventTable().addEvent(addrInactive.toEventID(), getEventName(false)); 120 } 121 } 122 123 /** 124 * Computes the display name of a given event to be entered into the Event Table. 125 * @param isActive true for sensor active, false for inactive. 126 * @return user-visible string to represent this event. 127 */ 128 public String getEventName(boolean isActive) { 129 String name = getUserName(); 130 if (name == null) name = mSystemName; 131 String msgName = isActive ? "SensorActiveEventName": "SensorInactiveEventName"; 132 return Bundle.getMessage(msgName, name); 133 } 134 135 public EventID getEventID(boolean isActive) { 136 if (isActive) return addrActive.toEventID(); 137 else return addrInactive.toEventID(); 138 } 139 140 @Override 141 @CheckReturnValue 142 @Nonnull 143 public String getRecommendedToolTip() { 144 return addrActive.toDottedString()+";"+addrInactive.toDottedString(); 145 } 146 147 /** 148 * Updates event table entries when the user name changes. 149 * @param s new user name 150 * @throws NamedBean.BadUserNameException see {@link NamedBean} 151 */ 152 @Override 153 @OverridingMethodsMustInvokeSuper 154 public void setUserName(String s) throws NamedBean.BadUserNameException { 155 super.setUserName(s); 156 if (activeEventTableEntryHolder != null) { 157 activeEventTableEntryHolder.getEntry().updateDescription(getEventName(true)); 158 } 159 if (inactiveEventTableEntryHolder != null) { 160 inactiveEventTableEntryHolder.getEntry().updateDescription(getEventName(false)); 161 } 162 } 163 164 /** 165 * Request an update on status by sending an OpenLCB message. 166 */ 167 @Override 168 public void requestUpdateFromLayout() { 169 if (pc != null) { 170 pc.resetToDefault(); 171 pc.sendQuery(); 172 } 173 } 174 175 /** 176 * User request to set the state, which means that we broadcast that to all 177 * listeners by putting it out on CBUS. In turn, the code in this class 178 * should use setOwnState to handle internal sets and bean notifies. 179 * 180 */ 181 @Override 182 public void setKnownState(int s) { 183 if (s == Sensor.ACTIVE) { 184 sensorListener.setFromOwnerWithForceNotify(true); 185 if (addrInactive == null) { 186 setTimeout(); 187 } 188 } else if (s == Sensor.INACTIVE) { 189 sensorListener.setFromOwnerWithForceNotify(false); 190 } else if (s == Sensor.UNKNOWN) { 191 if (pc != null) { 192 pc.resetToDefault(); 193 } 194 } 195 setOwnState(s); 196 } 197 198 /** 199 * Have sensor return to inactive after delay, used if no inactive event was 200 * specified 201 */ 202 void setTimeout() { 203 timerTask = new java.util.TimerTask() { 204 @Override 205 public void run() { 206 timerTask = null; 207 jmri.util.ThreadingUtil.runOnGUI(() -> setKnownState(Sensor.INACTIVE)); 208 } 209 }; 210 jmri.util.TimerUtil.schedule(timerTask, ON_TIME); 211 } 212 213 /** 214 * Changes how the turnout reacts to inquire state events. With authoritative == false the 215 * state will always be reported as UNKNOWN to the layout when queried. 216 * 217 * @param authoritative whether we should respond true state or unknown to the layout event 218 * state inquiries. 219 */ 220 public void setAuthoritative(boolean authoritative) { 221 boolean recreate = (authoritative != isAuthoritative()) && (pc != null); 222 setProperty(OlcbUtils.PROPERTY_IS_AUTHORITATIVE, authoritative); 223 if (recreate) { 224 finishLoad(); 225 } 226 } 227 228 /** 229 * @return whether this producer/consumer is enabled to return state to the layout upon queries. 230 */ 231 public boolean isAuthoritative() { 232 Boolean value = (Boolean) getProperty(OlcbUtils.PROPERTY_IS_AUTHORITATIVE); 233 if (value != null) { 234 return value; 235 } 236 return DEFAULT_IS_AUTHORITATIVE; 237 } 238 239 @Override 240 public void setProperty(@Nonnull String key, Object value) { 241 Object old = getProperty(key); 242 super.setProperty(key, value); 243 if (value.equals(old)) return; 244 if (pc == null) return; 245 finishLoad(); 246 } 247 248 /** 249 * @return whether this producer/consumer is always listening to state declaration messages. 250 */ 251 public boolean isListeningToStateMessages() { 252 Boolean value = (Boolean) getProperty(OlcbUtils.PROPERTY_LISTEN); 253 if (value != null) { 254 return value; 255 } 256 return DEFAULT_LISTEN; 257 } 258 259 /** 260 * Changes how the turnout reacts to state declaration messages. With listen == true state 261 * declarations will update local state at all times. With listen == false state declarations 262 * will update local state only if local state is unknown. 263 * 264 * @param listen whether we should always listen to state declaration messages. 265 */ 266 public void setListeningToStateMessages(boolean listen) { 267 boolean recreate = (listen != isListeningToStateMessages()) && (pc != null); 268 setProperty(OlcbUtils.PROPERTY_LISTEN, listen); 269 if (recreate) { 270 finishLoad(); 271 } 272 } 273 274 /* 275 * since the events that drive a sensor can be whichever state a user 276 * wants, the order of the event pair determines what is the 'active' state 277 */ 278 @Override 279 public boolean canInvert() { 280 return false; 281 } 282 283 @Override 284 public void dispose() { 285 disposePc(); 286 if (timerTask!=null) timerTask.cancel(); 287 super.dispose(); 288 } 289 290 private void disposePc() { 291 if (sensorListener != null) { 292 sensorListener.release(); 293 sensorListener = null; 294 } 295 if (pc != null) { 296 pc.release(); 297 pc = null; 298 } 299 } 300 301 /** 302 * {@inheritDoc} 303 * 304 * Sorts by decoded EventID(s) 305 */ 306 @CheckReturnValue 307 @Override 308 public int compareSystemNameSuffix(@Nonnull String suffix1, @Nonnull String suffix2, @Nonnull jmri.NamedBean n) { 309 return OlcbAddress.compareSystemNameSuffix(suffix1, suffix2, memo); 310 } 311 312 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(OlcbSensor.class); 313 314}