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}