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 {
138            if (addrInactive != null) {
139                return addrInactive.toEventID();
140            } else {
141                return null;
142            }
143        }
144    }
145
146    @Override
147    @CheckReturnValue
148    @Nonnull
149    public String getRecommendedToolTip() {
150        // Some events are not pairs
151        if (addrInactive == null) {
152            return addrActive.toDottedString();
153        }
154        return addrActive.toDottedString()+";"+addrInactive.toDottedString();
155    }
156
157    /**
158     * Updates event table entries when the user name changes.
159     * @param s new user name
160     * @throws NamedBean.BadUserNameException see {@link NamedBean}
161     */
162    @Override
163    @OverridingMethodsMustInvokeSuper
164    public void setUserName(String s) throws NamedBean.BadUserNameException {
165        super.setUserName(s);
166        if (activeEventTableEntryHolder != null) {
167            activeEventTableEntryHolder.getEntry().updateDescription(getEventName(true));
168        }
169        if (inactiveEventTableEntryHolder != null) {
170            inactiveEventTableEntryHolder.getEntry().updateDescription(getEventName(false));
171        }
172    }
173
174    /**
175     * Request an update on status by sending an OpenLCB message.
176     */
177    @Override
178    public void requestUpdateFromLayout() {
179        if (pc != null) {
180            pc.resetToDefault();
181            pc.sendQuery();
182        }
183    }
184
185    /**
186     * User request to set the state, which means that we broadcast that to all
187     * listeners by putting it out on CBUS. In turn, the code in this class
188     * should use setOwnState to handle internal sets and bean notifies.
189     *
190     */
191    @Override
192    public void setKnownState(int s) {
193        if (s == Sensor.ACTIVE) {
194            sensorListener.setFromOwnerWithForceNotify(true);
195            if (addrInactive == null) {
196                setTimeout();
197            }
198        } else if (s == Sensor.INACTIVE) {
199            sensorListener.setFromOwnerWithForceNotify(false);
200        } else if (s == Sensor.UNKNOWN) {
201            if (pc != null) {
202                pc.resetToDefault();
203            }
204        }
205        setOwnState(s);
206    }
207
208    /**
209     * Have sensor return to inactive after delay, used if no inactive event was
210     * specified
211     */
212    void setTimeout() {
213        timerTask = new java.util.TimerTask() {
214            @Override
215            public void run() {
216                timerTask = null;
217                jmri.util.ThreadingUtil.runOnGUI(() -> setKnownState(Sensor.INACTIVE));
218            }
219        };
220        jmri.util.TimerUtil.schedule(timerTask, ON_TIME);
221    }
222
223    /**
224     * Changes how the sensor reacts to inquire state events. With authoritative == false the
225     * state will always be reported as UNKNOWN to the layout when queried.
226     *
227     * @param authoritative whether we should respond true state or unknown to the layout event
228     *                      state inquiries.
229     */
230    public void setAuthoritative(boolean authoritative) {
231        boolean recreate = (authoritative != isAuthoritative()) && (pc != null);
232        setProperty(OlcbUtils.PROPERTY_IS_AUTHORITATIVE, authoritative);
233        if (recreate) {
234            finishLoad();
235        }
236    }
237
238    /**
239     * @return whether this producer/consumer is enabled to return state to the layout upon queries.
240     */
241    public boolean isAuthoritative() {
242        Boolean value = (Boolean) getProperty(OlcbUtils.PROPERTY_IS_AUTHORITATIVE);
243        if (value != null) {
244            return value;
245        }
246        return DEFAULT_IS_AUTHORITATIVE;
247    }
248
249    @Override
250    public void setProperty(@Nonnull String key, Object value) {
251        Object old = getProperty(key);
252        super.setProperty(key, value);
253        if (value.equals(old)) return;
254        if (pc == null) return;
255        finishLoad();
256    }
257
258    /**
259     * @return whether this producer/consumer is always listening to state declaration messages.
260     */
261    public boolean isListeningToStateMessages() {
262        Boolean value = (Boolean) getProperty(OlcbUtils.PROPERTY_LISTEN);
263        if (value != null) {
264            return value;
265        }
266        return DEFAULT_LISTEN;
267    }
268
269    /**
270     * Changes how the sensor reacts to state declaration messages. With listen == true state
271     * declarations will update local state at all times. With listen == false state declarations
272     * will update local state only if local state is unknown.
273     *
274     * @param listen whether we should always listen to state declaration messages.
275     */
276    public void setListeningToStateMessages(boolean listen) {
277        boolean recreate = (listen != isListeningToStateMessages()) && (pc != null);
278        setProperty(OlcbUtils.PROPERTY_LISTEN, listen);
279        if (recreate) {
280            finishLoad();
281        }
282    }
283
284    /*
285     * since the events that drive a sensor can be whichever state a user
286     * wants, the order of the event pair determines what is the 'active' state
287     */
288    @Override
289    public boolean canInvert() {
290        return false;
291    }
292
293    @Override
294    public void dispose() {
295        disposePc();
296        if (timerTask!=null) timerTask.cancel();
297        super.dispose();
298    }
299
300    private void disposePc() {
301        if (sensorListener != null) {
302            sensorListener.release();
303            sensorListener = null;
304        }
305        if (pc != null) {
306            pc.release();
307            pc = null;
308        }
309    }
310
311    /**
312     * {@inheritDoc}
313     *
314     * Sorts by decoded EventID(s)
315     */
316    @CheckReturnValue
317    @Override
318    public int compareSystemNameSuffix(@Nonnull String suffix1, @Nonnull String suffix2, @Nonnull jmri.NamedBean n) {
319        return OlcbAddress.compareSystemNameSuffix(suffix1, suffix2, memo);
320    }
321
322    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(OlcbSensor.class);
323
324}