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