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