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