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