001package jmri.jmrix.loconet;
002
003import java.util.ArrayList;
004import java.util.Arrays;
005import java.util.Hashtable;
006import java.util.List;
007import java.util.Vector;
008
009import javax.annotation.CheckForNull;
010import javax.annotation.Nonnull;
011import jmri.CommandStation;
012import jmri.ProgListener;
013import jmri.Programmer;
014import jmri.ProgrammingMode;
015import jmri.jmrix.AbstractProgrammer;
016import jmri.jmrix.loconet.SlotMapEntry.SlotType;
017
018/**
019 * Controls a collection of slots, acting as the counter-part of a LocoNet
020 * command station.
021 * <p>
022 * A SlotListener can register to hear changes. By registering here, the
023 * SlotListener is saying that it wants to be notified of a change in any slot.
024 * Alternately, the SlotListener can register with some specific slot, done via
025 * the LocoNetSlot object itself.
026 * <p>
027 * Strictly speaking, functions 9 through 28 are not in the actual slot, but
028 * it's convenient to imagine there's an "extended slot" and keep track of them
029 * here. This is a partial implementation, though, because setting is still done
030 * directly in {@link LocoNetThrottle}. In particular, if this slot has not been
031 * read from the command station, the first message directly setting F9 through
032 * F28 will not have a place to store information. Instead, it will trigger a
033 * slot read, so the following messages will be properly handled.
034 * <p>
035 * Some of the message formats used in this class are Copyright Digitrax, Inc.
036 * and used with permission as part of the JMRI project. That permission does
037 * not extend to uses in other software products. If you wish to use this code,
038 * algorithm or these message formats outside of JMRI, please contact Digitrax
039 * Inc for separate permission.
040 * <p>
041 * This Programmer implementation is single-user only. It's not clear whether
042 * the command stations can have multiple programming requests outstanding (e.g.
043 * service mode and ops mode, or two ops mode) at the same time, but this code
044 * definitely can't.
045 *
046 * @author Bob Jacobsen Copyright (C) 2001, 2003, 2024
047 * @author B. Milhaupt, Copyright (C) 2018, 2024
048 */
049public class SlotManager extends AbstractProgrammer implements LocoNetListener, CommandStation {
050
051    /**
052     * Time to wait after programming operation complete on LocoNet
053     * before reporting completion and hence starting next operation
054     */
055    public static int postProgDelay = 50; // this is public to allow changes via script
056
057    public int slotScanInterval = 50; // this is public to allow changes via script and tests
058
059    /**
060     * The interval between slow scans when checking a InUse or Common slot
061     * that has not been updated within this interval IN SECONDS.
062     * A value of Zero or less disables Slow Scanning.
063     */
064    public double slowScanIntervalOveride =  0.0;
065
066    private double slowScanInterval =  90.0;
067
068    public int serviceModeReplyDelay = 20;  // this is public to allow changes via script and tests. Adjusted by UsbDcs210PlusAdapter
069
070    public int opsModeReplyDelay = 100;  // this is public to allow changes via script and tests.
071
072    public boolean pmManagerGotReply = false;  //this is public to allow changes via script and tests
073
074    public boolean supportsSlot250;
075//    public boolean supportsSlot126;
076    public boolean supportsSlot127;
077
078     /**
079     * a Map of the CS slots.
080     */
081    public List<SlotMapEntry> slotMap = new ArrayList<SlotMapEntry>();
082
083    /**
084     * Constructor for a SlotManager on a given TrafficController.
085     *
086     * @param tc Traffic Controller to be used by SlotManager for communication
087     *          with LocoNet
088     */
089    public SlotManager(LnTrafficController tc) {
090        this.tc = tc;
091
092        // change timeout values from AbstractProgrammer superclass
093        LONG_TIMEOUT = 180000;  // Fleischmann command stations take forever
094        SHORT_TIMEOUT = 8000;   // DCS240 reads
095
096        // dummy slot map until command station set (if ever)
097        slotMap = Arrays.asList(new SlotMapEntry(0,0,SlotType.SYSTEM),
098                    new SlotMapEntry(1,120,SlotType.LOCO),
099                    new SlotMapEntry(121,127,SlotType.SYSTEM),
100                    new SlotMapEntry(128,247,SlotType.UNKNOWN),
101                    new SlotMapEntry(248,256,SlotType.SYSTEM),   // potential stat slots
102                    new SlotMapEntry(257,375,SlotType.UNKNOWN),
103                    new SlotMapEntry(376,384,SlotType.SYSTEM),
104                    new SlotMapEntry(385,432,SlotType.UNKNOWN));
105
106        loadSlots(true);
107
108        // listen to the LocoNet
109        tc.addLocoNetListener(~0, this);
110
111    }
112
113    /**
114     * Initialize the slots array.
115     * @param initialize if true a new slot is created else it is just updated with type
116     *                  and protocol
117     */
118    protected void loadSlots(boolean initialize) {
119        // initialize slot array
120        for (SlotMapEntry item : slotMap) {
121            for (int slotIx = item.getFrom(); slotIx <= item.getTo() ; slotIx++) {
122                if (initialize) {
123                    _slots[slotIx] = new LocoNetSlot( slotIx,getLoconetProtocol(),item.getSlotType());
124                }
125                else {
126                    _slots[slotIx].setSlotType(item.getSlotType());
127                }
128            }
129        }
130    }
131
132    protected LnTrafficController tc;
133
134    /**
135     * Send a DCC packet to the rails. This implements the CommandStation
136     * interface.  This mechanism can pass any valid NMRA packet of up to
137     * 6 data bytes (including the error-check byte).
138     *
139     * When available, these messages are forwarded to LocoNet using a
140     * "throttledTransmitter".  This decreases the speed with which these
141     * messages are sent, resulting in lower throughput, but fewer
142     * rejections by the command station on account of "buffer-overflow".
143     *
144     * @param packet  the data bytes of the raw NMRA packet to be sent.  The
145     *          "error check" byte must be included, even though the LocoNet
146     *          message will not include that byte; the command station
147     *          will re-create the error byte from the bytes encoded in
148     *          the LocoNet message.  LocoNet is unable to propagate packets
149     *          longer than 6 bytes (including the error-check byte).
150     *
151     * @param sendCount  the total number of times the packet is to be
152     *          sent on the DCC track signal (not LocoNet!).  Valid range is
153     *          between 1 and 8.  sendCount will be forced to this range if it
154     *          is outside of this range.
155     */
156    @Override
157    public boolean sendPacket(byte[] packet, int sendCount) {
158        if (sendCount > 8) {
159            log.warn("Ops Mode Accessory Packet 'Send count' reduced from {} to 8.", sendCount); // NOI18N
160            sendCount = 8;
161        }
162        if (sendCount < 1) {
163            log.warn("Ops Mode Accessory Packet 'Send count' of {} is illegal and is forced to 1.", sendCount); // NOI18N
164            sendCount = 1;
165        }
166        if (packet.length <= 1) {
167            log.error("Invalid DCC packet length: {}", packet.length); // NOI18N
168        }
169        if (packet.length > 6) {
170            log.error("DCC packet length is too great: {} bytes were passed; ignoring the request. ", packet.length); // NOI18N
171        }
172
173        LocoNetMessage m = new LocoNetMessage(11);
174        m.setElement(0, LnConstants.OPC_IMM_PACKET);
175        m.setElement(1, 0x0B);
176        m.setElement(2, 0x7F);
177        // the incoming packet includes a check byte that's not included in LocoNet packet
178        int length = packet.length - 1;
179
180        m.setElement(3, ((sendCount - 1) & 0x7) + 16 * (length & 0x7));
181
182        int highBits = 0;
183        if (length >= 1 && ((packet[0] & 0x80) != 0)) {
184            highBits |= 0x01;
185        }
186        if (length >= 2 && ((packet[1] & 0x80) != 0)) {
187            highBits |= 0x02;
188        }
189        if (length >= 3 && ((packet[2] & 0x80) != 0)) {
190            highBits |= 0x04;
191        }
192        if (length >= 4 && ((packet[3] & 0x80) != 0)) {
193            highBits |= 0x08;
194        }
195        if (length >= 5 && ((packet[4] & 0x80) != 0)) {
196            highBits |= 0x10;
197        }
198        m.setElement(4, highBits);
199
200        m.setElement(5, 0);
201        m.setElement(6, 0);
202        m.setElement(7, 0);
203        m.setElement(8, 0);
204        m.setElement(9, 0);
205        for (int i = 0; i < packet.length - 1; i++) {
206            m.setElement(5 + i, packet[i] & 0x7F);
207        }
208
209        if (throttledTransmitter != null) {
210            throttledTransmitter.sendLocoNetMessage(m);
211        } else {
212            tc.sendLocoNetMessage(m);
213        }
214        return true;
215    }
216
217    /*
218     * command station switches
219     */
220    private final int SLOTS_DCS240 = 433;
221    private int numSlots = SLOTS_DCS240;         // This is the largest number so far.
222    private int slot248CommandStationType;
223    private int slot248CommandStationSerial;
224    private int slot250InUseSlots;
225    private int slot250IdleSlots;
226    private int slot250FreeSlots;
227
228    /**
229     * Command station opswitch can be THROWN, CLOSED or NUll
230     */
231    public enum CsOpSwValue {
232        THROWN,
233        CLOSED
234    }
235    private CsOpSwValue[] csOpSw = new CsOpSwValue[129];
236
237    /**
238     * Gets the value of an OpSw if known else Null
239     * @param csOpSwNumber CS op sw number
240     * @return csOpSwValue THROWN CLOSED or null
241     */
242    @CheckForNull
243    public CsOpSwValue getCsOpSw(int csOpSwNumber) {
244        if (csOpSwNumber < 1 || csOpSwNumber > 128) {
245            return null;
246        }
247        return csOpSw[csOpSwNumber];
248    }
249
250    /**
251     * The network protocol.
252     */
253    private int loconetProtocol = LnConstants.LOCONETPROTOCOL_UNKNOWN;    // defaults to unknown
254
255    /**
256     *
257     * @param value the loconet protocol supported
258     */
259    public void setLoconet2Supported(int value) {
260        loconetProtocol = value;
261    }
262
263    /**
264     * Get the Command Station type reported in slot 248 message
265     * @return model
266     */
267    public String getSlot248CommandStationType() {
268        return LnConstants.IPL_NAME(slot248CommandStationType);
269    }
270
271    /**
272     * Get the total number of slots reported in the slot250 message;
273     * @return number of slots
274     */
275    public int getSlot250CSSlots() {
276        return slot250InUseSlots + slot250IdleSlots + slot250FreeSlots;
277    }
278
279    /**
280     *
281     * @return the loconet protocol supported
282     */
283    public int getLoconetProtocol() {
284        return loconetProtocol;
285    }
286
287    /**
288     * Information on slot state is stored in an array of LocoNetSlot objects.
289     * This is declared final because we never need to modify the array itself,
290     * just its contents.
291     */
292    protected LocoNetSlot _slots[] = new LocoNetSlot[getNumSlots()];
293
294    /**
295     * Access the information in a specific slot. Note that this is a mutable
296     * access, so that the information in the LocoNetSlot object can be changed.
297     *
298     * @param i Specific slot, counted starting from zero.
299     * @return The Slot object
300     */
301    public LocoNetSlot slot(int i) {
302        return _slots[i];
303    }
304
305    public int getNumSlots() {
306        return numSlots;
307    }
308    /**
309     * Obtain a slot for a particular loco address.
310     * <p>
311     * This requires access to the command station, even if the locomotive
312     * address appears in the current contents of the slot array, to ensure that
313     * our local image is up-to-date.
314     * <p>
315     * This method sends an info request. When the echo of this is returned from
316     * the LocoNet, the next slot-read is recognized as the response.
317     * <p>
318     * The object that's looking for this information must provide a
319     * SlotListener to notify when the slot ID becomes available.
320     * <p>
321     * The SlotListener is not subscribed for slot notifications; it can do that
322     * later if it wants. We don't currently think that's a race condition.
323     *
324     * @param i Specific slot, counted starting from zero.
325     * @param l The SlotListener to notify of the answer.
326     */
327    public void slotFromLocoAddress (int i, SlotListener l) {
328        // store connection between this address and listener for later
329        mLocoAddrHash.put(Integer.valueOf(i), l);
330
331        // send info request
332        LocoNetMessage m = new LocoNetMessage(4);
333        if (loconetProtocol != LnConstants.LOCONETPROTOCOL_TWO ) {
334            m.setOpCode(LnConstants.OPC_LOCO_ADR);  // OPC_LOCO_ADR
335        } else {
336            m.setOpCode(LnConstants.OPC_EXP_REQ_SLOT); //  Extended slot
337        }
338        m.setElement(1, (i / 128) & 0x7F);
339        m.setElement(2, i & 0x7F);
340        tc.sendLocoNetMessage(m);
341    }
342
343    javax.swing.Timer staleSlotCheckTimer = null;
344
345    /**
346     * Calculate the effective slow scan rate to use.
347     * @return the slowScanInterval to use.
348     */
349    public double getEffectiveslowScanInterval() {
350        double slowScanIntervalToUse = slowScanInterval;
351        if (getCsOpSw(13) == CsOpSwValue.CLOSED) {
352            // with extended purging extend period.
353            slowScanIntervalToUse *= 2;
354        }
355        if (getCsOpSw(14) != null && getCsOpSw(14) == CsOpSwValue.CLOSED) {
356            // with purging disabled dont both slow scanning
357            slowScanIntervalToUse = -1;
358        }
359        if (slowScanIntervalOveride != 0) {
360            slowScanIntervalToUse = slowScanIntervalOveride;
361        }
362        return slowScanIntervalToUse;
363    }
364
365    /**
366     * Scan the slot array looking for slots that are in-use or common but have
367     * not had any updates in over 90s and issue a read slot request to update
368     * their state as the command station may have purged or stopped updating
369     * the slot without telling us via a LocoNet message.
370     * <p>
371     * This is intended to be called from the staleSlotCheckTimer
372     */
373    private void checkStaleSlots() {
374        double slowScanIntervalToUse = getEffectiveslowScanInterval();
375        if (slowScanIntervalToUse > 0) {
376            long staleTimeout = System.currentTimeMillis() - ((long) (slowScanIntervalToUse * 1000)); // 90 seconds ago
377            LocoNetSlot slot;
378
379            // We will just check the normal loco slots 1 to numSlots exclude systemslots
380            for (int i = 1; i < numSlots; i++) {
381                slot = _slots[i];
382                if (!slot.isSystemSlot()) {
383                    if ((((slot.slotStatus() == LnConstants.LOCO_IN_USE
384                            ||    slot.slotStatus() == LnConstants.LOCO_COMMON)
385                            && (slot.consistStatus() == LnConstants.CONSIST_NO))
386                            || (slot.slotStatus() == LnConstants.LOCO_IN_USE && slot.consistStatus() == LnConstants.CONSIST_TOP ))
387                            && slot.getLastUpdateTime() <= staleTimeout ) {
388                        if (slot.getSlowScanStartedAt() == 0) {
389                            slot.setSlowScanStartedAt(System.currentTimeMillis());
390                        }
391                        sendReadSlot(i);
392                        break; // only send the first one found
393                    } else if (((slot.slotStatus() != LnConstants.LOCO_IN_USE
394                                 && slot.slotStatus() != LnConstants.LOCO_COMMON
395                                 && slot.consistStatus() == LnConstants.CONSIST_NO) )
396                            || ( slot.slotStatus() != LnConstants.LOCO_IN_USE
397                                 && slot.consistStatus() == LnConstants.CONSIST_TOP) ) {
398                        slot.setSlowScanStartedAt(0);
399                    }
400                }
401            }
402        }
403    }
404
405    java.util.TimerTask slot250Task = null;
406    /**
407     * Request slot data for 248 and 250
408     * Runs delayed
409     * <p>
410     * A call is trigger after the first slot response (PowerManager) received.
411     */
412    private void pollSpecialSlots() {
413        sendReadSlot(248);
414        slot250Task = new java.util.TimerTask() {
415            @Override
416            public void run() {
417                try {
418                    sendReadSlot(250);
419                } catch (Exception e) {
420                    log.error("Exception occurred while checking slot250", e);
421                }
422            }
423        };
424        jmri.util.TimerUtil.schedule(slot250Task,100);
425    }
426
427    /**
428     * Provide a mapping between locomotive addresses and the SlotListener
429     * that's interested in them.
430     */
431    Hashtable<Integer, SlotListener> mLocoAddrHash = new Hashtable<>();
432
433    // data members to hold contact with the slot listeners
434    private final Vector<SlotListener> slotListeners = new Vector<>();
435
436    /**
437     * Add a slot listener, if it is not already registered
438     * <p>
439     * The slot listener will be invoked every time a slot changes state.
440     *
441     * @param l Slot Listener to be added
442     */
443    public synchronized void addSlotListener(SlotListener l) {
444        // add only if not already registered
445        if (!slotListeners.contains(l)) {
446            slotListeners.addElement(l);
447        }
448    }
449
450    /**
451     * Add a slot listener, if it is registered.
452     * <p>
453     * The slot listener will be removed from the list of listeners which are
454     * invoked whenever a slot changes state.
455     *
456     * @param l Slot Listener to be removed
457     */
458    public synchronized void removeSlotListener(SlotListener l) {
459        if (slotListeners.contains(l)) {
460            slotListeners.removeElement(l);
461        }
462    }
463
464    /**
465     * Trigger the notification of all SlotListeners.
466     *
467     * @param s The changed slot to notify.
468     */
469    @SuppressWarnings("unchecked")
470    protected void notify(LocoNetSlot s) {
471        // make a copy of the listener vector to synchronized not needed for transmit
472        Vector<SlotListener> v;
473        synchronized (this) {
474            v = (Vector<SlotListener>) slotListeners.clone();
475        }
476        log.debug("notify {} SlotListeners about slot {}", // NOI18N
477                v.size(), s.getSlot());
478        // forward to all listeners
479        int cnt = v.size();
480        for (int i = 0; i < cnt; i++) {
481            SlotListener client = v.elementAt(i);
482            client.notifyChangedSlot(s);
483        }
484    }
485
486    LocoNetMessage immedPacket;
487
488    /**
489     * Listen to the LocoNet. This is just a steering routine, which invokes
490     * others for the various processing steps.
491     *
492     * @param m incoming message
493     */
494    @Override
495    public void message(LocoNetMessage m) {
496        if (m.getOpCode() == LnConstants.OPC_RE_LOCORESET_BUTTON) {
497            if (commandStationType.getSupportsLocoReset()) {
498                // Command station LocoReset button was triggered.
499                //
500                // Note that sending a LocoNet message using this OpCode to the command
501                // station does _not_ seem to trigger the equivalent effect; only
502                // pressing the button seems to do so.
503                // If the OpCode is received by JMRI, regardless of its source,
504                // JMRI will simply trigger a re-read of all slots.  This will
505                // allow the JMRI slots to stay consistent with command station
506                // slot information, regardless of whether the command station
507                // just modified the slot information.
508                javax.swing.Timer t = new javax.swing.Timer(500, (java.awt.event.ActionEvent e) -> {
509                    log.debug("Updating slots account received opcode 0x8a message");   // NOI18N
510                    update(slotMap,slotScanInterval);
511                });
512                t.stop();
513                t.setInitialDelay(500);
514                t.setRepeats(false);
515                t.start();
516            }
517            return;
518        }
519
520        // LACK processing for resend of immediate command
521        if (!mTurnoutNoRetry && immedPacket != null &&
522                m.getOpCode() == LnConstants.OPC_LONG_ACK &&
523                m.getElement(1) == 0x6D && m.getElement(2) == 0x00) {
524            // LACK reject, resend immediately
525            tc.sendLocoNetMessage(immedPacket);
526            immedPacket = null;
527        }
528        if (m.getOpCode() == LnConstants.OPC_IMM_PACKET &&
529                m.getElement(1) == 0x0B && m.getElement(2) == 0x7F) {
530            immedPacket = m;
531        } else {
532            immedPacket = null;
533        }
534
535        // slot specific message?
536        int i = findSlotFromMessage(m);
537        if (i != -1) {
538            getMoreDetailsForSlot(m, i);
539            checkSpecialSlots(m, i);
540            forwardMessageToSlot(m, i);
541            respondToAddrRequest(m, i);
542            programmerOpMessage(m, i);
543            checkLoconetProtocol(m,i);
544        }
545
546        // LONG_ACK response?
547        if (m.getOpCode() == LnConstants.OPC_LONG_ACK) {
548            handleLongAck(m);
549        }
550
551        // see if extended function message
552        if (isExtFunctionMessage(m)) {
553            // yes, get address
554            int addr = getDirectFunctionAddress(m);
555            // find slot(s) containing this address
556            // and route message to them
557            boolean found = false;
558            for (int j = 0; j < 120; j++) {
559                LocoNetSlot slot = slot(j);
560                if (slot == null) {
561                    continue;
562                }
563                if ((slot.locoAddr() != addr)
564                        || (slot.slotStatus() == LnConstants.LOCO_FREE)) {
565                    continue;
566                }
567                // found!
568                slot.functionMessage(getDirectDccPacket(m));
569                found = true;
570            }
571            if (!found) {
572                // rats! Slot not loaded since program start.  Request it be
573                // reloaded for later, but that'll be too late
574                // for this one.
575                LocoNetMessage mo = new LocoNetMessage(4);
576                mo.setOpCode(LnConstants.OPC_LOCO_ADR);  // OPC_LOCO_ADR
577                mo.setElement(1, (addr / 128) & 0x7F);
578                mo.setElement(2, addr & 0x7F);
579                tc.sendLocoNetMessage(mo);
580            }
581        }
582    }
583
584    /*
585     * Collect data from specific slots
586     */
587    void checkSpecialSlots(LocoNetMessage m, int slot) {
588        if (!pmManagerGotReply && slot == 0 &&
589                (m.getOpCode() == LnConstants.OPC_EXP_RD_SL_DATA || m.getOpCode() == LnConstants.OPC_SL_RD_DATA)) {
590            pmManagerGotReply = true;
591            if (supportsSlot250) {
592                pollSpecialSlots();
593            }
594            return;
595        }
596
597        if (m.getElement(1) != 0x15) {
598            // check short special slots
599            int opSwNo  = -1;  // will be 1 for slot 127, 65 for 126
600            if (supportsSlot127 && slot == 127) {
601                opSwNo = 1;
602            }
603//            else if (supportsSlot126 && slot == 126) {
604//                opSwNo = 65;
605//            }
606            if (opSwNo > 0 ) {
607                int[] numbers = {3,4,5,6,8,9,10,11};   // skips power/status byte
608                for ( int i: numbers) {
609                    int b = m.getElement(i);
610                    for (int x = 0 ; x < 8 ; x++) {
611                        csOpSw[opSwNo] = ((b & 0x01) == 0x01) ? CsOpSwValue.CLOSED : CsOpSwValue.THROWN;
612                        log.debug("CS OpSw [{}] is {}", opSwNo, csOpSw[opSwNo].name());
613                        opSwNo++;
614                        b = b >> 1;
615                    }
616                }
617            }
618            return;
619        }
620
621        switch (slot) {
622            case 250:
623                // slot info if we have serial, the serial number in this slot
624                // does not indicate whether in booster or cs mode.
625                if (slot248CommandStationSerial == ((m.getElement(19) & 0x3F) * 128) + m.getElement(18)) {
626                    slot250InUseSlots = (m.getElement(4) + ((m.getElement(5) & 0x03) * 128));
627                    slot250IdleSlots = (m.getElement(6) + ((m.getElement(7) & 0x03) * 128));
628                    slot250FreeSlots = (m.getElement(8) + ((m.getElement(9) & 0x03) * 128));
629                }
630                break;
631            case 248:
632                // Base HW Information
633                // If a CS in CS mode then byte 19 bit 6 in on. else its in
634                // booster mode
635                // The device type is in byte 14
636                if ((m.getElement(19) & 0x40) == 0x40) {
637                    slot248CommandStationSerial = ((m.getElement(19) & 0x3F) * 128) + m.getElement(18);
638                    slot248CommandStationType = m.getElement(14);
639                }
640                break;
641            default:
642        }
643    }
644
645    /*
646     * If protocol not yet established use slot status for protocol support
647     * System slots , except zero, do not have this info
648     */
649    void checkLoconetProtocol(LocoNetMessage m, int slot) {
650        // detect protocol if not yet set
651        if (getLoconetProtocol() == LnConstants.LOCONETPROTOCOL_UNKNOWN) {
652            if (_slots[slot].getSlotType() != SlotType.SYSTEM || slot == 0) {
653                if ((m.getOpCode() == LnConstants.OPC_EXP_RD_SL_DATA && m.getNumDataElements() == 21) ||
654                        (m.getOpCode() == LnConstants.OPC_SL_RD_DATA)) {
655                    if ((m.getElement(7) & 0b01000000) == 0b01000000) {
656                        log.info("Setting protocol Loconet 2");
657                        setLoconet2Supported(LnConstants.LOCONETPROTOCOL_TWO);
658                    } else {
659                        log.info("Setting protocol Loconet 1");
660                        setLoconet2Supported(LnConstants.LOCONETPROTOCOL_ONE);
661                    }
662                }
663            }
664        }
665    }
666
667    /**
668     * Checks a LocoNet message to see if it encodes a DCC "direct function" packet.
669     *
670     * @param m  a LocoNet Message
671     * @return the loco address if the LocoNet message encodes a "direct function" packet,
672     * else returns -1
673     */
674    int getDirectFunctionAddress(LocoNetMessage m) {
675        if (m.getOpCode() != LnConstants.OPC_IMM_PACKET) {
676            return -1;
677        }
678        if (m.getElement(1) != 0x0B) {
679            return -1;
680        }
681        if (m.getElement(2) != 0x7F) {
682            return -1;
683        }
684        // Direct packet, check length
685        if ((m.getElement(3) & 0x70) < 0x20) {
686            return -1;
687        }
688        int addr = -1;
689        // check long address
690        if ((m.getElement(4) & 0x01) == 0) { //bit 7=0 short
691            addr = (m.getElement(5) & 0xFF);
692            if ((m.getElement(4) & 0x01) != 0) {
693                addr += 128;  // and high bit
694            }
695        } else if ((m.getElement(5) & 0x40) == 0x40) { // bit 7 = 1 if bit 6 = 1 then long
696            addr = (m.getElement(5) & 0x3F) * 256 + (m.getElement(6) & 0xFF);
697            if ((m.getElement(4) & 0x02) != 0) {
698                addr += 128;  // and high bit
699            }
700        } else { // accessory decoder or extended accessory decoder
701            addr = (m.getElement(5) & 0x3F);
702        }
703        return addr;
704    }
705
706    /**
707     * Extracts a DCC "direct packet" from a LocoNet message, if possible.
708     * <p>
709     * if this is a direct DCC packet, return as one long
710     * else return -1. Packet does not include address bytes.
711     *
712     * @param m a LocoNet message to be inspected
713     * @return an integer containing the bytes of the DCC packet, except the address bytes.
714     */
715    int getDirectDccPacket(LocoNetMessage m) {
716        if (m.getOpCode() != LnConstants.OPC_IMM_PACKET) {
717            return -1;
718        }
719        if (m.getElement(1) != 0x0B) {
720            return -1;
721        }
722        if (m.getElement(2) != 0x7F) {
723            return -1;
724        }
725        // Direct packet, check length
726        if ((m.getElement(3) & 0x70) < 0x20) {
727            return -1;
728        }
729        int result = 0;
730        int n = (m.getElement(3) & 0xF0) / 16;
731        int start;
732        int high = m.getElement(4);
733        // check long or short address
734        if ((m.getElement(4) & 0x01) == 1 && (m.getElement(5) & 0x40) == 0x40 ) {  //long address bit 7 im1 = 1 and bit6 im1 = 1
735            start = 7;
736            high = high >> 2;
737            n = n - 2;
738         } else {  //short or accessory
739            start = 6;
740            high = high >> 1;
741            n = n - 1;
742        }
743        // get result
744        for (int i = 0; i < n; i++) {
745            result = result * 256 + (m.getElement(start + i) & 0x7F);
746            if ((high & 0x01) != 0) {
747                result += 128;
748            }
749            high = high >> 1;
750        }
751        return result;
752    }
753
754    /**
755     * Determines if a LocoNet message encodes a direct request to control
756     * DCC functions F9 thru F28
757     *
758     * @param m the LocoNet message to be evaluated
759     * @return true if the message is an external DCC packet request for F9-F28,
760     *      else false.
761     */
762    boolean isExtFunctionMessage(LocoNetMessage m) {
763        int pkt = getDirectDccPacket(m);
764        if (pkt < 0) {
765            return false;
766        }
767        // check F9-12
768        if ((pkt & 0xFFFFFF0) == 0xA0) {
769            return true;
770        }
771        // check F13-28
772        if ((pkt & 0xFFFFFE00) == 0xDE00) {
773            return true;
774        }
775        return false;
776    }
777
778    /**
779     * Extracts the LocoNet slot number from a LocoNet message, if possible.
780     * <p>
781     * Find the slot number that a message references
782     * <p>
783     * This routine only looks for explicit slot references; it does not, for example,
784     * identify a loco address in the message and then work thru the slots to find a
785     * slot which references that loco address.
786     *
787     * @param m LocoNet Message to be inspected
788     * @return an integer representing the slot number encoded in the LocoNet
789     *          message, or -1 if the message does not contain a slot reference
790     */
791    public int findSlotFromMessage(LocoNetMessage m) {
792
793        int i = -1;  // find the slot index in the message and store here
794
795        // decode the specific message type and hence slot number
796        switch (m.getOpCode()) {
797            case LnConstants.OPC_WR_SL_DATA:
798            case LnConstants.OPC_SL_RD_DATA:
799                i = m.getElement(2);
800                break;
801            case LnConstants.OPC_EXP_SLOT_MOVE_RE_OPC_IB2_SPECIAL:
802                if ( m.getElement(1) == LnConstants.RE_IB2_SPECIAL_FUNCS_TOKEN) {
803                    i = m.getElement(2);
804                    break;
805                }
806                i = ( (m.getElement(1) & 0x03 ) *128) + m.getElement(2);
807                break;
808            case LnConstants.OPC_LOCO_DIRF:
809            case LnConstants.OPC_LOCO_SND:
810            case LnConstants.OPC_LOCO_SPD:
811            case LnConstants.OPC_SLOT_STAT1:
812            case LnConstants.OPC_LINK_SLOTS:
813            case LnConstants.OPC_UNLINK_SLOTS:
814                i = m.getElement(1);
815                break;
816
817            case LnConstants.OPC_MOVE_SLOTS:  // No follow on for some moves
818                if (m.getElement(1) != 0) {
819                    i = m.getElement(1);
820                    return i;
821                }
822                break;
823            case LnConstants.OPC_EXP_SEND_FUNCTION_OR_SPEED_AND_DIR:
824                i = ( (m.getElement(1) & 0x03 ) *128) + m.getElement(2);
825                break;
826            case LnConstants.OPC_EXP_RD_SL_DATA:
827            case LnConstants.OPC_EXP_WR_SL_DATA:
828                //only certain lengths get passed to slot
829                if (m.getElement(1) == 21) {
830                    i = ( (m.getElement(2) & 0x03 ) *128) + m.getElement(3);
831                }
832                return i;
833            default:
834                // nothing here for us
835                return i;
836        }
837        // break gets to here
838        return i;
839    }
840
841    /**
842     * Check CV programming LONG_ACK message byte 1
843     * <p>
844     * The following methods are for parsing LACK as response to CV programming.
845     * It is divided into numerous small methods so that each bit can be
846     * overridden for special parsing for individual command station types.
847     *
848     * @param byte1 from the LocoNet message
849     * @return true if byte1 encodes a response to a OPC_SL_WRITE or an
850     *          Expanded Slot Write
851     */
852    protected boolean checkLackByte1(int byte1) {
853        if ((byte1 & 0xEF) == 0x6F) {
854            return true;
855        } else {
856            return false;
857        }
858    }
859
860    /**
861     * Checks the status byte of an OPC_LONG_ACK when performing CV programming
862     * operations.
863     *
864     * @param byte2 status byte
865     * @return True if status byte indicates acceptance of the command, else false.
866     */
867    protected boolean checkLackTaskAccepted(int byte2) {
868        if (byte2 == 1 // task accepted
869                || byte2 == 0x23 || byte2 == 0x2B || byte2 == 0x6B // added as DCS51 fix
870                // deliberately ignoring 0x7F varient, see okToIgnoreLack
871            ) {
872            return true;
873        } else {
874            return false;
875        }
876    }
877
878    /**
879     * Checks the OPC_LONG_ACK status byte response to a programming
880     * operation.
881     *
882     * @param byte2 from the OPC_LONG_ACK message
883     * @return true if the programmer returned "busy" else false
884     */
885    protected boolean checkLackProgrammerBusy(int byte2) {
886        if (byte2 == 0) {
887            return true;
888        } else {
889            return false;
890        }
891    }
892
893    /**
894     * Checks the OPC_LONG_ACK status byte response to a programming
895     * operation to see if the programmer accepted the operation "blindly".
896     *
897     * @param byte2 from the OPC_LONG_ACK message
898     * @return true if the programmer indicated a "blind operation", else false
899     */
900    protected boolean checkLackAcceptedBlind(int byte2) {
901        if (byte2 == 0x40) {
902            return true;
903        } else {
904            return false;
905        }
906    }
907
908    /**
909     * Some LACKs with specific OPC_LONG_ACK status byte values can just be ignored.
910     *
911     * @param byte2 from the OPC_LONG_ACK message
912     * @return true if this form of LACK can be ignored without a warning message
913     */
914    protected boolean okToIgnoreLack(int byte2) {
915        if (byte2 == 0x7F ) {
916            return true;
917        } else {
918            return false;
919        }
920    }
921
922    private boolean acceptAnyLACK = false;
923    /**
924     * Indicate that the command station LONG_ACK response details can be ignored
925     * for this operation.  Typically this is used when accessing Loconet-attached boards.
926     */
927    public final void setAcceptAnyLACK() {
928        acceptAnyLACK = true;
929    }
930
931    /**
932     * Handles OPC_LONG_ACK replies to programming slot operations.
933     *
934     * LACK 0x6D00 which requests a retransmission is handled
935     * separately in the message(..) method.
936     *
937     * @param m LocoNet message being analyzed
938     */
939    protected void handleLongAck(LocoNetMessage m) {
940        // handle if reply to slot. There's no slot number in the LACK, unfortunately.
941        // If this is a LACK to a Slot op, and progState is command pending,
942        // assume its for us...
943        log.debug("LACK in state {} message: {}", progState, m.toString()); // NOI18N
944        if (checkLackByte1(m.getElement(1)) && progState == 1) {
945            // in programming state
946            if (acceptAnyLACK) {
947                log.debug("accepted LACK {} via acceptAnyLACK", m.getElement(2));
948                // Any form of LACK response from CS is accepted here.
949                // Loconet-attached decoders (LOCONETOPSBOARD) receive the program commands
950                // directly via loconet and respond as required without needing any CS action,
951                // making the details of the LACK response irrelevant.
952                if (_progRead || _progConfirm) {
953                    // move to commandExecuting state
954                    startShortTimer();
955                    progState = 2;
956                } else {
957                    // move to not programming state
958                    progState = 0;
959                    stopTimer();
960                    // allow the target device time to execute then notify ProgListener
961                    notifyProgListenerEndAfterDelay();
962                }
963                acceptAnyLACK = false;      // restore normal state for next operation
964            }
965            // check status byte
966            else if (checkLackTaskAccepted(m.getElement(2))) { // task accepted
967                // 'not implemented' (op on main)
968                // but BDL16 and other devices can eventually reply, so
969                // move to commandExecuting state
970                log.debug("checkLackTaskAccepted accepted, next state 2"); // NOI18N
971                if ((_progRead || _progConfirm) && mServiceMode) {
972                    startLongTimer();
973                } else {
974                    startShortTimer();
975                }
976                progState = 2;
977            } else if (checkLackProgrammerBusy(m.getElement(2))) { // task aborted as busy
978                // move to not programming state
979                progState = 0;
980                // notify user ProgListener
981                stopTimer();
982                notifyProgListenerLack(jmri.ProgListener.ProgrammerBusy);
983            } else if (checkLackAcceptedBlind(m.getElement(2))) { // task accepted blind
984                if ((_progRead || _progConfirm) && !mServiceMode) { // incorrect Reserved OpSw setting can cause this response to OpsMode Read
985                    // just treat it as a normal OpsMode Read response
986                    // move to commandExecuting state
987                    log.debug("LACK accepted (ignoring incorrect OpSw), next state 2"); // NOI18N
988                    startShortTimer();
989                    progState = 2;
990                } else {
991                    // move to not programming state
992                    progState = 0;
993                    stopTimer();
994                    // allow command station time to execute then notify ProgListener
995                    notifyProgListenerEndAfterDelay();
996                }
997            } else if (okToIgnoreLack(m.getElement(2))) {
998                // this form of LACK can be silently ignored
999                log.debug("Ignoring LACK with {}", m.getElement(2));
1000            } else { // not sure how to cope, so complain
1001                log.warn("unexpected LACK reply code {}", m.getElement(2)); // NOI18N
1002                // move to not programming state
1003                progState = 0;
1004                // notify user ProgListener
1005                stopTimer();
1006                notifyProgListenerLack(jmri.ProgListener.UnknownError);
1007            }
1008        }
1009    }
1010
1011    /**
1012     * Internal method to notify ProgListener after a short delay that the operation is complete.
1013     * The delay ensures that the target device has completed the operation prior to the notification.
1014     */
1015    protected void notifyProgListenerEndAfterDelay() {
1016        javax.swing.Timer timer = new javax.swing.Timer(postProgDelay, new java.awt.event.ActionListener() {
1017            @Override
1018            public void actionPerformed(java.awt.event.ActionEvent e) {
1019                notifyProgListenerEnd(-1, 0); // no value (e.g. -1), no error status (e.g.0)
1020            }
1021        });
1022        timer.stop();
1023        timer.setInitialDelay(postProgDelay);
1024        timer.setRepeats(false);
1025        timer.start();
1026    }
1027
1028    /**
1029     * Forward Slot-related LocoNet message to the slot.
1030     *
1031     * @param m a LocoNet message targeted at a slot
1032     * @param i the slot number to which the LocoNet message is targeted.
1033     */
1034    public void forwardMessageToSlot(LocoNetMessage m, int i) {
1035
1036        // if here, i holds the slot number, and we expect to be able to parse
1037        // and have the slot handle the message
1038        if (i >= _slots.length || i < 0) {
1039            log.error("Received slot number {} is greater than array length {} Message was {}", // NOI18N
1040                    i, _slots.length, m.toString()); // NOI18N
1041            return; // prevents array index out-of-bounds when referencing _slots[i]
1042        }
1043
1044        if ( !validateSlotNumber(i)) {
1045            log.warn("Received slot number {} is not in the slot map, have you defined the wrong cammand station type? Message was {}",
1046                   i,  m.toString());
1047        }
1048
1049        try {
1050            _slots[i].setSlot(m);
1051        } catch (LocoNetException e) {
1052            // must not have been interesting, or at least routed right
1053            log.error("slot rejected LocoNetMessage {}", m); // NOI18N
1054            return;
1055        } catch (Exception e) {
1056            log.error("Unexplained error _slots[{}].setSlot({})",i,m,e);
1057            return;
1058        }
1059        // notify listeners that slot may have changed
1060        notify(_slots[i]);
1061    }
1062
1063    /**
1064     * A sort of slot listener which handles loco address requests
1065     *
1066     * @param m a LocoNet message
1067     * @param i the slot to which it is directed
1068     */
1069    protected void respondToAddrRequest(LocoNetMessage m, int i) {
1070        // is called any time a LocoNet message is received.  Note that we do _NOT_ know why a given message happens!
1071
1072        // if this is OPC_SL_RD_DATA
1073        if (m.getOpCode() == LnConstants.OPC_SL_RD_DATA || m.getOpCode() == LnConstants.OPC_EXP_RD_SL_DATA ) {
1074            // yes, see if request exists
1075            // note that the appropriate _slots[] entry has already been updated
1076            // to reflect the content of the LocoNet message, so _slots[i]
1077            // has the locomotive address of this request
1078            int addr = _slots[i].locoAddr();
1079            log.debug("LOCO_ADR resp is slot {} for addr {}", i, addr); // NOI18N
1080            SlotListener l = mLocoAddrHash.get(Integer.valueOf(addr));
1081            if (l != null) {
1082                // only notify once per request
1083                mLocoAddrHash.remove(Integer.valueOf(addr));
1084                // and send the notification
1085                log.debug("notify listener"); // NOI18N
1086                l.notifyChangedSlot(_slots[i]);
1087            } else {
1088                log.debug("no request for addr {}", addr); // NOI18N
1089            }
1090        }
1091    }
1092
1093    /**
1094     * If it is a slot being sent COMMON,
1095     *  after a delay, get the new status of the slot
1096     * If it is a true slot move, not dispatch or null
1097     *  after a delay, get the new status of the from slot, which varies by CS.
1098     *  the to slot should come in the reply.
1099     * @param m a LocoNet message
1100     * @param i the slot to which it is directed
1101     */
1102    protected void getMoreDetailsForSlot(LocoNetMessage m, int i) {
1103        // is called any time a LocoNet message is received.
1104        // sets up delayed slot read to update our effected slots to match the CS
1105        if (m.getOpCode() == LnConstants.OPC_SLOT_STAT1 &&
1106                ((m.getElement(2) & LnConstants.LOCOSTAT_MASK) == LnConstants.LOCO_COMMON ) ) {
1107            // Changing a slot to common. Depending on a CS and its OpSw, and throttle speed
1108            // it could have its status changed a number of ways.
1109            sendReadSlotDelayed(i,100);
1110        } else if (m.getOpCode() == LnConstants.OPC_EXP_SLOT_MOVE_RE_OPC_IB2_SPECIAL) {
1111            boolean isSettingStatus = ((m.getElement(3) & 0b01110000) == 0b01100000);
1112            if (isSettingStatus) {
1113                int stat = m.getElement(4);
1114                if ((stat & LnConstants.LOCOSTAT_MASK) == LnConstants.LOCO_COMMON) {
1115                    sendReadSlotDelayed(i,100);
1116                }
1117            }
1118            boolean isUnconsisting = ((m.getElement(3) & 0b01110000) == 0b01010000);
1119            if (isUnconsisting) {
1120                // read lead slot
1121                sendReadSlotDelayed(slot(i).getLeadSlot(),100);
1122            }
1123            boolean isConsisting = ((m.getElement(3) & 0b01110000) == 0b01000000);
1124            if (isConsisting) {
1125                // read 2nd slot
1126                int slotTwo = ((m.getElement(3) & 0b00000011) * 128 )+ m.getElement(4);
1127                sendReadSlotDelayed(slotTwo,100);
1128            }
1129        } else if (m.getOpCode() == LnConstants.OPC_MOVE_SLOTS) {
1130            // if a true move get the new from slot status
1131            // the to slot status is sent in the reply, but not if dispatch or null
1132            // as those return slot info.
1133            int slotTwo;
1134            slotTwo = m.getElement(2);
1135            if (i != 0 && slotTwo != 0 && i != slotTwo) {
1136                sendReadSlotDelayed(i,100);
1137            }
1138        } else if (m.getOpCode() == LnConstants.OPC_LINK_SLOTS ||
1139                m.getOpCode() == LnConstants.OPC_UNLINK_SLOTS ) {
1140            // unlink and link return first slot by not second (to or from)
1141            // the to slot status is sent in the reply
1142            int slotTwo;
1143            slotTwo = m.getElement(2);
1144            if (i != 0 && slotTwo != 0) {
1145                sendReadSlotDelayed(slotTwo,100);
1146            }
1147       }
1148    }
1149
1150    /**
1151     * Schedule a delayed slot read.
1152     * @param slotNo - the slot.
1153     * @param delay - delay in msecs.
1154     */
1155    protected void sendReadSlotDelayed(int slotNo, long delay) {
1156        java.util.TimerTask meterTask = new java.util.TimerTask() {
1157            int slotNumber = slotNo;
1158
1159            @Override
1160            public void run() {
1161                try {
1162                    sendReadSlot(slotNumber);
1163                } catch (Exception e) {
1164                    log.error("Exception occurred sendReadSlotDelayed:", e);
1165                }
1166            }
1167        };
1168        jmri.util.TimerUtil.schedule(meterTask, delay);
1169    }
1170
1171    /**
1172     * Handle LocoNet messages related to CV programming operations
1173     *
1174     * @param m a LocoNet message
1175     * @param i the slot toward which the message is destined
1176     */
1177    protected void programmerOpMessage(LocoNetMessage m, int i) {
1178
1179        // start checking for programming operations in slot 124
1180        if (i == 124) {
1181            // here its an operation on the programmer slot
1182            log.debug("Prog Message {} for slot 124 in state {}", // NOI18N
1183                    m.getOpCodeHex(), progState); // NOI18N
1184            switch (progState) {
1185                case 0:   // notProgramming
1186                    break;
1187                case 1:   // commandPending: waiting for an (optional) LACK
1188                case 2:   // commandExecuting
1189                    // waiting for slot read, is it present?
1190                    if (m.getOpCode() == LnConstants.OPC_SL_RD_DATA) {
1191                        log.debug("  was OPC_SL_RD_DATA"); // NOI18N
1192                        // yes, this is the end
1193                        // move to not programming state
1194                        stopTimer();
1195                        progState = 0;
1196
1197                        // parse out value returned
1198                        int value = -1;
1199                        int status = 0;
1200                        if (_progConfirm) {
1201                            // read command, get value; check if OK
1202                            value = _slots[i].cvval();
1203                            if (value != _confirmVal) {
1204                                status = status | jmri.ProgListener.ConfirmFailed;
1205                            }
1206                        }
1207                        if (_progRead) {
1208                            // read command, get value
1209                            value = _slots[i].cvval();
1210                        }
1211                        // parse out status
1212                        if ((_slots[i].pcmd() & LnConstants.PSTAT_NO_DECODER) != 0) {
1213                            status = (status | jmri.ProgListener.NoLocoDetected);
1214                        }
1215                        if ((_slots[i].pcmd() & LnConstants.PSTAT_WRITE_FAIL) != 0) {
1216                            status = (status | jmri.ProgListener.NoAck);
1217                        }
1218                        if ((_slots[i].pcmd() & LnConstants.PSTAT_READ_FAIL) != 0) {
1219                            status = (status | jmri.ProgListener.NoAck);
1220                        }
1221                        if ((_slots[i].pcmd() & LnConstants.PSTAT_USER_ABORTED) != 0) {
1222                            status = (status | jmri.ProgListener.UserAborted);
1223                        }
1224
1225                        // and send the notification
1226                        notifyProgListenerEnd(value, status);
1227                    }
1228                    break;
1229                default:  // error!
1230                    log.error("unexpected programming state {}", progState); // NOI18N
1231                    break;
1232            }
1233        }
1234    }
1235
1236    ProgrammingMode csOpSwProgrammingMode = new ProgrammingMode(
1237            "LOCONETCSOPSWMODE",
1238            Bundle.getMessage("LOCONETCSOPSWMODE"));
1239
1240    // members for handling the programmer interface
1241
1242    /**
1243     * Return a list of ProgrammingModes supported by this interface
1244     * Types implemented here.
1245     *
1246     * @return a List of ProgrammingMode objects containing the supported
1247     *          programming modes.
1248     */
1249
1250    @Override
1251    @Nonnull
1252    public List<ProgrammingMode> getSupportedModes() {
1253        List<ProgrammingMode> ret = new ArrayList<>();
1254        ret.add(ProgrammingMode.DIRECTBYTEMODE);
1255        ret.add(ProgrammingMode.PAGEMODE);
1256        ret.add(ProgrammingMode.REGISTERMODE);
1257        ret.add(ProgrammingMode.ADDRESSMODE);
1258        ret.add(csOpSwProgrammingMode);
1259
1260        return ret;
1261    }
1262
1263    /**
1264     * Remember whether the attached command station needs a sequence sent after
1265     * programming. The default operation is implemented in doEndOfProgramming
1266     * and turns power back on by sending a GPON message.
1267     */
1268    private boolean mProgEndSequence = false;
1269
1270    /**
1271     * Remember whether the attached command station can read from Decoders.
1272     */
1273    private boolean mCanRead = true;
1274
1275    /**
1276     * Determine whether this Programmer implementation is capable of reading
1277     * decoder contents. This is entirely determined by the attached command
1278     * station, not the code here, so it refers to the mCanRead member variable
1279     * which is recording the known state of that.
1280     *
1281     * @return True if reads are possible
1282     */
1283    @Override
1284    public boolean getCanRead() {
1285        return mCanRead;
1286    }
1287
1288    /**
1289     * Return the write confirm mode implemented by the command station.
1290     * <p>
1291     * Service mode always checks for DecoderReply. (The DCS240 also seems to do
1292     * ReadAfterWrite, but that's not fully understood yet)
1293     *
1294     * @param addr This implementation ignores this parameter
1295     * @return the supported WriteConfirmMode
1296     */
1297    @Nonnull
1298    @Override
1299    public Programmer.WriteConfirmMode getWriteConfirmMode(String addr) { return WriteConfirmMode.DecoderReply; }
1300
1301    /**
1302     * Set the command station type to one of the known types in the
1303     * {@link LnCommandStationType} enum.
1304     *
1305     * @param value contains the command station type
1306     */
1307    public void setCommandStationType(LnCommandStationType value) {
1308        commandStationType = value;
1309        mCanRead = value.getCanRead();
1310        mProgEndSequence = value.getProgPowersOff();
1311        slotMap = commandStationType.getSlotMap();
1312        supportsSlot250 = value.getSupportsSlot250();
1313//        supportsSlot126 = value.getSupportsSlot126();
1314        supportsSlot127 = value.getSupportsSlot127();
1315        loadSlots(false);
1316
1317        // We will scan the slot table every 0.3 s for in-use slots that are stale
1318        final int slotScanDelay = 300; // Must be short enough that 128 can be scanned in 90 seconds, see checkStaleSlots()
1319        staleSlotCheckTimer = new javax.swing.Timer(slotScanDelay, new java.awt.event.ActionListener() {
1320            @Override
1321            public void actionPerformed(java.awt.event.ActionEvent e) {
1322                checkStaleSlots();
1323            }
1324        });
1325
1326        staleSlotCheckTimer.setRepeats(true);
1327        staleSlotCheckTimer.setInitialDelay(30000);  // wait a bit at startup
1328        staleSlotCheckTimer.start();
1329
1330    }
1331
1332    LocoNetThrottledTransmitter throttledTransmitter = null;
1333    boolean mTurnoutNoRetry = false;
1334
1335    /**
1336     * Provide a ThrottledTransmitter for sending immediate packets.
1337     *
1338     * @param value contains a LocoNetThrottledTransmitter object
1339     * @param m contains a boolean value indicating mTurnoutNoRetry
1340     */
1341    public void setThrottledTransmitter(LocoNetThrottledTransmitter value, boolean m) {
1342        throttledTransmitter = value;
1343        mTurnoutNoRetry = m;
1344    }
1345
1346    /**
1347     * Get the command station type.
1348     *
1349     * @return an LnCommandStationType object
1350     */
1351    public LnCommandStationType getCommandStationType() {
1352        return commandStationType;
1353    }
1354
1355    protected LnCommandStationType commandStationType = null;
1356
1357    /**
1358     * Internal routine to handle a timeout.
1359     */
1360    @Override
1361    synchronized protected void timeout() {
1362        log.debug("timeout fires in state {}", progState); // NOI18N
1363
1364        if (progState != 0) {
1365            // we're programming, time to stop
1366            log.debug("timeout while programming"); // NOI18N
1367
1368            // perhaps no communications present? Fail back to end of programming
1369            progState = 0;
1370            // and send the notification; error code depends on state
1371            if (progState == 2 && !mServiceMode) { // ops mode command executing,
1372                // so did talk to command station at first
1373                notifyProgListenerEnd(_slots[124].cvval(), jmri.ProgListener.NoAck);
1374            } else {
1375                // all others
1376                notifyProgListenerEnd(_slots[124].cvval(), jmri.ProgListener.FailedTimeout);
1377                // might be leaving power off, but that's currently up to user to fix
1378            }
1379            acceptAnyLACK = false;      // ensure cleared if timed out without getting a LACK
1380        }
1381    }
1382
1383    int progState = 0;
1384    // 1 is commandPending
1385    // 2 is commandExecuting
1386    // 0 is notProgramming
1387    boolean _progRead = false;
1388    boolean _progConfirm = false;
1389    int _confirmVal;
1390    boolean mServiceMode = true;
1391
1392    /**
1393     * Write a CV via Ops Mode programming.
1394     *
1395     * @param CVname CV number
1396     * @param val value to write to the CV
1397     * @param p programmer
1398     * @param addr address of decoder
1399     * @param longAddr true if the address is a long address
1400     * @throws jmri.ProgrammerException if an unsupported programming mode is exercised
1401     */
1402    public void writeCVOpsMode(String CVname, int val, jmri.ProgListener p,
1403            int addr, boolean longAddr) throws jmri.ProgrammerException {
1404        final int CV = Integer.parseInt(CVname);
1405        lopsa = addr & 0x7f;
1406        hopsa = (addr / 128) & 0x7f;
1407        mServiceMode = false;
1408        doWrite(CV, val, p, 0x67);  // ops mode byte write, with feedback
1409    }
1410
1411    /**
1412     * Write a CV via the Service Mode programmer.
1413     *
1414     * @param cvNum CV id as String
1415     * @param val value to write to the CV
1416     * @param p programmer
1417     * @throws jmri.ProgrammerException if an unsupported programming mode is exercised
1418     */
1419    @Override
1420    public void writeCV(String cvNum, int val, jmri.ProgListener p) throws jmri.ProgrammerException {
1421        log.debug("writeCV(string): cvNum={}, value={}", cvNum, val);
1422        if (getMode().equals(csOpSwProgrammingMode)) {
1423            log.debug("cvOpSw mode write!");
1424            // handle Command Station OpSw programming here
1425            String[] parts = cvNum.split("\\.");
1426            if ((parts[0].equals("csOpSw")) && (parts.length==2)) {
1427                if (csOpSwAccessor == null) {
1428                    csOpSwAccessor = new CsOpSwAccess(adaptermemo, p);
1429                } else {
1430                    csOpSwAccessor.setProgrammerListener(p);
1431                }
1432                // perform the CsOpSwMode read access
1433                log.debug("going to try the opsw access");
1434                csOpSwAccessor.writeCsOpSw(cvNum, val, p);
1435                return;
1436
1437            } else {
1438                log.warn("rejecting the cs opsw access account unsupported CV name format");
1439                // unsupported format in "cv" name. Signal an error
1440                notifyProgListenerEnd(p, 1, ProgListener.SequenceError);
1441                return;
1442
1443            }
1444        } else {
1445            // regular CV case
1446            int CV = Integer.parseInt(cvNum);
1447
1448            lopsa = 0;
1449            hopsa = 0;
1450            mServiceMode = true;
1451            // parse the programming command
1452            int pcmd = 0x43;       // LPE implies 0x40, but 0x43 is observed
1453            if (getMode().equals(ProgrammingMode.PAGEMODE)) {
1454                pcmd = pcmd | 0x20;
1455            } else if (getMode().equals(ProgrammingMode.DIRECTBYTEMODE)) {
1456                pcmd = pcmd | 0x28;
1457            } else if (getMode().equals(ProgrammingMode.REGISTERMODE)
1458                    || getMode().equals(ProgrammingMode.ADDRESSMODE)) {
1459                pcmd = pcmd | 0x10;
1460            } else {
1461                throw new jmri.ProgrammerException("mode not supported"); // NOI18N
1462            }
1463
1464            doWrite(CV, val, p, pcmd);
1465        }
1466    }
1467
1468    /**
1469     * Perform a write a CV via the Service Mode programmer.
1470     *
1471     * @param CV CV number
1472     * @param val value to write to the CV
1473     * @param p programmer
1474     * @param pcmd programming command
1475     * @throws jmri.ProgrammerException if an unsupported programming mode is exercised
1476     */
1477    public void doWrite(int CV, int val, jmri.ProgListener p, int pcmd) throws jmri.ProgrammerException {
1478        log.debug("writeCV: {}", CV); // NOI18N
1479
1480        stopEndOfProgrammingTimer();  // still programming, so no longer waiting for power off
1481
1482        useProgrammer(p);
1483        _progRead = false;
1484        _progConfirm = false;
1485        // set commandPending state
1486        progState = 1;
1487
1488        // format and send message
1489        startShortTimer();
1490        tc.sendLocoNetMessage(progTaskStart(pcmd, val, CV, true));
1491    }
1492
1493    /**
1494     * Confirm a CV via the OpsMode programmer.
1495     *
1496     * @param CVname a String containing the CV name
1497     * @param val expected value
1498     * @param p programmer
1499     * @param addr address of loco to write to
1500     * @param longAddr true if addr is a long address
1501     * @throws jmri.ProgrammerException if an unsupported programming mode is exercised
1502     */
1503    public void confirmCVOpsMode(String CVname, int val, jmri.ProgListener p,
1504            int addr, boolean longAddr) throws jmri.ProgrammerException {
1505        int CV = Integer.parseInt(CVname);
1506        lopsa = addr & 0x7f;
1507        hopsa = (addr / 128) & 0x7f;
1508        mServiceMode = false;
1509        doConfirm(CV, val, p, 0x2F);  // although LPE implies 0x2C, 0x2F is observed
1510    }
1511
1512    /**
1513     * Confirm a CV via the Service Mode programmer.
1514     *
1515     * @param CVname a String containing the CV name
1516     * @param val expected value
1517     * @param p programmer
1518     * @throws jmri.ProgrammerException if an unsupported programming mode is exercised
1519     */
1520    @Override
1521    public void confirmCV(String CVname, int val, jmri.ProgListener p) throws jmri.ProgrammerException {
1522        int CV = Integer.parseInt(CVname);
1523        lopsa = 0;
1524        hopsa = 0;
1525        mServiceMode = true;
1526        if (getMode().equals(csOpSwProgrammingMode)) {
1527            log.debug("cvOpSw mode!");
1528            //handle Command Station OpSw programming here
1529            String[] parts = CVname.split("\\.");
1530            if ((parts[0].equals("csOpSw")) && (parts.length==2)) {
1531                if (csOpSwAccessor == null) {
1532                    csOpSwAccessor = new CsOpSwAccess(adaptermemo, p);
1533                } else {
1534                    csOpSwAccessor.setProgrammerListener(p);
1535                }
1536                // perform the CsOpSwMode read access
1537                log.debug("going to try the opsw access");
1538                csOpSwAccessor.readCsOpSw(CVname, p);
1539                return;
1540            } else {
1541                log.warn("rejecting the cs opsw access account unsupported CV name format");
1542                // unsupported format in "cv" name.  Signal an error.
1543                notifyProgListenerEnd(p, 1, ProgListener.SequenceError);
1544                return;
1545            }
1546        }
1547
1548        // parse the programming command
1549        int pcmd = 0x03;       // LPE implies 0x00, but 0x03 is observed
1550        if (getMode().equals(ProgrammingMode.PAGEMODE)) {
1551            pcmd = pcmd | 0x20;
1552        } else if (getMode().equals(ProgrammingMode.DIRECTBYTEMODE)) {
1553            pcmd = pcmd | 0x28;
1554        } else if (getMode().equals(ProgrammingMode.REGISTERMODE)
1555                || getMode().equals(ProgrammingMode.ADDRESSMODE)) {
1556            pcmd = pcmd | 0x10;
1557        } else {
1558            throw new jmri.ProgrammerException("mode not supported"); // NOI18N
1559        }
1560
1561        doConfirm(CV, val, p, pcmd);
1562    }
1563
1564    /**
1565     * Perform a confirm operation of a CV via the Service Mode programmer.
1566     *
1567     * @param CV the CV number
1568     * @param val expected value
1569     * @param p programmer
1570     * @param pcmd programming command
1571     * @throws jmri.ProgrammerException if an unsupported programming mode is exercised
1572     */
1573    public void doConfirm(int CV, int val, ProgListener p,
1574            int pcmd) throws jmri.ProgrammerException {
1575
1576        log.debug("confirmCV: {}, val: {}", CV, val); // NOI18N
1577
1578        stopEndOfProgrammingTimer();  // still programming, so no longer waiting for power off
1579
1580        useProgrammer(p);
1581        _progRead = false;
1582        _progConfirm = true;
1583        _confirmVal = val;
1584
1585        // set commandPending state
1586        progState = 1;
1587
1588        // format and send message
1589        startShortTimer();
1590        tc.sendLocoNetMessage(progTaskStart(pcmd, val, CV, false));
1591    }
1592
1593    int hopsa; // high address for CV read/write
1594    int lopsa; // low address for CV read/write
1595
1596    CsOpSwAccess csOpSwAccessor;
1597
1598    @Override
1599    public void readCV(String cvNum, jmri.ProgListener p) throws jmri.ProgrammerException {
1600        readCV(cvNum, p, 0);
1601    }
1602
1603    /**
1604     * Read a CV via the OpsMode programmer.
1605     *
1606     * @param cvNum a String containing the CV number
1607     * @param p programmer
1608     * @param startVal initial "guess" for value of CV, can improve speed if used
1609     * @throws jmri.ProgrammerException if an unsupported programming mode is exercised
1610     */
1611    @Override
1612    public void readCV(String cvNum, jmri.ProgListener p, int startVal) throws jmri.ProgrammerException {
1613        log.debug("readCV(string): cvNum={}, startVal={}, mode={}", cvNum, startVal, getMode());
1614        if (getMode().equals(csOpSwProgrammingMode)) {
1615            log.debug("cvOpSw mode!");
1616            //handle Command Station OpSw programming here
1617            String[] parts = cvNum.split("\\.");
1618            if ((parts[0].equals("csOpSw")) && (parts.length==2)) {
1619                if (csOpSwAccessor == null) {
1620                    csOpSwAccessor = new CsOpSwAccess(adaptermemo, p);
1621                } else {
1622                    csOpSwAccessor.setProgrammerListener(p);
1623                }
1624                // perform the CsOpSwMode read access
1625                log.debug("going to try the opsw access");
1626                csOpSwAccessor.readCsOpSw(cvNum, p);
1627                return;
1628
1629            } else {
1630                log.warn("rejecting the cs opsw access account unsupported CV name format");
1631                // unsupported format in "cv" name.  Signal an error.
1632                notifyProgListenerEnd(p, 1, ProgListener.SequenceError);
1633                return;
1634
1635            }
1636        } else {
1637            // regular integer address for DCC form
1638            int CV = Integer.parseInt(cvNum);
1639
1640            lopsa = 0;
1641            hopsa = 0;
1642            mServiceMode = true;
1643            // parse the programming command
1644            int pcmd = 0x03;       // LPE implies 0x00, but 0x03 is observed
1645            if (getMode().equals(ProgrammingMode.PAGEMODE)) {
1646                pcmd = pcmd | 0x20;
1647            } else if (getMode().equals(ProgrammingMode.DIRECTBYTEMODE)) {
1648                pcmd = pcmd | 0x28;
1649            } else if (getMode().equals(ProgrammingMode.REGISTERMODE)
1650                    || getMode().equals(ProgrammingMode.ADDRESSMODE)) {
1651                pcmd = pcmd | 0x10;
1652            } else {
1653                throw new jmri.ProgrammerException("mode not supported"); // NOI18N
1654            }
1655
1656            doRead(CV, p, pcmd, startVal);
1657
1658        }
1659    }
1660
1661    /**
1662     * Invoked by LnOpsModeProgrammer to start an ops-mode read operation.
1663     *
1664     * @param CVname       Which CV to read
1665     * @param p        Who to notify on complete
1666     * @param addr     Address of the locomotive
1667     * @param longAddr true if a long address, false if short address
1668     * @throws jmri.ProgrammerException if an unsupported programming mode is exercised
1669     */
1670    public void readCVOpsMode(String CVname, jmri.ProgListener p, int addr, boolean longAddr) throws jmri.ProgrammerException {
1671        final int CV = Integer.parseInt(CVname);
1672        lopsa = addr & 0x7f;
1673        hopsa = (addr / 128) & 0x7f;
1674        mServiceMode = false;
1675        doRead(CV, p, 0x2F, 0);  // although LPE implies 0x2C, 0x2F is observed
1676    }
1677
1678    /**
1679     * Perform a CV Read.
1680     *
1681     * @param CV the CV number
1682     * @param p programmer
1683     * @param progByte programming command
1684     * @param startVal initial "guess" for value of CV, can improve speed if used
1685     * @throws jmri.ProgrammerException if an unsupported programming mode is exercised
1686     */
1687    void doRead(int CV, jmri.ProgListener p, int progByte, int startVal) throws jmri.ProgrammerException {
1688
1689        log.debug("readCV: {} with startVal: {}", CV, startVal); // NOI18N
1690
1691        stopEndOfProgrammingTimer();  // still programming, so no longer waiting for power off
1692
1693        useProgrammer(p);
1694        _progRead = true;
1695        _progConfirm = false;
1696        // set commandPending state
1697        progState = 1;
1698
1699        // format and send message
1700        startShortTimer();
1701//        tc.sendLocoNetMessage(progTaskStart(progByte, 0, CV, false));
1702        tc.sendLocoNetMessage(progTaskStart(progByte, startVal, CV, false));
1703    }
1704
1705    private jmri.ProgListener _usingProgrammer = null;
1706
1707    // internal method to remember who's using the programmer
1708    protected void useProgrammer(jmri.ProgListener p) throws jmri.ProgrammerException {
1709        // test for only one!
1710        if (_usingProgrammer != null && _usingProgrammer != p) {
1711
1712            log.info("programmer already in use by {}", _usingProgrammer); // NOI18N
1713
1714            throw new jmri.ProgrammerException("programmer in use"); // NOI18N
1715        } else {
1716            _usingProgrammer = p;
1717            return;
1718        }
1719    }
1720
1721    /**
1722     * Internal method to create the LocoNetMessage for programmer task start.
1723     *
1724     * @param pcmd programmer command
1725     * @param val value to be used
1726     * @param cvnum CV number
1727     * @param write true if write, else false
1728     * @return a LocoNet message containing a programming task start operation
1729     */
1730    protected LocoNetMessage progTaskStart(int pcmd, int val, int cvnum, boolean write) {
1731
1732        int addr = cvnum - 1;    // cvnum is in human readable form; addr is what's sent over LocoNet
1733
1734        LocoNetMessage m = new LocoNetMessage(14);
1735
1736        m.setOpCode(LnConstants.OPC_WR_SL_DATA);
1737        m.setElement(1, 0x0E);
1738        m.setElement(2, LnConstants.PRG_SLOT);
1739
1740        m.setElement(3, pcmd);
1741
1742        // set zero, then HOPSA, LOPSA, TRK
1743        m.setElement(4, 0);
1744        m.setElement(5, hopsa);
1745        m.setElement(6, lopsa);
1746        m.setElement(7, 0);  // TRK was 0, then 7 for PR2, now back to zero
1747
1748        // store address in CVH, CVL. Note CVH format is truely wierd...
1749        m.setElement(8, ((addr & 0x300)>>4) | ((addr & 0x80) >> 7) | ((val & 0x80) >> 6));
1750        m.setElement(9, addr & 0x7F);
1751
1752        // store low bits of CV value
1753        m.setElement(10, val & 0x7F);
1754
1755        // throttle ID
1756        m.setElement(11, 0x7F);
1757        m.setElement(12, 0x7F);
1758        return m;
1759    }
1760
1761    /**
1762     * Internal method to notify of the final result.
1763     *
1764     * @param value  The cv value to be returned
1765     * @param status The error code, if any
1766     */
1767    protected void notifyProgListenerEnd(int value, int status) {
1768        log.debug("  notifyProgListenerEnd with {}, {} and _usingProgrammer = {}", value, status, _usingProgrammer); // NOI18N
1769        // (re)start power timer
1770        restartEndOfProgrammingTimer();
1771        // and send the reply
1772        ProgListener p = _usingProgrammer;
1773        _usingProgrammer = null;
1774        if (p != null) {
1775            sendProgrammingReply(p, value, status);
1776        }
1777    }
1778
1779    /**
1780     * Internal method to notify of the LACK result. This is a separate routine
1781     * from nPLRead in case we need to handle something later.
1782     *
1783     * @param status The error code, if any
1784     */
1785    protected void notifyProgListenerLack(int status) {
1786        // (re)start power timer
1787        restartEndOfProgrammingTimer();
1788        // and send the reply
1789        sendProgrammingReply(_usingProgrammer, -1, status);
1790        _usingProgrammer = null;
1791    }
1792
1793    /**
1794     * Internal routine to forward a programming reply. This is delayed to
1795     * prevent overruns of the command station.
1796     *
1797     * @param p a ProgListener object
1798     * @param value  the value to return
1799     * @param status The error code, if any
1800     */
1801    protected void sendProgrammingReply(ProgListener p, int value, int status) {
1802        int delay = serviceModeReplyDelay;  // value in service mode
1803        if (!mServiceMode) {
1804            delay = opsModeReplyDelay;  // value in ops mode
1805        }
1806
1807        // delay and run on GUI thread
1808        javax.swing.Timer timer = new javax.swing.Timer(delay, new java.awt.event.ActionListener() {
1809            @Override
1810            public void actionPerformed(java.awt.event.ActionEvent e) {
1811                notifyProgListenerEnd(p, value, status);
1812            }
1813        });
1814        timer.setInitialDelay(delay);
1815        timer.setRepeats(false);
1816        timer.start();
1817    }
1818
1819    /**
1820     * Internal routine to stop end-of-programming timer, as another programming
1821     * operation has happened.
1822     */
1823    protected void stopEndOfProgrammingTimer() {
1824        if (mPowerTimer != null) {
1825            mPowerTimer.stop();
1826        }
1827    }
1828
1829    /**
1830     * Internal routine to handle timer restart if needed to restore power. This
1831     * is only needed in service mode.
1832     */
1833    protected void restartEndOfProgrammingTimer() {
1834        final int delay = 10000;
1835        if (mProgEndSequence) {
1836            if (mPowerTimer == null) {
1837                mPowerTimer = new javax.swing.Timer(delay, new java.awt.event.ActionListener() {
1838                    @Override
1839                    public void actionPerformed(java.awt.event.ActionEvent e) {
1840                        doEndOfProgramming();
1841                    }
1842                });
1843            }
1844            mPowerTimer.stop();
1845            mPowerTimer.setInitialDelay(delay);
1846            mPowerTimer.setRepeats(false);
1847            mPowerTimer.start();
1848        }
1849    }
1850
1851    /**
1852     * Internal routine to handle a programming timeout by turning power off.
1853     */
1854    synchronized protected void doEndOfProgramming() {
1855        if (progState == 0) {
1856             if ( mServiceMode ) {
1857                // finished service-track programming, time to power on
1858                log.debug("end service-mode programming: turn power on"); // NOI18N
1859                try {
1860                    jmri.InstanceManager.getDefault(jmri.PowerManager.class).setPower(jmri.PowerManager.ON);
1861                } catch (jmri.JmriException e) {
1862                    log.error("exception during power on at end of programming", e); // NOI18N
1863                }
1864            } else {
1865                log.debug("end ops-mode programming: no power change"); // NOI18N
1866            }
1867        }
1868    }
1869
1870    javax.swing.Timer mPowerTimer = null;
1871
1872    ReadAllSlots_Helper _rAS = null;
1873
1874    /**
1875     * Start the process of checking each slot for contents.
1876     * <p>
1877     * This is not invoked by this class, but can be invoked from elsewhere to
1878     * start the process of scanning all slots to update their contents.
1879     *
1880     * If an instance is already running then the request is ignored
1881     *
1882     * @param inputSlotMap array of from to pairs
1883     * @param interval ms between slt rds
1884     */
1885    public synchronized void update(List<SlotMapEntry> inputSlotMap, int interval) {
1886        if (_rAS == null) {
1887            _rAS = new ReadAllSlots_Helper(  inputSlotMap, interval);
1888            jmri.util.ThreadingUtil.newThread(_rAS, getUserName() + READ_ALL_SLOTS_THREADNAME).start();
1889        } else {
1890            if (!_rAS.isRunning()) {
1891                jmri.util.ThreadingUtil.newThread(_rAS, getUserName() + READ_ALL_SLOTS_THREADNAME).start();
1892            }
1893        }
1894    }
1895
1896    /**
1897     * String with name for Read all slots thread.
1898     * Requires getUserName prepending.
1899     */
1900    public static final String READ_ALL_SLOTS_THREADNAME = " Read All Slots ";
1901
1902    /**
1903     * Checks slotNum valid for slot map
1904     *
1905     * @param slotNum the slot number
1906     * @return true if it is
1907     */
1908    private boolean validateSlotNumber(int slotNum) {
1909        for (SlotMapEntry item : slotMap) {
1910            if (slotNum >= item.getFrom() && slotNum <= item.getTo()) {
1911                return true;
1912            }
1913        }
1914        return false;
1915    }
1916
1917    public void update() {
1918        update(slotMap, slotScanInterval);
1919    }
1920
1921    /**
1922     * Send a message requesting the data from a particular slot.
1923     *
1924     * @param slot Slot number
1925     */
1926    public void sendReadSlot(int slot) {
1927        LocoNetMessage m = new LocoNetMessage(4);
1928        m.setOpCode(LnConstants.OPC_RQ_SL_DATA);
1929        m.setElement(1, slot & 0x7F);
1930        // one is always short
1931        // THis gets a little akward, slots 121 thru 127 incl. seem to always old slots.
1932        // All slots gt 127 are always expanded format.
1933        if ( slot > 127 || ( ( slot > 0 && slot < 121 ) && loconetProtocol == LnConstants.LOCONETPROTOCOL_TWO ) ) {
1934            m.setElement(2, (slot / 128 ) & 0b00000111 | 0x40 );
1935        }
1936        tc.sendLocoNetMessage(m);
1937    }
1938
1939    protected int nextReadSlot = 0;
1940
1941    /**
1942     * Continue the sequence of reading all slots.
1943     * @param toSlot index of the next slot to read
1944     * @param interval wait time before operation, milliseconds
1945     */
1946    synchronized protected void readNextSlot(int toSlot, int interval) {
1947        // send info request
1948        sendReadSlot(nextReadSlot++);
1949
1950        // schedule next read if needed
1951        if (nextReadSlot < toSlot) {
1952            javax.swing.Timer t = new javax.swing.Timer(interval, new java.awt.event.ActionListener() {
1953                @Override
1954                public void actionPerformed(java.awt.event.ActionEvent e) {
1955                    readNextSlot(toSlot,interval);
1956                }
1957            });
1958            t.setRepeats(false);
1959            t.start();
1960        }
1961    }
1962
1963    /**
1964     * Provide a snapshot of the slots in use.
1965     * <p>
1966     * Note that the count of "in-use" slots may be somewhat misleading,
1967     * as slots in the "common" state can be controlled and are occupying
1968     * a slot in a meaningful way.
1969     *
1970     * @return the count of in-use LocoNet slots
1971     */
1972    public int getInUseCount() {
1973        int result = 0;
1974        for (int i = 0; i <= 120; i++) {
1975            if (slot(i).slotStatus() == LnConstants.LOCO_IN_USE) {
1976                result++;
1977            }
1978        }
1979        return result;
1980    }
1981
1982    /**
1983     * Set the system connection memo.
1984     *
1985     * @param memo a LocoNetSystemConnectionMemo
1986     */
1987    public void setSystemConnectionMemo(LocoNetSystemConnectionMemo memo) {
1988        adaptermemo = memo;
1989    }
1990
1991    LocoNetSystemConnectionMemo adaptermemo;
1992
1993    /**
1994     * Get the "user name" for the slot manager connection, from the memo.
1995     *
1996     * @return the connection's user name or "LocoNet" if the memo
1997     * does not exist
1998     */
1999    @Override
2000    public String getUserName() {
2001        if (adaptermemo == null) {
2002            return "LocoNet"; // NOI18N
2003        }
2004        return adaptermemo.getUserName();
2005    }
2006
2007    /**
2008     * Return the memo "system prefix".
2009     *
2010     * @return the system prefix or "L" if the memo
2011     * does not exist
2012     */
2013    @Override
2014    public String getSystemPrefix() {
2015        if (adaptermemo == null) {
2016            return "L";
2017        }
2018        return adaptermemo.getSystemPrefix();
2019    }
2020
2021    boolean transpondingAvailable = false;
2022    public void setTranspondingAvailable(boolean val) { transpondingAvailable = val; }
2023    public boolean getTranspondingAvailable() { return transpondingAvailable; }
2024
2025    /**
2026     *
2027     * @param val If false then we only use protocol one.
2028     */
2029    public void setLoconetProtocolAutoDetect(boolean val) {
2030        if (!val) {
2031            loconetProtocol = LnConstants.LOCONETPROTOCOL_ONE;
2032            // slots would have been created with unknown for auto detect
2033            for( int ix = 0; ix < 128; ix++ ) {
2034                slot(ix).setProtocol(loconetProtocol);
2035            }
2036        }
2037    }
2038
2039    /**
2040     * Get the memo.
2041     *
2042     * @return the memo
2043     */
2044    public LocoNetSystemConnectionMemo getSystemConnectionMemo() {
2045        return adaptermemo;
2046    }
2047
2048    /**
2049     * Dispose of this by stopped it's ongoing actions
2050     */
2051    @Override
2052    public void dispose() {
2053        if (staleSlotCheckTimer != null) {
2054            staleSlotCheckTimer.stop();
2055        }
2056        if ( _rAS != null ) {
2057            _rAS.setAbort();
2058        }
2059    }
2060
2061    // initialize logging
2062    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SlotManager.class);
2063
2064    // Read all slots
2065    class ReadAllSlots_Helper implements Runnable {
2066
2067        ReadAllSlots_Helper(List<SlotMapEntry> inputSlotMap, int interval) {
2068            this.interval = interval;
2069        }
2070
2071        private int interval;
2072        private boolean abort = false;
2073        private boolean isRunning = false;
2074
2075        /**
2076         * Aborts current run
2077         */
2078        public void setAbort() {
2079            abort = true;
2080        }
2081
2082        /**
2083         * Gets the current stae of the run.
2084         * @return true if running
2085         */
2086        public boolean isRunning() {
2087            return isRunning;
2088        }
2089
2090        @Override
2091        public void run() {
2092            abort = false;
2093            isRunning = true;
2094            // read all slots that are not of unknown type
2095            for (int slot = 0; slot < getNumSlots() && !abort; slot++) {
2096                if (_slots[slot].getSlotType() != SlotType.UNKNOWN) {
2097                    sendReadSlot(slot);
2098                    try {
2099                        Thread.sleep(this.interval);
2100                    } catch (Exception ex) {
2101                        // just abort
2102                        abort = true;
2103                        break;
2104                    }
2105                }
2106            }
2107            isRunning = false;
2108        }
2109    }
2110
2111}