001package jmri.jmrix.openlcb; 002 003import javax.annotation.OverridingMethodsMustInvokeSuper; 004import jmri.NamedBean; 005import jmri.Turnout; 006import jmri.jmrix.can.CanSystemConnectionMemo; 007 008import org.openlcb.EventID; 009import org.openlcb.OlcbInterface; 010import org.openlcb.implementations.BitProducerConsumer; 011import org.openlcb.implementations.EventTable; 012import org.openlcb.implementations.VersionedValueListener; 013 014import javax.annotation.Nonnull; 015import javax.annotation.CheckReturnValue; 016 017/** 018 * Turnout for OpenLCB connections. 019 * <p> 020 * State Diagram for read and write operations (click to magnify): 021 * <a href="doc-files/OlcbTurnout-State-Diagram.png"><img src="doc-files/OlcbTurnout-State-Diagram.png" alt="UML State diagram" height="50%" width="50%"></a> 022 * 023 * @author Bob Jacobsen Copyright (C) 2001, 2008, 2010, 2011 024 */ 025 026 /* 027 * @startuml jmri/jmrix/openlcb/doc-files/OlcbTurnout-State-Diagram.png 028 * CLOSED --> CLOSED: Event 1 029 * THROWN --> CLOSED: Event 1 030 * THROWN --> THROWN: Event 0 031 * CLOSED --> THROWN: Event 0 032 * [*] --> UNKNOWN 033 * UNKNOWN --> CLOSED: Event 1\nEvent 1 Produced msg with valid set\nEvent 1 Consumed msg with valid set 034 * UNKNOWN --> THROWN: Event 0\nEvent 1 Produced msg with valid set\nEvent 0 Consumed msg with valid set 035 * state INCONSISTENT 036 * @enduml 037*/ 038 039 040public final class OlcbTurnout extends jmri.implementation.AbstractTurnout { 041 042 OlcbAddress addrThrown; // go to thrown state 043 OlcbAddress addrClosed; // go to closed state 044 private final OlcbInterface iface; 045 private final CanSystemConnectionMemo memo; 046 047 VersionedValueListener<Boolean> turnoutListener; 048 BitProducerConsumer pc; 049 EventTable.EventTableEntryHolder thrownEventTableEntryHolder = null; 050 EventTable.EventTableEntryHolder closedEventTableEntryHolder = null; 051 052 static final boolean DEFAULT_IS_AUTHORITATIVE = true; 053 static final boolean DEFAULT_LISTEN = true; 054 private static final String[] validFeedbackNames = {"MONITORING", "ONESENSOR", "TWOSENSOR", 055 "DIRECT"}; 056 private static final int[] validFeedbackModes = {MONITORING, ONESENSOR, TWOSENSOR, DIRECT}; 057 private static final int validFeedbackTypes = MONITORING | ONESENSOR | TWOSENSOR | DIRECT; 058 private static final int defaultFeedbackType = MONITORING; 059 060 protected OlcbTurnout(String prefix, String address, CanSystemConnectionMemo memo) { 061 super(prefix + "T" + address); 062 this.memo = memo; 063 if (memo != null) { // greatly simplify testing 064 this.iface = memo.get(OlcbInterface.class); 065 } else { 066 this.iface = null; 067 } 068 this._validFeedbackNames = validFeedbackNames; 069 this._validFeedbackModes = validFeedbackModes; 070 this._validFeedbackTypes = validFeedbackTypes; 071 this._activeFeedbackType = defaultFeedbackType; 072 init(address); 073 } 074 075 /** 076 * Common initialization for constructor. 077 */ 078 private void init(String address) { 079 // build local addresses 080 OlcbAddress a = new OlcbAddress(address, memo); 081 OlcbAddress[] v = a.split(memo); 082 if (v == null) { 083 log.error("Did not find usable system name: {}", address); 084 return; 085 } 086 if (v.length == 2) { 087 addrThrown = v[0]; 088 addrClosed = v[1]; 089 } else { 090 log.error("Can't parse OpenLCB Turnout system name: {}", address); 091 } 092 } 093 094 /** 095 * Helper function that will be invoked after construction once the feedback type has been 096 * set. Used specifically for preventing double initialization when loading turnouts from XML. 097 */ 098 public void finishLoad() { 099 // Clear some objects first. 100 disposePc(); 101 102 int flags; 103 switch (_activeFeedbackType) { 104 case MONITORING: 105 default: 106 flags = BitProducerConsumer.IS_PRODUCER | BitProducerConsumer.IS_CONSUMER | 107 BitProducerConsumer.LISTEN_EVENT_IDENTIFIED | BitProducerConsumer 108 .QUERY_AT_STARTUP; 109 break; 110 case DIRECT: 111 flags = BitProducerConsumer.IS_PRODUCER; 112 break; 113 } 114 flags = OlcbUtils.overridePCFlagsFromProperties(this, flags); 115 pc = new BitProducerConsumer(iface, addrThrown.toEventID(), addrClosed.toEventID(), flags); 116 turnoutListener = new VersionedValueListener<Boolean>(pc.getValue()) { 117 @Override 118 public void update(Boolean value) { 119 int s = ((value ^ getInverted()) ? THROWN : CLOSED); 120 if (_activeFeedbackType != DIRECT) { 121 newCommandedState(s); 122 if (_activeFeedbackType == MONITORING) { 123 newKnownState(s); 124 } 125 } 126 } 127 }; 128 if (thrownEventTableEntryHolder != null) { 129 thrownEventTableEntryHolder.release(); 130 thrownEventTableEntryHolder = null; 131 } 132 if (closedEventTableEntryHolder != null) { 133 closedEventTableEntryHolder.release(); 134 closedEventTableEntryHolder = null; 135 } 136 thrownEventTableEntryHolder = iface.getEventTable().addEvent(addrThrown.toEventID(), getEventName(true)); 137 closedEventTableEntryHolder = iface.getEventTable().addEvent(addrClosed.toEventID(), getEventName(false)); 138 } 139 140 /** 141 * Computes the display name of a given event to be entered into the Event Table. 142 * @param isThrown true for thrown event, false for closed event 143 * @return user-visible string to represent this event. 144 */ 145 public String getEventName(boolean isThrown) { 146 String name = getUserName(); 147 if (name == null) name = mSystemName; 148 String msgName = isThrown ? "TurnoutThrownEventName": "TurnoutClosedEventName"; 149 return Bundle.getMessage(msgName, name); 150 } 151 152 public EventID getEventID(boolean isThrown) { 153 if (isThrown) return addrThrown.toEventID(); 154 else return addrClosed.toEventID(); 155 } 156 157 @Override 158 @CheckReturnValue 159 @Nonnull 160 public String getRecommendedToolTip() { 161 return addrClosed.toDottedString()+";"+addrThrown.toDottedString(); 162 } 163 164 /** 165 * Updates event table entries when the user name changes. 166 * @param s new user name 167 * @throws NamedBean.BadUserNameException see {@link NamedBean} 168 */ 169 @Override 170 @OverridingMethodsMustInvokeSuper 171 public void setUserName(String s) throws NamedBean.BadUserNameException { 172 super.setUserName(s); 173 if (thrownEventTableEntryHolder != null) { 174 thrownEventTableEntryHolder.getEntry().updateDescription(getEventName(true)); 175 } 176 if (closedEventTableEntryHolder != null) { 177 closedEventTableEntryHolder.getEntry().updateDescription(getEventName(false)); 178 } 179 } 180 181 @Override 182 public void setFeedbackMode(int mode) throws IllegalArgumentException { 183 boolean recreate = (mode != _activeFeedbackType) && (pc != null); 184 super.setFeedbackMode(mode); 185 if (recreate) { 186 finishLoad(); 187 } 188 } 189 190 @Override 191 public void setProperty(@Nonnull String key, Object value) { 192 Object old = getProperty(key); 193 super.setProperty(key, value); 194 if (value.equals(old)) return; 195 if (pc == null) return; 196 finishLoad(); 197 } 198 199 /** 200 * {@inheritDoc} 201 * Sends an OpenLCB command 202 */ 203 @Override 204 protected void forwardCommandChangeToLayout(int s) { 205 if (s == Turnout.THROWN) { 206 turnoutListener.setFromOwnerWithForceNotify(true ^ getInverted()); 207 if (_activeFeedbackType == MONITORING) { 208 newKnownState(THROWN); 209 } 210 } else if (s == Turnout.CLOSED) { 211 turnoutListener.setFromOwnerWithForceNotify(false ^ getInverted()); 212 if (_activeFeedbackType == MONITORING) { 213 newKnownState(CLOSED); 214 } 215 } else if (s == Turnout.UNKNOWN) { 216 if (pc != null) { 217 pc.resetToDefault(); 218 } 219 newKnownState(Turnout.UNKNOWN); 220 } 221 } 222 223 @Override 224 public void requestUpdateFromLayout() { 225 if (_activeFeedbackType == MONITORING) { 226 if (pc != null) { 227 pc.resetToDefault(); 228 pc.sendQuery(); 229 } 230 } 231 super.requestUpdateFromLayout(); 232 } 233 234 @Override 235 protected void turnoutPushbuttonLockout(boolean locked) { 236 // TODO: maybe we could get another pair of events in the address and use that event pair 237 // to perform a lockout change on the turnout decoder itself. 238 } 239 240 @Override 241 public boolean canInvert() { 242 return true; 243 } 244 245 @Override 246 public void dispose() { 247 if (thrownEventTableEntryHolder != null) { 248 thrownEventTableEntryHolder.release(); 249 thrownEventTableEntryHolder = null; 250 } 251 if (closedEventTableEntryHolder != null) { 252 closedEventTableEntryHolder.release(); 253 closedEventTableEntryHolder = null; 254 } 255 disposePc(); 256 super.dispose(); 257 } 258 259 private void disposePc() { 260 if (turnoutListener != null) turnoutListener.release(); 261 if (pc != null) pc.release(); 262 turnoutListener = null; 263 pc = null; 264 } 265 266 /** 267 * Changes how the turnout reacts to inquire state events. With authoritative == false the 268 * state will always be reported as UNKNOWN to the layout when queried. 269 * 270 * @param authoritative whether we should respond true state or unknown to the layout event 271 * state inquiries. 272 */ 273 public void setAuthoritative(boolean authoritative) { 274 boolean recreate = (authoritative != isAuthoritative()) && (pc != null); 275 setProperty(OlcbUtils.PROPERTY_IS_AUTHORITATIVE, authoritative); 276 if (recreate) { 277 finishLoad(); 278 } 279 } 280 281 /** 282 * @return whether this producer/consumer is enabled to return state to the layout upon queries. 283 */ 284 public boolean isAuthoritative() { 285 Boolean value = (Boolean) getProperty(OlcbUtils.PROPERTY_IS_AUTHORITATIVE); 286 if (value != null) { 287 return value; 288 } 289 return DEFAULT_IS_AUTHORITATIVE; 290 } 291 292 /** 293 * @return whether this producer/consumer is always listening to state declaration messages. 294 */ 295 public boolean isListeningToStateMessages() { 296 Boolean value = (Boolean) getProperty(OlcbUtils.PROPERTY_LISTEN); 297 if (value != null) { 298 return value; 299 } 300 return DEFAULT_LISTEN; 301 } 302 303 /** 304 * Changes how the turnout reacts to state declaration messages. With listen == true state 305 * declarations will update local state at all times. With listen == false state declarations 306 * will update local state only if local state is unknown. 307 * 308 * @param listen whether we should always listen to state declaration messages. 309 */ 310 public void setListeningToStateMessages(boolean listen) { 311 boolean recreate = (listen != isListeningToStateMessages()) && (pc != null); 312 setProperty(OlcbUtils.PROPERTY_LISTEN, listen); 313 if (recreate) { 314 finishLoad(); 315 } 316 } 317 318 /** 319 * {@inheritDoc} 320 * 321 * Sorts by decoded EventID(s) 322 */ 323 @CheckReturnValue 324 @Override 325 public int compareSystemNameSuffix(@Nonnull String suffix1, @Nonnull String suffix2, @Nonnull jmri.NamedBean n) { 326 return OlcbAddress.compareSystemNameSuffix(suffix1, suffix2, memo); 327 } 328 329 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(OlcbTurnout.class); 330 331}