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