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