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