001package jmri.jmrix.openlcb;
002
003import java.util.TimerTask;
004import javax.annotation.CheckReturnValue;
005import javax.annotation.Nonnull;
006import javax.annotation.OverridingMethodsMustInvokeSuper;
007
008import jmri.NamedBean;
009import jmri.Sensor;
010import jmri.implementation.AbstractSensor;
011import jmri.jmrix.can.CanSystemConnectionMemo;
012
013import org.openlcb.EventID;
014import org.openlcb.OlcbInterface;
015import org.openlcb.implementations.BitProducerConsumer;
016import org.openlcb.implementations.EventTable;
017import org.openlcb.implementations.VersionedValueListener;
018
019/**
020 * Extend jmri.AbstractSensor for OpenLCB controls.
021 *
022 * @author Bob Jacobsen Copyright (C) 2008, 2010, 2011
023 */
024public final class OlcbSensor extends AbstractSensor {
025
026    static final int ON_TIME = 500; // time that sensor is active after being tripped
027
028    OlcbAddress addrActive;    // go to active state
029    OlcbAddress addrInactive;  // go to inactive state
030    private final OlcbInterface iface;
031    private final CanSystemConnectionMemo memo;
032
033    VersionedValueListener<Boolean> sensorListener;
034    BitProducerConsumer pc;
035    EventTable.EventTableEntryHolder activeEventTableEntryHolder = null;
036    EventTable.EventTableEntryHolder inactiveEventTableEntryHolder = null;
037    private static final boolean DEFAULT_IS_AUTHORITATIVE = true;
038    private static final boolean DEFAULT_LISTEN = true;
039    private static final int PC_DEFAULT_FLAGS = BitProducerConsumer.DEFAULT_FLAGS &
040            (~BitProducerConsumer.LISTEN_INVALID_STATE);
041
042    private TimerTask timerTask;
043
044    public OlcbSensor(String prefix, String address, CanSystemConnectionMemo memo) {
045        super(prefix + "S" + address);
046        this.memo = memo;
047        if (memo != null) { // greatly simplify testing
048            this.iface = memo.get(OlcbInterface.class);
049        } else {
050            this.iface = null;
051        }
052        init(address);
053    }
054
055    /**
056     * Common initialization for both constructors.
057     * <p>
058     *
059     */
060    private void init(String address) {
061        // build local addresses
062        OlcbAddress a = new OlcbAddress(address, memo);
063        OlcbAddress[] v = a.split(memo);
064        if (v == null) {
065            log.error("Did not find usable system name: {}", address);
066            return;
067        }
068        switch (v.length) {
069            case 1:
070                // momentary sensor
071                addrActive = v[0];
072                addrInactive = null;
073                break;
074            case 2:
075                addrActive = v[0];
076                addrInactive = v[1];
077                break;
078            default:
079                log.error("Can't parse OpenLCB Sensor system name: {}", address);
080        }
081
082    }
083
084    /**
085     * Helper function that will be invoked after construction once the properties have been
086     * loaded. Used specifically for preventing double initialization when loading sensors from
087     * XML.
088     */
089    void finishLoad() {
090        int flags = PC_DEFAULT_FLAGS;
091        flags = OlcbUtils.overridePCFlagsFromProperties(this, flags);
092        log.debug("Sensor Flags: default {} overridden {} listen bit {}", PC_DEFAULT_FLAGS, flags,
093                    BitProducerConsumer.LISTEN_EVENT_IDENTIFIED);
094        disposePc();
095        if (addrInactive == null) {
096            pc = new BitProducerConsumer(iface, addrActive.toEventID(), BitProducerConsumer.nullEvent, flags);
097
098            sensorListener = new VersionedValueListener<Boolean>(pc.getValue()) {
099                @Override
100                public void update(Boolean value) {
101                    setOwnState(value ? Sensor.ACTIVE : Sensor.INACTIVE);
102                    if (value) {
103                        setTimeout();
104                    }
105                }
106            };
107        } else {
108            pc = new BitProducerConsumer(iface, addrActive.toEventID(),
109                    addrInactive.toEventID(), flags);
110            sensorListener = new VersionedValueListener<Boolean>(pc.getValue()) {
111                @Override
112                public void update(Boolean value) {
113                    setOwnState(value ? Sensor.ACTIVE : Sensor.INACTIVE);
114                }
115            };
116        }
117        activeEventTableEntryHolder = iface.getEventTable().addEvent(addrActive.toEventID(), getEventName(true));
118        if (addrInactive != null) {
119            inactiveEventTableEntryHolder = iface.getEventTable().addEvent(addrInactive.toEventID(), getEventName(false));
120        }
121    }
122
123    /**
124     * Computes the display name of a given event to be entered into the Event Table.
125     * @param isActive true for sensor active, false for inactive.
126     * @return user-visible string to represent this event.
127     */
128    public String getEventName(boolean isActive) {
129        String name = getUserName();
130        if (name == null) name = mSystemName;
131        String msgName = isActive ? "SensorActiveEventName": "SensorInactiveEventName";
132        return Bundle.getMessage(msgName, name);
133    }
134
135    public EventID getEventID(boolean isActive) {
136        if (isActive) return addrActive.toEventID();
137        else return addrInactive.toEventID();
138    }
139
140    @Override
141    @CheckReturnValue
142    @Nonnull
143    public String getRecommendedToolTip() {
144        return addrActive.toDottedString()+";"+addrInactive.toDottedString();
145    }
146
147    /**
148     * Updates event table entries when the user name changes.
149     * @param s new user name
150     * @throws NamedBean.BadUserNameException see {@link NamedBean}
151     */
152    @Override
153    @OverridingMethodsMustInvokeSuper
154    public void setUserName(String s) throws NamedBean.BadUserNameException {
155        super.setUserName(s);
156        if (activeEventTableEntryHolder != null) {
157            activeEventTableEntryHolder.getEntry().updateDescription(getEventName(true));
158        }
159        if (inactiveEventTableEntryHolder != null) {
160            inactiveEventTableEntryHolder.getEntry().updateDescription(getEventName(false));
161        }
162    }
163
164    /**
165     * Request an update on status by sending an OpenLCB message.
166     */
167    @Override
168    public void requestUpdateFromLayout() {
169        if (pc != null) {
170            pc.resetToDefault();
171            pc.sendQuery();
172        }
173    }
174
175    /**
176     * User request to set the state, which means that we broadcast that to all
177     * listeners by putting it out on CBUS. In turn, the code in this class
178     * should use setOwnState to handle internal sets and bean notifies.
179     *
180     */
181    @Override
182    public void setKnownState(int s) {
183        if (s == Sensor.ACTIVE) {
184            sensorListener.setFromOwnerWithForceNotify(true);
185            if (addrInactive == null) {
186                setTimeout();
187            }
188        } else if (s == Sensor.INACTIVE) {
189            sensorListener.setFromOwnerWithForceNotify(false);
190        } else if (s == Sensor.UNKNOWN) {
191            if (pc != null) {
192                pc.resetToDefault();
193            }
194        }
195        setOwnState(s);
196    }
197
198    /**
199     * Have sensor return to inactive after delay, used if no inactive event was
200     * specified
201     */
202    void setTimeout() {
203        timerTask = new java.util.TimerTask() {
204            @Override
205            public void run() {
206                timerTask = null;
207                jmri.util.ThreadingUtil.runOnGUI(() -> setKnownState(Sensor.INACTIVE));
208            }
209        };
210        jmri.util.TimerUtil.schedule(timerTask, ON_TIME);
211    }
212
213    /**
214     * Changes how the turnout reacts to inquire state events. With authoritative == false the
215     * state will always be reported as UNKNOWN to the layout when queried.
216     *
217     * @param authoritative whether we should respond true state or unknown to the layout event
218     *                      state inquiries.
219     */
220    public void setAuthoritative(boolean authoritative) {
221        boolean recreate = (authoritative != isAuthoritative()) && (pc != null);
222        setProperty(OlcbUtils.PROPERTY_IS_AUTHORITATIVE, authoritative);
223        if (recreate) {
224            finishLoad();
225        }
226    }
227
228    /**
229     * @return whether this producer/consumer is enabled to return state to the layout upon queries.
230     */
231    public boolean isAuthoritative() {
232        Boolean value = (Boolean) getProperty(OlcbUtils.PROPERTY_IS_AUTHORITATIVE);
233        if (value != null) {
234            return value;
235        }
236        return DEFAULT_IS_AUTHORITATIVE;
237    }
238
239    @Override
240    public void setProperty(@Nonnull String key, Object value) {
241        Object old = getProperty(key);
242        super.setProperty(key, value);
243        if (value.equals(old)) return;
244        if (pc == null) return;
245        finishLoad();
246    }
247
248    /**
249     * @return whether this producer/consumer is always listening to state declaration messages.
250     */
251    public boolean isListeningToStateMessages() {
252        Boolean value = (Boolean) getProperty(OlcbUtils.PROPERTY_LISTEN);
253        if (value != null) {
254            return value;
255        }
256        return DEFAULT_LISTEN;
257    }
258
259    /**
260     * Changes how the turnout reacts to state declaration messages. With listen == true state
261     * declarations will update local state at all times. With listen == false state declarations
262     * will update local state only if local state is unknown.
263     *
264     * @param listen whether we should always listen to state declaration messages.
265     */
266    public void setListeningToStateMessages(boolean listen) {
267        boolean recreate = (listen != isListeningToStateMessages()) && (pc != null);
268        setProperty(OlcbUtils.PROPERTY_LISTEN, listen);
269        if (recreate) {
270            finishLoad();
271        }
272    }
273
274    /*
275     * since the events that drive a sensor can be whichever state a user
276     * wants, the order of the event pair determines what is the 'active' state
277     */
278    @Override
279    public boolean canInvert() {
280        return false;
281    }
282
283    @Override
284    public void dispose() {
285        disposePc();
286        if (timerTask!=null) timerTask.cancel();
287        super.dispose();
288    }
289
290    private void disposePc() {
291        if (sensorListener != null) {
292            sensorListener.release();
293            sensorListener = null;
294        }
295        if (pc != null) {
296            pc.release();
297            pc = null;
298        }
299    }
300
301    /**
302     * {@inheritDoc}
303     *
304     * Sorts by decoded EventID(s)
305     */
306    @CheckReturnValue
307    @Override
308    public int compareSystemNameSuffix(@Nonnull String suffix1, @Nonnull String suffix2, @Nonnull jmri.NamedBean n) {
309        return OlcbAddress.compareSystemNameSuffix(suffix1, suffix2, memo);
310    }
311
312    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(OlcbSensor.class);
313
314}