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 { 138 if (addrInactive != null) { 139 return addrInactive.toEventID(); 140 } else { 141 return null; 142 } 143 } 144 } 145 146 @Override 147 @CheckReturnValue 148 @Nonnull 149 public String getRecommendedToolTip() { 150 // Some events are not pairs 151 if (addrInactive == null) { 152 return addrActive.toDottedString(); 153 } 154 return addrActive.toDottedString()+";"+addrInactive.toDottedString(); 155 } 156 157 /** 158 * Updates event table entries when the user name changes. 159 * @param s new user name 160 * @throws NamedBean.BadUserNameException see {@link NamedBean} 161 */ 162 @Override 163 @OverridingMethodsMustInvokeSuper 164 public void setUserName(String s) throws NamedBean.BadUserNameException { 165 super.setUserName(s); 166 if (activeEventTableEntryHolder != null) { 167 activeEventTableEntryHolder.getEntry().updateDescription(getEventName(true)); 168 } 169 if (inactiveEventTableEntryHolder != null) { 170 inactiveEventTableEntryHolder.getEntry().updateDescription(getEventName(false)); 171 } 172 } 173 174 /** 175 * Request an update on status by sending an OpenLCB message. 176 */ 177 @Override 178 public void requestUpdateFromLayout() { 179 if (pc != null) { 180 pc.resetToDefault(); 181 pc.sendQuery(); 182 } 183 } 184 185 /** 186 * User request to set the state, which means that we broadcast that to all 187 * listeners by putting it out on CBUS. In turn, the code in this class 188 * should use setOwnState to handle internal sets and bean notifies. 189 * 190 */ 191 @Override 192 public void setKnownState(int s) { 193 if (s == Sensor.ACTIVE) { 194 sensorListener.setFromOwnerWithForceNotify(true); 195 if (addrInactive == null) { 196 setTimeout(); 197 } 198 } else if (s == Sensor.INACTIVE) { 199 sensorListener.setFromOwnerWithForceNotify(false); 200 } else if (s == Sensor.UNKNOWN) { 201 if (pc != null) { 202 pc.resetToDefault(); 203 } 204 } 205 setOwnState(s); 206 } 207 208 /** 209 * Have sensor return to inactive after delay, used if no inactive event was 210 * specified 211 */ 212 void setTimeout() { 213 timerTask = new java.util.TimerTask() { 214 @Override 215 public void run() { 216 timerTask = null; 217 jmri.util.ThreadingUtil.runOnGUI(() -> setKnownState(Sensor.INACTIVE)); 218 } 219 }; 220 jmri.util.TimerUtil.schedule(timerTask, ON_TIME); 221 } 222 223 /** 224 * Changes how the sensor reacts to inquire state events. With authoritative == false the 225 * state will always be reported as UNKNOWN to the layout when queried. 226 * 227 * @param authoritative whether we should respond true state or unknown to the layout event 228 * state inquiries. 229 */ 230 public void setAuthoritative(boolean authoritative) { 231 boolean recreate = (authoritative != isAuthoritative()) && (pc != null); 232 setProperty(OlcbUtils.PROPERTY_IS_AUTHORITATIVE, authoritative); 233 if (recreate) { 234 finishLoad(); 235 } 236 } 237 238 /** 239 * @return whether this producer/consumer is enabled to return state to the layout upon queries. 240 */ 241 public boolean isAuthoritative() { 242 Boolean value = (Boolean) getProperty(OlcbUtils.PROPERTY_IS_AUTHORITATIVE); 243 if (value != null) { 244 return value; 245 } 246 return DEFAULT_IS_AUTHORITATIVE; 247 } 248 249 @Override 250 public void setProperty(@Nonnull String key, Object value) { 251 Object old = getProperty(key); 252 super.setProperty(key, value); 253 if (value.equals(old)) return; 254 if (pc == null) return; 255 finishLoad(); 256 } 257 258 /** 259 * @return whether this producer/consumer is always listening to state declaration messages. 260 */ 261 public boolean isListeningToStateMessages() { 262 Boolean value = (Boolean) getProperty(OlcbUtils.PROPERTY_LISTEN); 263 if (value != null) { 264 return value; 265 } 266 return DEFAULT_LISTEN; 267 } 268 269 /** 270 * Changes how the sensor reacts to state declaration messages. With listen == true state 271 * declarations will update local state at all times. With listen == false state declarations 272 * will update local state only if local state is unknown. 273 * 274 * @param listen whether we should always listen to state declaration messages. 275 */ 276 public void setListeningToStateMessages(boolean listen) { 277 boolean recreate = (listen != isListeningToStateMessages()) && (pc != null); 278 setProperty(OlcbUtils.PROPERTY_LISTEN, listen); 279 if (recreate) { 280 finishLoad(); 281 } 282 } 283 284 /* 285 * since the events that drive a sensor can be whichever state a user 286 * wants, the order of the event pair determines what is the 'active' state 287 */ 288 @Override 289 public boolean canInvert() { 290 return false; 291 } 292 293 @Override 294 public void dispose() { 295 disposePc(); 296 if (timerTask!=null) timerTask.cancel(); 297 super.dispose(); 298 } 299 300 private void disposePc() { 301 if (sensorListener != null) { 302 sensorListener.release(); 303 sensorListener = null; 304 } 305 if (pc != null) { 306 pc.release(); 307 pc = null; 308 } 309 } 310 311 /** 312 * {@inheritDoc} 313 * 314 * Sorts by decoded EventID(s) 315 */ 316 @CheckReturnValue 317 @Override 318 public int compareSystemNameSuffix(@Nonnull String suffix1, @Nonnull String suffix2, @Nonnull jmri.NamedBean n) { 319 return OlcbAddress.compareSystemNameSuffix(suffix1, suffix2, memo); 320 } 321 322 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(OlcbSensor.class); 323 324}