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