001package jmri.jmrix.loconet;
002
003import java.util.Locale;
004import javax.annotation.Nonnull;
005import jmri.JmriException;
006import jmri.Sensor;
007import org.slf4j.Logger;
008import org.slf4j.LoggerFactory;
009import java.time.Instant;
010
011/**
012 * Manage the LocoNet-specific Sensor implementation.
013 * System names are "LSnnn", where L is the user configurable system prefix,
014 * nnn is the sensor number without padding.  Valid sensor numbers are in the
015 * range 1 to 2048, inclusive.
016 *
017 * Provides a mechanism to perform the LocoNet "Interrogate" process in order
018 * to get initial values from those LocoNet devices which support the process
019 * and provide LocoNet Sensor (and/or LocoNet Turnout) functionality.
020 *
021 * @author Bob Jacobsen Copyright (C) 2001
022 * @author B. Milhaupt  Copyright (C) 2020
023 */
024public class LnSensorManager extends jmri.managers.AbstractSensorManager implements LocoNetListener {
025
026    protected final LnTrafficController tc;
027
028    /**
029     * Minimum amount of time since previous LocoNet Sensor State report or
030     * previous LocoNet Turnout State report or previous LocoNet "interrogate"
031     * message.
032     */
033    protected int restingTime;
034
035    /**
036     * Instant at which last LocoNet Sensor State message or LocoNet Turnout state
037     * message or LocoNet Interrogation request message was received.
038     *
039     */
040    private volatile Instant lastSensTurnInterrog;
041
042    public LnSensorManager(LocoNetSystemConnectionMemo memo, boolean interrogateAtStart) {
043        super(memo);
044        this.restingTime = 1250;
045        tc = memo.getLnTrafficController();
046        if (tc == null) {
047            log.error("SensorManager Created, yet there is no Traffic Controller");
048            return;
049        }
050        lastSensTurnInterrog = Instant.now();   // a baseline starting-point for
051                    // interrogation timing
052
053        // ctor has to register for LocoNet events
054        tc.addLocoNetListener(~0, this);
055
056        if (interrogateAtStart) {
057            // start the update sequence. Until JMRI 2.9.4, this waited
058            // until files have been read, but starts automatically
059            // since 2.9.5 for multi-system support.
060            updateAll();
061        }
062    }
063
064    /** {@inheritDoc} */
065    @Override
066    @Nonnull
067    public LocoNetSystemConnectionMemo getMemo() {
068        return (LocoNetSystemConnectionMemo) memo;
069    }
070
071    // to free resources when no longer used
072    /** {@inheritDoc} */
073    @Override
074    public void dispose() {
075        tc.removeLocoNetListener(~0, this);
076        Thread t = thread;
077        if (t != null) {
078            try {
079                t.interrupt();
080                t.join();
081            } catch (InterruptedException ex) {
082                log.warn("dispose interrupted");
083            } finally {
084                thread = null;
085            }
086        }
087        super.dispose();
088    }
089
090    // LocoNet-specific methods
091
092    /** {@inheritDoc} */
093    @Override
094    @Nonnull
095    protected Sensor createNewSensor(@Nonnull String systemName, String userName) throws IllegalArgumentException {
096        return new LnSensor(systemName, userName, tc, getSystemPrefix());
097    }
098
099    /**
100     * Listen for sensor messages, creating them as needed.
101     * @param l LocoNet message to be examined
102     */
103    @Override
104    public void message(LocoNetMessage l) {
105        // parse message type
106        LnSensorAddress a;
107        LnSensor ns;
108        switch (l.getOpCode()) {
109            case LnConstants.OPC_INPUT_REP:                /* page 9 of LocoNet PE */
110
111                int sw1 = l.getElement(1);
112                int sw2 = l.getElement(2);
113                a = new LnSensorAddress(sw1, sw2, getSystemPrefix());
114                log.debug("INPUT_REP received with address {}", a);
115                lastSensTurnInterrog = Instant.now();
116                break;
117            case LnConstants.OPC_SW_REP:
118                lastSensTurnInterrog = Instant.now();
119                return;
120            case LnConstants.OPC_SW_REQ:
121            case LnConstants.OPC_SW_ACK:
122                int address = ((l.getElement(1)& 0x7f) + 128*(l.getElement(2) & 0x0f));
123                switch (address) {
124                    case 0x3F8:
125                    case 0x3F9:
126                    case 0x3FA:
127                    case 0x3FB:
128                        lastSensTurnInterrog = Instant.now();
129                        return;
130                    default:
131                        break;
132                }
133            //$FALL-THROUGH$
134            default:  // here we didn't find an interesting command
135                return;
136        }
137        // reach here for LocoNet sensor input command; make sure we know about this one
138        String s = a.getNumericAddress();
139        ns = (LnSensor) getBySystemName(s);
140        if (ns == null) {
141            // need to store a new one
142            if (log.isDebugEnabled()) {
143                log.debug("Create new LnSensor as {}", s);
144            }
145            ns = (LnSensor) newSensor(s, null);
146        }
147        ns.messageFromManager(l);  // have it update state
148    }
149
150    volatile LnSensorUpdateThread thread;
151
152    /**
153     * Requests status updates from all layout sensors.
154     */
155    @Override
156    public void updateAll() {
157        if (!busy) {
158            setUpdateBusy();
159            thread = new LnSensorUpdateThread(this, tc, getRestingTime());
160            thread.setName("LnSensorUpdateThread"); // NOI18N
161            thread.start();
162        }
163    }
164
165    /**
166     * Set Route busy when commands are being issued to Route turnouts.
167     */
168    public void setUpdateBusy() {
169        busy = true;
170    }
171
172    /**
173     * Set Route not busy when all commands have been issued to Route
174     * turnouts.
175     */
176    public void setUpdateNotBusy() {
177        busy = false;
178    }
179
180    private boolean busy = false;
181
182    /** {@inheritDoc} */
183    @Override
184    public boolean allowMultipleAdditions(@Nonnull String systemName) {
185        return true;
186    }
187
188    /** {@inheritDoc} */
189    @Override
190    @Nonnull
191    public String createSystemName(@Nonnull String curAddress, @Nonnull String prefix) throws JmriException {
192        if (curAddress.contains(":")) { // NOI18N
193
194            // NOTE: This format is deprecated in JMRI 4.17.4 on account the
195            // "byte:bit" format cannot be used under normal JMRI usage
196            // circumstances.  It is retained for the normal deprecation period
197            // in order to support any atypical usage patterns.
198
199            int board = 0;
200            int channel = 0;
201            // Address format passed is in the form of board:channel or T:turnout address
202            int seperator = curAddress.indexOf(":"); // NOI18N
203            boolean turnout = false;
204            if (curAddress.substring(0, seperator).toUpperCase().equals("T")) { // NOI18N
205                turnout = true;
206            } else {
207                try {
208                    board = Integer.parseInt(curAddress.substring(0, seperator));
209                } catch (NumberFormatException ex) {
210                    throw new JmriException("Unable to convert '"+curAddress+"' into the cab and channel format of nn:xx"); // NOI18N
211                }
212            }
213            try {
214                channel = Integer.parseInt(curAddress.substring(seperator + 1));
215            } catch (NumberFormatException ex) {
216                throw new JmriException("Unable to convert '"+curAddress+"' into the cab and channel format of nn:xx"); // NOI18N
217            }
218            if (turnout) {
219                iName = 2 * (channel - 1) + 1;
220            } else {
221                iName = 16 * board + channel - 16;
222            }
223            jmri.util.LoggingUtil.warnOnce(log,
224                    "LnSensorManager.createSystemName(curAddress, prefix) support for curAddress using the '{}' format is deprecated as of JMRI 4.17.4 and will be removed in a future JMRI release.  Use the curAddress format '{}' instead.",  // NOI18N
225                    curAddress, iName);
226        } else {
227            // Entered in using the old format
228            log.debug("LnSensorManager creating system name for {}", curAddress);
229            try {
230                iName = Integer.parseInt(curAddress);
231            } catch (NumberFormatException ex) {
232                throw new JmriException("Hardware Address passed "+curAddress+" should be a number"); // NOI18N
233            }
234        }
235        return prefix + typeLetter() + iName;
236    }
237
238    int iName;
239
240    /** {@inheritDoc} */
241    @Override
242    public NameValidity validSystemNameFormat(@Nonnull String systemName) {
243        return (getBitFromSystemName(systemName) != 0) ? NameValidity.VALID : NameValidity.INVALID;
244    }
245
246    /** {@inheritDoc} */
247    @Override
248    @Nonnull
249    public String validateSystemNameFormat(@Nonnull String systemName, @Nonnull Locale locale) {
250        return validateIntegerSystemNameFormat(systemName, 1, 4096, locale);
251    }
252
253    /**
254     * Get the bit address from the system name.
255     * @param systemName a valid LocoNet-based Turnout System Name
256     * @return the turnout number extracted from the system name
257     */
258    public int getBitFromSystemName(String systemName) {
259        try {
260            validateSystemNameFormat(systemName, Locale.getDefault());
261        } catch (IllegalArgumentException ex) {
262            return 0;
263        }
264        return Integer.parseInt(systemName.substring(getSystemNamePrefix().length()));
265    }
266
267    /** {@inheritDoc} */
268    @Override
269    public String getEntryToolTip() {
270        return Bundle.getMessage("AddInputEntryToolTip");
271    }
272
273    /**
274     * Set "resting time" for the Interrogation process.
275     *
276     * A minimum of 500 (milliseconds) and a maximum of 200000 (milliseconds) is
277     * implemented.  Values below this lower limit are forced to the lower
278     * limit, and values above this upper limit are forced to the upper limit.
279     *
280     * @param rest Number of milliseconds (minimum) before sending next
281     *      LocoNet Interrogate message.
282     */
283    public void setRestingTime(int rest) {
284        if (rest < 500) {
285            rest = 500;
286        } else if (rest > 200000) {
287            rest = 200000;
288        }
289        restingTime = rest;
290    }
291
292    /**
293     * get Interrogation process's "resting time"
294     * @return Resting time, in milliseconds
295     */
296    public int getRestingTime() {
297        return restingTime;
298    }
299
300    /**
301     * Class providing a thread to query LocoNet Sensor and Turnout states.
302     */
303    class LnSensorUpdateThread extends Thread {
304        private LnSensorManager sm = null;
305        private LnTrafficController tc = null;
306        private java.time.Duration restingTime;
307
308        /**
309         * Constructs the thread
310         * @param sm SensorManager to use
311         * @param tc TrafficController to use
312         * @param restingTime Min time before next LN query message sent
313         */
314        public LnSensorUpdateThread(LnSensorManager sm, LnTrafficController tc,
315                int restingTime) {
316            this.sm = sm;
317            this.tc = tc;
318            this.restingTime = java.time.Duration.ofMillis(restingTime);
319        }
320
321        /**
322         * Runs the thread - sends 8 commands to query status of all stationary
323         * sensors (per LocoNet PE Specs, page 12-13).
324         *
325         * Timing between query messages is determined by certain previous LocoNet
326         * traffic (as noted by lastSensTurnInterrog) and restingTime.
327         */
328        @Override
329        public void run() {
330            sm.setUpdateBusy();
331            while (!tc.status()) {
332                try {
333                    // Delay 500 mSec to allow init of traffic controller,
334                    // listeners, and to limit amount of LocoNet traffic at
335                    // JMRI start-up.
336                    Thread.sleep(500);
337                } catch (InterruptedException e) {
338                    Thread.currentThread().interrupt(); // retain if needed later
339                    sm.setUpdateNotBusy();
340                    return; // and stop work
341                }
342            }
343            byte sw1[] = {0x78, 0x79, 0x7a, 0x7b, 0x78, 0x79, 0x7a, 0x7b};
344            byte sw2[] = {0x27, 0x27, 0x27, 0x27, 0x07, 0x07, 0x07, 0x07};
345            // create and initialize LocoNet message
346            LocoNetMessage msg = new LocoNetMessage(4);
347            msg.setOpCode(LnConstants.OPC_SW_REQ);
348            for (int k = 0; k < 8; k++) {
349                Instant n = Instant.now();
350                Instant n2 = lastSensTurnInterrog.plus(restingTime);
351                int result = n.compareTo(n2);
352                log.debug("Interrogation phase {}: now {}, lastSensInterrog {}, target{}, time compare result {}",
353                        k, n, lastSensTurnInterrog, n2, result);
354                while (result < 0) {
355                    try {
356                        // Delay 100 mSec
357                        Thread.sleep(100);
358                    } catch (InterruptedException e) {
359                        Thread.currentThread().interrupt(); // retain if needed later
360                        sm.setUpdateNotBusy();
361                        return; // and stop work
362                    }
363                    n = Instant.now();
364                    result = n.compareTo(n2);
365                    log.debug("Interrogation phase {}: now {}, lastSensInterrog {}, target{}, time compare result {}",
366                            k, n, lastSensTurnInterrog, n2, result);
367                }
368                msg.setElement(1, sw1[k]);
369                msg.setElement(2, sw2[k]);
370
371                tc.sendLocoNetMessage(msg);
372
373                /* lastSensTurnInterrog needs to be updated here to prevent quick
374                 * sending of the next query message.  (It will be updated upon
375                 * reception of the LocoNet "echo".)
376                 */
377                lastSensTurnInterrog = Instant.now();
378
379                log.debug("LnSensorUpdate sent");
380            }
381            sm.setUpdateNotBusy();
382        }
383    }
384
385    private final static Logger log = LoggerFactory.getLogger(LnSensorManager.class);
386
387}