001package jmri.jmrix.nce;
002
003import java.util.Locale;
004import javax.annotation.Nonnull;
005import jmri.JmriException;
006import jmri.NamedBean;
007import jmri.Sensor;
008import jmri.jmrix.AbstractMRReply;
009import org.slf4j.Logger;
010import org.slf4j.LoggerFactory;
011
012
013/**
014 * Manage the NCE-specific Sensor implementation.
015 * <p>
016 * System names are "NSnnn", where N is the user configurable system prefix,
017 * nnn is the sensor number without padding.
018 * <p>
019 * This class is responsible for generating polling messages for the
020 * NceTrafficController, see nextAiuPoll()
021 *
022 * @author Bob Jacobsen Copyright (C) 2003
023 * @author Ken Cameron (C) 2023
024 */
025public class NceSensorManager extends jmri.managers.AbstractSensorManager
026        implements NceListener {
027
028    public NceSensorManager(NceSystemConnectionMemo memo) {
029        super(memo);
030        for (int i = MINAIU; i <= MAXAIU; i++) {
031            aiuArray[i] = null;
032        }
033        listener = new NceListener() {
034            @Override
035            public void message(NceMessage m) {
036            }
037
038            @Override
039            public void reply(NceReply r) {
040                if (r.isSensorMessage()) {
041                    mInstance.handleSensorMessage(r);
042                }
043            }
044        };
045        memo.getNceTrafficController().addNceListener(listener);
046    }
047
048    private final NceSensorManager mInstance = null;
049
050    /**
051     * {@inheritDoc}
052     */
053    @Override
054    @Nonnull
055    public NceSystemConnectionMemo getMemo() {
056        return (NceSystemConnectionMemo) memo;
057    }
058
059    // to free resources when no longer used
060    @Override
061    public void dispose() {
062        stopPolling = true;  // tell polling thread to go away
063        Thread thread = pollThread;
064        if (thread != null) {
065            try {
066                thread.interrupt();
067                thread.join();
068            } catch (InterruptedException ex) {
069                log.warn("dispose interrupted");
070            }
071        }
072        getMemo().getNceTrafficController().removeNceListener(listener);
073        super.dispose();
074    }
075
076    /**
077     * {@inheritDoc}
078     * <p>
079     * Assumes calling method has checked that a Sensor with this system
080     * name does not already exist.
081     *
082     * @throws IllegalArgumentException if the system name is not in a valid format
083     */
084    @Override
085    @Nonnull
086    protected Sensor createNewSensor(@Nonnull String systemName, String userName) throws IllegalArgumentException {
087
088        int number = 0;
089        String normName;
090        try {
091            // see if this is a valid address
092            String address = systemName.substring(getSystemPrefix().length() + 1);
093            normName = createSystemName(address, getSystemPrefix());
094            // parse converted system name
095            number = Integer.parseInt(normName.substring(getSystemPrefix().length() + 1));
096        } catch (NumberFormatException | JmriException e) {
097            throw new IllegalArgumentException("Unable to convert " +  // NOI18N
098                    systemName.substring(getSystemPrefix().length() + 1) +
099                    " to NCE sensor address"); // NOI18N
100        }
101        Sensor s = new NceSensor(normName);
102        s.setUserName(userName);
103
104        // ensure the AIU exists
105        int index = (number / 16) + 1;
106        if (aiuArray[index] == null) {
107            aiuArray[index] = new NceAIU();
108            buildActiveAIUs();
109        }
110
111        // register this sensor with the AIU
112        aiuArray[index].registerSensor(s, number - (index - 1) * 16);
113
114        return s;
115    }
116
117    NceAIU[] aiuArray = new NceAIU[MAXAIU + 1];  // element 0 isn't used
118    int[] activeAIUs = new int[MAXAIU];  // keep track of those worth polling
119    int activeAIUMax = 0;       // last+1 element used of activeAIUs
120    private static final int MINAIU = 1;
121    private static final int MAXAIU = 63;
122    private static final int MAXPIN = 14;    // only pins 1 - 14 used on NCE AIU
123
124    volatile Thread pollThread;
125    volatile boolean stopPolling = false;
126    NceListener listener;
127
128    // polling parameters and variables
129    private boolean loggedAiuNotSupported = false;  // set after logging that AIU isn't supported on this config
130    private final int shortCycleInterval = 200;
131    private final int longCycleInterval = 10000;  // when we know async messages are flowing
132    private final long maxSilentInterval = 30000;  // max slow poll time without hearing an async message
133    private final int pollTimeout = 20000;    // in case of lost response
134    private int aiuCycleCount;
135    private long lastMessageReceived;     // time of last async message
136    private NceAIU currentAIU;
137    private boolean awaitingReply = false;
138    private boolean awaitingDelay = false;
139
140    /**
141     * Build the array of the indices of AIUs which have been polled, and
142     * ensures that pollManager has all the information it needs to work
143     * correctly.
144     *
145     */
146    /* Some logic notes
147     *
148     * Sensor polling normally happens on a short cycle - the NCE round-trip
149     * response time (normally 50mS, set by the serial line timeout) plus
150     * the "shortCycleInterval" defined above. If an async sensor message is received,
151     * we switch to the longCycleInterval since really we don't need to poll at all.
152     *
153     * We use the long poll only if the following conditions are satisified:
154     *
155     * -- there have been at least two poll cycle completions since the last change
156     * to the list of active sensor - this means at least one complete poll cycle,
157     * so we are sure we know the states of all the sensors to begin with
158     *
159     * -- we have received an async message in the last maxSilentInterval, so that
160     * if the user turns off async messages (possible, though dumb in mid session)
161     * the system will stumble back to life
162     *
163     * The interaction between buildActiveAIUs and pollManager is designed so that
164     * no explicit sync or locking is needed when the former changes the list of active
165     * AIUs used by the latter. At worst, there will be one cycle which polls the same
166     * sensor twice.
167     *
168     * Be VERY CAREFUL if you change any of this.
169     *
170     */
171    private void buildActiveAIUs() {
172        if ((getMemo().getNceTrafficController().getCmdGroups() & NceTrafficController.CMDS_AUI_READ) 
173                != NceTrafficController.CMDS_AUI_READ) {
174            if (!loggedAiuNotSupported) {
175                log.info("AIU not supported in this configuration");
176                loggedAiuNotSupported = true;
177                return;
178            }
179        }
180        activeAIUMax = 0;
181        for (int a = MINAIU; a <= MAXAIU; ++a) {
182            if (aiuArray[a] != null) {
183                activeAIUs[activeAIUMax++] = a;
184            }
185        }
186        aiuCycleCount = 0;    // force another polling cycle
187        lastMessageReceived = Long.MIN_VALUE;
188        if (activeAIUMax > 0) {
189            if (pollThread == null) {
190                pollThread = new Thread(new Runnable() {
191                    @Override
192                    public void run() {
193                        pollManager();
194                    }
195                });
196                pollThread.setName("NCE Sensor Poll");
197                pollThread.setDaemon(true);
198                pollThread.start();
199            } else {
200                synchronized (this) {
201                    if (awaitingDelay) {  // interrupt long between-poll wait
202                        notify();
203                    }
204                }
205            }
206        }
207    }
208
209    public NceMessage makeAIUPoll(int aiuNo) {
210        if (getMemo().getNceTrafficController().getUsbSystem() == NceTrafficController.USB_SYSTEM_NONE) {
211            // use old 4 byte read command if not USB
212            return makeAIUPoll4ByteReply(aiuNo);
213        } else {
214            // use new 2 byte read command if USB
215            return makeAIUPoll2ByteReply(aiuNo);
216        }
217    }
218
219    /**
220     * Construct a binary-formatted AIU poll message
221     *
222     * @param aiuNo number of AIU to poll
223     * @return message to be queued
224     */
225    private NceMessage makeAIUPoll4ByteReply(int aiuNo) {
226        NceMessage m = new NceMessage(2);
227        m.setBinary(true);
228        m.setReplyLen(NceMessage.REPLY_4);
229        m.setElement(0, NceMessage.READ_AUI4_CMD);
230        m.setElement(1, aiuNo);
231        m.setTimeout(pollTimeout);
232        return m;
233    }
234
235    /**
236     * construct a binary-formatted AIU poll message
237     *
238     * @param aiuNo number of AIU to poll
239     * @return message to be queued
240     */
241    private NceMessage makeAIUPoll2ByteReply(int aiuNo) {
242        NceMessage m = new NceMessage(2);
243        m.setBinary(true);
244        m.setReplyLen(NceMessage.REPLY_2);
245        m.setElement(0, NceMessage.READ_AUI2_CMD);
246        m.setElement(1, aiuNo);
247        m.setTimeout(pollTimeout);
248        return m;
249    }
250
251    /**
252     * Send poll messages for AIU sensors. Also interact with
253     * asynchronous sensor state messages. Adjust poll cycle according to
254     * whether any async messages have been received recently. Also we require
255     * one poll of each sensor before squelching active polls.
256     */
257    private void pollManager() {
258        if ((getMemo().getNceTrafficController().getCmdGroups() & NceTrafficController.CMDS_AUI_READ) 
259                != NceTrafficController.CMDS_AUI_READ) {
260            if (!loggedAiuNotSupported) {
261                log.info("AIU not supported in this configuration");
262                loggedAiuNotSupported = true;
263            }
264        } else {
265            while (!stopPolling) {
266                for (int a = 0; a < activeAIUMax; ++a) {
267                    int aiuNo = activeAIUs[a];
268                    currentAIU = aiuArray[aiuNo];
269                    if (currentAIU != null) {    // in case it has gone away
270                        NceMessage m = makeAIUPoll(aiuNo);
271                        synchronized (this) {
272                            log.debug("queueing poll request for AIU {}", aiuNo);
273                            getMemo().getNceTrafficController().sendNceMessage(m, this);
274                            awaitingReply = true;
275                            try {
276                                wait(pollTimeout);
277                            } catch (InterruptedException e) {
278                                Thread.currentThread().interrupt(); // retain if needed later
279                                return;
280                            }
281                        }
282                        int delay = shortCycleInterval;
283                        if (aiuCycleCount >= 2
284                                && lastMessageReceived >= System.currentTimeMillis() - maxSilentInterval) {
285                            delay = longCycleInterval;
286                        }
287                        synchronized (this) {
288                            if (awaitingReply && !stopPolling) {
289                                log.warn("timeout awaiting poll response for AIU {}", aiuNo);
290                                // slow down the poll since we're not getting responses
291                                // this lets NceConnectionStatus to do its thing
292                                delay = pollTimeout;
293                            }
294                            try {
295                                awaitingDelay = true;
296                                wait(delay);
297                            } catch (InterruptedException e) {
298                                Thread.currentThread().interrupt(); // retain if needed later
299                                return;
300                            } finally {
301                                awaitingDelay = false;
302                            }
303                        }
304                    }
305                }
306                ++aiuCycleCount;
307            }
308        }
309    }
310
311    @Override
312    public void message(NceMessage r) {
313        log.warn("unexpected message");
314    }
315
316    /**
317     * Process single received reply from sensor poll.
318     */
319    @Override
320    public void reply(NceReply r) {
321        if (!r.isUnsolicited()) {
322            int bits;
323            synchronized (this) {
324                bits = r.pollValue();  // bits is the value in hex from the message
325                awaitingReply = false;
326                this.notify();
327            }
328            currentAIU.markChanges(bits);
329            if (log.isDebugEnabled()) {
330                String str = jmri.util.StringUtil.twoHexFromInt((bits >> 4) & 0xf);
331                str += " ";
332                str = jmri.util.StringUtil.appendTwoHexFromInt(bits & 0xf, str);
333                log.debug("sensor poll reply received: \"{}\"", str);
334            }
335        }
336    }
337
338    /**
339     * Handle an unsolicited sensor (AIU) state message.
340     *
341     * @param r sensor message
342     */
343    public void handleSensorMessage(AbstractMRReply r) {
344        int index = r.getElement(1) - 0x30;
345        int indicator = r.getElement(2);
346        if (r.getElement(0) == 0x61 && r.getElement(1) >= 0x30 && r.getElement(1) <= 0x6f
347                && ((indicator >= 0x41 && indicator <= 0x5e) || (indicator >= 0x61 && indicator <= 0x7e))) {
348            lastMessageReceived = System.currentTimeMillis();
349            if (aiuArray[index] == null) {
350                log.debug("unsolicited message \"{}\" for unused sensor array", r.toString());
351            } else {
352                int sensorNo;
353                int newState;
354                if (indicator >= 0x60) {
355                    sensorNo = indicator - 0x61;
356                    newState = Sensor.ACTIVE;
357                } else {
358                    sensorNo = indicator - 0x41;
359                    newState = Sensor.INACTIVE;
360                }
361                Sensor s = aiuArray[index].getSensor(sensorNo);
362                if (s.getInverted()) {
363                    if (newState == Sensor.ACTIVE) {
364                        newState = Sensor.INACTIVE;
365                    } else if (newState == Sensor.INACTIVE) {
366                        newState = Sensor.ACTIVE;
367                    }
368                }
369
370                if (log.isDebugEnabled()) {
371                    log.debug("Handling sensor message \"{}\" for {} {}",
372                        r, s.getSystemName(), s.describeState(newState) );
373                }
374                aiuArray[index].sensorChange(sensorNo, newState);
375            }
376        } else {
377            log.warn("incorrect sensor message: {}", r.toString());
378        }
379    }
380
381    @Override
382    public boolean allowMultipleAdditions(@Nonnull String systemName) {
383        return true;
384    }
385
386    @Override
387    @Nonnull
388    public String createSystemName(@Nonnull String curAddress, @Nonnull String prefix) throws JmriException {
389        if (curAddress.contains(":")) {
390            // Sensor address is presented in the format AIU Cab Address:Pin Number On AIU
391            // Should we be validating the values of aiucab address and pin number?
392            // Yes we should, added check for valid AIU and pin ranges DBoudreau 2/13/2013
393            int seperator = curAddress.indexOf(":");
394            try {
395                aiucab = Integer.parseInt(curAddress.substring(0, seperator));
396                pin = Integer.parseInt(curAddress.substring(seperator + 1));
397            } catch (NumberFormatException ex) {
398                throw new JmriException("Unable to convert "+curAddress+" into the cab and pin format of nn:xx");
399            }
400            iName = (aiucab - 1) * 16 + pin - 1;
401
402        } else {
403            //Entered in using the old format
404            try {
405                iName = Integer.parseInt(curAddress);
406            } catch (NumberFormatException ex) {
407                throw new JmriException("Hardware Address passed "+curAddress+" should be a number or the cab and pin format of nn:xx");
408            }
409            pin = iName % 16 + 1;
410            aiucab = iName / 16 + 1;
411        }
412        // only pins 1 through 14 are valid
413        if (pin == 0 || pin > MAXPIN) {
414            throw new JmriException("Sensor pin number "+pin+" for address "+curAddress+" is out of range; only pin numbers 1 - 14 are valid");
415        }
416        if (aiucab == 0 || aiucab > MAXAIU) {
417            throw new JmriException("AIU number "+aiucab+" for address "+curAddress+" is out of range; only AIU 1 - 63 are valid");
418        }
419        return prefix + typeLetter() + iName;
420    }
421
422    int aiucab = 0;
423    int pin = 0;
424    int iName = 0;
425
426    /**
427     * {@inheritDoc}
428     */
429    @Override
430    @Nonnull
431    public String validateSystemNameFormat(@Nonnull String name, @Nonnull Locale locale) {
432        String parts[];
433        int num;
434        if (name.contains(":")) {
435            parts = super.validateSystemNameFormat(name, locale)
436                    .substring(getSystemNamePrefix().length()).split(":");
437            if (parts.length != 2) {
438                throw new NamedBean.BadSystemNameException(
439                        Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameNeedCabAndPin", name),
440                        Bundle.getMessage(locale, "InvalidSystemNameNeedCabAndPin", name));
441            }
442        } else {
443            parts = new String[]{"0", "0"};
444            try {
445                num = Integer.parseInt(super.validateSystemNameFormat(name, locale)
446                        .substring(getSystemNamePrefix().length()));
447                parts[0] = Integer.toString((num / 16) + 1); // aiu cab
448                parts[1] = Integer.toString((num % 16) + 1); // aiu pin
449            } catch (NumberFormatException ex) {
450                throw new NamedBean.BadSystemNameException(
451                        Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameNeedCabAndPin", name),
452                        Bundle.getMessage(locale, "InvalidSystemNameNeedCabAndPin", name));
453            }
454        }
455        try {
456            num = Integer.parseInt(parts[0]);
457            if (num < MINAIU || num > MAXAIU) {
458                throw new NamedBean.BadSystemNameException(
459                        Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameBadAIUCab", name),
460                        Bundle.getMessage(locale, "InvalidSystemNameBadAIUCab", name));
461            }
462        } catch (NumberFormatException ex) {
463            throw new NamedBean.BadSystemNameException(
464                    Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameBadAIUCab", name),
465                    Bundle.getMessage(locale, "InvalidSystemNameBadAIUCab", name));
466        }
467        try {
468            num = Integer.parseInt(parts[1]);
469            if (num < 1 || num > MAXPIN) {
470                throw new NamedBean.BadSystemNameException(
471                        Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameBadAIUPin", name),
472                        Bundle.getMessage(locale, "InvalidSystemNameBadAIUPin", name));
473            }
474        } catch (NumberFormatException ex) {
475            throw new NamedBean.BadSystemNameException(
476                    Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameBadAIUCab", name),
477                    Bundle.getMessage(locale, "InvalidSystemNameBadAIUCab", name));
478        }
479        return name;
480    }
481
482    /**
483     * {@inheritDoc}
484     */
485    @Override
486    public NameValidity validSystemNameFormat(@Nonnull String systemName) {
487        if (super.validSystemNameFormat(systemName) == NameValidity.VALID) {
488            try {
489                validateSystemNameFormat(systemName);
490            } catch (IllegalArgumentException ex) {
491                if (systemName.endsWith(":")) {
492                    try {
493                        int num = Integer.parseInt(systemName.substring(getSystemNamePrefix().length(), systemName.length() - 1));
494                        if (num >= MINAIU && num <= MAXAIU) {
495                            return NameValidity.VALID_AS_PREFIX_ONLY;
496                        }
497                    } catch (NumberFormatException | IndexOutOfBoundsException iex) {
498                        // do nothing; will return INVALID
499                    }
500                }
501                return NameValidity.INVALID;
502            }
503        }
504        return NameValidity.VALID;
505    }
506
507    /**
508     * {@inheritDoc}
509     */
510    @Override
511    public String getEntryToolTip() {
512        return Bundle.getMessage("AddInputEntryToolTip");
513    }
514
515    private final static Logger log = LoggerFactory.getLogger(NceSensorManager.class);
516
517}