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}