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