001package jmri.jmrix.sprog;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004
005import java.util.LinkedList;
006import java.util.Queue;
007import java.util.Vector;
008
009import jmri.CommandStation;
010import jmri.DccLocoAddress;
011import jmri.InstanceManager;
012import jmri.JmriException;
013import jmri.PowerManager;
014import jmri.util.swing.JmriJOptionPane;
015
016/**
017 * Control a collection of slots, acting as a soft command station for SPROG
018 * <p>
019 * A SlotListener can register to hear changes. By registering here, the
020 * SlotListener is saying that it wants to be notified of a change in any slot.
021 * Alternately, the SlotListener can register with some specific slot, done via
022 * the SprogSlot object itself.
023 * <p>
024 * This Programmer implementation is single-user only. It's not clear whether
025 * the command stations can have multiple programming requests outstanding (e.g.
026 * service mode and ops mode, or two ops mode) at the same time, but this code
027 * definitely can't.
028 * <p>
029 * Updated by Andrew Berridge, January 2010 - state management code now safer,
030 * uses enum, etc. Amalgamated with Sprog Slot Manager into a single class -
031 * reduces code duplication.
032 * <p>
033 * Updated by Andrew Crosland February 2012 to allow slots to hold 28 step speed
034 * packets
035 * <p>
036 * Re-written by Andrew Crosland to send the next packet as soon as a reply is
037 * notified. This removes a race between the old state machine running before
038 * the traffic controller despatches a reply, missing the opportunity to send a
039 * new packet to the layout until the next JVM time slot, which can be 15ms on
040 * Windows platforms.
041 * <p>
042 * May-17 Moved status reply handling to the slot monitor. Monitor messages from
043 * other sources and suppress messages from here to prevent queueing messages in
044 * the traffic controller.
045 * <p>
046 * Jan-18 Re-written again due to threading issues. Previous changes removed
047 * activity from the slot thread, which could result in loading the swing thread
048 * to the extent that the gui becomes very slow to respond.
049 * Moved status message generation to the slot monitor.
050 * Interact with power control as a way to allow the user to recover after a
051 * timeout error due to loss of communication with the hardware.
052 *
053 * @author Bob Jacobsen Copyright (C) 2001, 2003
054 * @author Andrew Crosland (C) 2006 ported to SPROG, 2012, 2016, 2018
055 */
056public class SprogCommandStation implements CommandStation, SprogListener, Runnable,
057        java.beans.PropertyChangeListener {
058
059    protected int currentSlot = 0;
060    protected int currentSprogAddress = -1;
061
062    protected LinkedList<SprogSlot> slots;
063    protected int numSlots = SprogConstants.MIN_SLOTS;
064    protected Queue<SprogSlot> sendNow;
065
066    private SprogTrafficController tc = null;
067
068    final Object lock = new Object();
069
070    private boolean waitingForReply = false;
071    private boolean replyAvailable = false;
072    private boolean sendSprogAddress = false;
073    private long time, timeNow, packetDelay;
074    private int lastId;
075
076    PowerManager powerMgr = null;
077    int powerState = PowerManager.OFF;
078    boolean powerChanged = false;
079
080    public SprogCommandStation(SprogTrafficController controller) {
081        sendNow = new LinkedList<>();
082        /**
083         * Create a default length queue
084         */
085        slots = new LinkedList<>();
086        numSlots = controller.getAdapterMemo().getNumSlots();
087        for (int i = 0; i < numSlots; i++) {
088            slots.add(new SprogSlot(i));
089        }
090        tc = controller;
091        tc.addSprogListener(this);
092    }
093
094    /**
095     * Send a specific packet as a SprogMessage.
096     *
097     * @param packet  Byte array representing the packet, including the
098     *                error-correction byte. Must not be null.
099     * @param repeats number of times to repeat the packet
100     */
101    @Override
102    public boolean sendPacket(byte[] packet, int repeats) {
103        if (packet.length <= 1) {
104            log.error("Invalid DCC packet length: {}", packet.length);
105        }
106        if (packet.length >= 7) {
107            log.error("Maximum 6-byte packets accepted: {}", packet.length);
108        }
109        final SprogMessage m = new SprogMessage(packet);
110        sendMessage(m);
111        return true;
112    }
113
114    /**
115     * Send the SprogMessage to the hardware.
116     * <p>
117     * sendSprogMessage will block until the message can be sent. When it returns
118     * we set the reply status for the message just sent.
119     *
120     * @param m       The message to be sent
121     */
122    protected void sendMessage(SprogMessage m) {
123        log.debug("Sending message [{}] id {}", m.toString(tc.isSIIBootMode()), m.getId());
124        lastId = m.getId();
125        tc.sendSprogMessage(m, this);
126    }
127
128    /**
129     * Return contents of Queue slot i.
130     *
131     * @param i int of slot requested
132     * @return SprogSlot slot i
133     */
134    public SprogSlot slot(int i) {
135        return slots.get(i);
136    }
137
138    /**
139     * Clear all slots.
140     */
141    @SuppressFBWarnings(value = "UPM_UNCALLED_PRIVATE_METHOD", justification="was previously marked with @SuppressWarnings, reason unknown")
142    private void clearAllSlots() {
143        slots.stream().forEach((s) -> {
144            s.clear();
145        });
146    }
147
148    /**
149     * Find a free slot entry.
150     *
151     * @return SprogSlot the next free Slot or null if all slots are full
152     */
153    protected SprogSlot findFree() {
154        for (SprogSlot s : slots) {
155            if (s.isFree()) {
156                if (log.isDebugEnabled()) {
157                    log.debug("Found free slot {}", s.getSlotNumber());
158                }
159                return s;
160            }
161        }
162        return (null);
163    }
164
165    /**
166     * Find a queue entry matching the address.
167     *
168     * @param address The address to locate
169     * @return The slot or null if the address is not in the queue
170     */
171    private SprogSlot findAddress(DccLocoAddress address) {
172        for (SprogSlot s : slots) {
173            if ( s.isActiveAddressMatch(address) ) {
174                return s;
175            }
176        }
177        return (null);
178    }
179
180    private SprogSlot findAddressSpeedPacket(DccLocoAddress address) {
181        // SPROG doesn't use IDLE packets but sends speed commands to last address selected by "A" command.
182        // We may need to move these pseudo-idle packets to an unused long address so locos will not receive conflicting speed commands.
183        // Some short-address-only decoders may also respond to same-numbered long address so we avoid any number match irrespective of type
184        // We need to find a suitable free long address, save (currentSprogAddress) and use it for pseudo-idle packets
185        int lastSprogAddress = currentSprogAddress;
186        while ( (currentSprogAddress <= 0) || // initialisation || avoid address 0 for reason above
187                    ( (address.getNumber() == currentSprogAddress ) ) || // avoid this address (slot may not exist but we will be creating one)
188                    ( findAddress(new DccLocoAddress(currentSprogAddress,true)) != null) || ( findAddress(new DccLocoAddress(currentSprogAddress,false)) != null) // avoid in-use (both long or short versions of) address
189                    ) {
190                    currentSprogAddress++;
191                    currentSprogAddress = currentSprogAddress % 10240;
192            }
193        if (currentSprogAddress != lastSprogAddress) {
194            log.info("Changing currentSprogAddress (for pseudo-idle packets) to {}(L)", currentSprogAddress);
195            // We want to ignore the reply to this message so it does not trigger an extra packet
196            // Set a flag to send this from the slot thread and avoid swing thread waiting
197            //sendMessage(new SprogMessage("A " + currentSprogAddress + " 0"));
198            sendSprogAddress = true;
199        }
200        for (SprogSlot s : slots) {
201            if (s.isActiveAddressMatch(address) && s.isSpeedPacket()) {
202                return s;
203            }
204        }
205        if (getInUseCount() < numSlots) {
206            return findFree();
207        }
208        return (null);
209    }
210
211    private SprogSlot findF0to4Packet(DccLocoAddress address) {
212        for (SprogSlot s : slots) {
213            if (s.isActiveAddressMatch(address) && s.isF0to4Packet()) {
214                return s;
215            }
216        }
217        if (getInUseCount() < numSlots) {
218            return findFree();
219        }
220        return (null);
221    }
222
223    private SprogSlot findF5to8Packet(DccLocoAddress address) {
224        for (SprogSlot s : slots) {
225            if (s.isActiveAddressMatch(address) && s.isF5to8Packet()) {
226                return s;
227            }
228        }
229        if (getInUseCount() < numSlots) {
230            return findFree();
231        }
232        return (null);
233    }
234
235    private SprogSlot findF9to12Packet(DccLocoAddress address) {
236        for (SprogSlot s : slots) {
237            if (s.isActiveAddressMatch(address) && s.isF9to12Packet()) {
238                return s;
239            }
240        }
241        if (getInUseCount() < numSlots) {
242            return findFree();
243        }
244        return (null);
245    }
246
247    private SprogSlot findF13to20Packet(DccLocoAddress address) {
248        for (SprogSlot s : slots) {
249            if (s.isActiveAddressMatch(address) && s.isF13to20Packet()) {
250                return s;
251            }
252        }
253        if (getInUseCount() < numSlots) {
254            return findFree();
255        }
256        return (null);
257    }
258
259    private SprogSlot findF21to28Packet(DccLocoAddress address) {
260        for (SprogSlot s : slots) {
261            if (s.isActiveAddressMatch(address) && s.isF21to28Packet()) {
262                return s;
263            }
264        }
265        if (getInUseCount() < numSlots) {
266            return findFree();
267        }
268        return (null);
269    }
270
271    private SprogSlot findF29to36Packet(DccLocoAddress address) {
272        for (SprogSlot s : slots) {
273            if (s.isActiveAddressMatch(address) && s.isF29to36Packet()) {
274                return s;
275            }
276        }
277        if (getInUseCount() < numSlots) {
278            return findFree();
279        }
280        return (null);
281    }
282
283    private SprogSlot findF37to44Packet(DccLocoAddress address) {
284        for (SprogSlot s : slots) {
285            if (s.isActiveAddressMatch(address) && s.isF37to44Packet()) {
286                return s;
287            }
288        }
289        if (getInUseCount() < numSlots) {
290            return findFree();
291        }
292        return (null);
293    }
294
295    private SprogSlot findF45to52Packet(DccLocoAddress address) {
296        for (SprogSlot s : slots) {
297            if (s.isActiveAddressMatch(address) && s.isF45to52Packet()) {
298                return s;
299            }
300        }
301        if (getInUseCount() < numSlots) {
302            return findFree();
303        }
304        return (null);
305    }
306
307    private SprogSlot findF53to60Packet(DccLocoAddress address) {
308        for (SprogSlot s : slots) {
309            if (s.isActiveAddressMatch(address) && s.isF53to60Packet()) {
310                return s;
311            }
312        }
313        if (getInUseCount() < numSlots) {
314            return findFree();
315        }
316        return (null);
317    }
318
319    private SprogSlot findF61to68Packet(DccLocoAddress address) {
320        for (SprogSlot s : slots) {
321            if (s.isActiveAddressMatch(address) && s.isF61to68Packet()) {
322                return s;
323            }
324        }
325        if (getInUseCount() < numSlots) {
326            return findFree();
327        }
328        return (null);
329    }
330
331    public void forwardCommandChangeToLayout(int address, boolean closed) {
332
333        SprogSlot s = this.findFree();
334        if (s != null) {
335            s.setAccessoryPacket(address, closed, SprogConstants.S_REPEATS);
336            notifySlotListeners(s);
337        }
338    }
339
340    public void function0Through4Packet(DccLocoAddress address,
341            boolean f0, boolean f0Momentary,
342            boolean f1, boolean f1Momentary,
343            boolean f2, boolean f2Momentary,
344            boolean f3, boolean f3Momentary,
345            boolean f4, boolean f4Momentary) {
346        SprogSlot s = this.findF0to4Packet(address);
347        if(s==null){
348            log.warn("Insufficient Sprogslots available, command not sent - increase number of Sprogslots in preferences");
349        }else{
350            s.f0to4packet(address.getNumber(), address.isLongAddress(), f0, f0Momentary,
351                f1, f1Momentary,
352                f2, f2Momentary,
353                f3, f3Momentary,
354                f4, f4Momentary);
355            notifySlotListeners(s);
356        }
357    }
358
359    public void function5Through8Packet(DccLocoAddress address,
360            boolean f5, boolean f5Momentary,
361            boolean f6, boolean f6Momentary,
362            boolean f7, boolean f7Momentary,
363            boolean f8, boolean f8Momentary) {
364        SprogSlot s = this.findF5to8Packet(address);
365        if(s==null){
366            log.warn("Insufficient Sprogslots available, command not sent - increase number of Sprogslots in preferences");
367        }else{
368            s.f5to8packet(address.getNumber(), address.isLongAddress(), f5, f5Momentary, f6, f6Momentary, f7, f7Momentary, f8, f8Momentary);
369            notifySlotListeners(s);
370        }
371    }
372
373    public void function9Through12Packet(DccLocoAddress address,
374            boolean f9, boolean f9Momentary,
375            boolean f10, boolean f10Momentary,
376            boolean f11, boolean f11Momentary,
377            boolean f12, boolean f12Momentary) {
378        SprogSlot s = this.findF9to12Packet(address);
379        if(s==null){
380            log.warn("Insufficient Sprogslots available, command not sent - increase number of Sprogslots in preferences");
381        }else{
382            s.f9to12packet(address.getNumber(), address.isLongAddress(), f9, f9Momentary, f10, f10Momentary, f11, f11Momentary, f12, f12Momentary);
383            notifySlotListeners(s);
384        }
385    }
386
387    public void function13Through20Packet(DccLocoAddress address,
388            boolean f13, boolean f13Momentary,
389            boolean f14, boolean f14Momentary,
390            boolean f15, boolean f15Momentary,
391            boolean f16, boolean f16Momentary,
392            boolean f17, boolean f17Momentary,
393            boolean f18, boolean f18Momentary,
394            boolean f19, boolean f19Momentary,
395            boolean f20, boolean f20Momentary) {
396        SprogSlot s = this.findF13to20Packet(address);
397        if(s==null){
398            log.warn("Insufficient Sprogslots available, command not sent - increase number of Sprogslots in preferences");
399        }else{
400            s.f13to20packet(address.getNumber(), address.isLongAddress(),
401                f13, f13Momentary, f14, f14Momentary, f15, f15Momentary, f16, f16Momentary,
402                f17, f17Momentary, f18, f18Momentary, f19, f19Momentary, f20, f20Momentary);
403            notifySlotListeners(s);
404        }
405    }
406
407    public void function21Through28Packet(DccLocoAddress address,
408            boolean f21, boolean f21Momentary,
409            boolean f22, boolean f22Momentary,
410            boolean f23, boolean f23Momentary,
411            boolean f24, boolean f24Momentary,
412            boolean f25, boolean f25Momentary,
413            boolean f26, boolean f26Momentary,
414            boolean f27, boolean f27Momentary,
415            boolean f28, boolean f28Momentary) {
416        SprogSlot s = this.findF21to28Packet(address);
417        if(s==null){
418            log.warn("Insufficient Sprogslots available, command not sent - increase number of Sprogslots in preferences");
419        }else{
420            s.f21to28packet(address.getNumber(), address.isLongAddress(),
421                f21, f21Momentary, f22, f22Momentary, f23, f23Momentary, f24, f24Momentary,
422                f25, f25Momentary, f26, f26Momentary, f27, f27Momentary, f28, f28Momentary);
423            notifySlotListeners(s);
424        }
425    }
426
427    public void function29Through36Packet(DccLocoAddress address,
428            boolean a, boolean am,
429            boolean b, boolean bm,
430            boolean c, boolean cm,
431            boolean d, boolean dm,
432            boolean e, boolean em,
433            boolean f, boolean fm,
434            boolean g, boolean gm,
435            boolean h, boolean hm) {
436        SprogSlot s = this.findF29to36Packet(address);
437        if(s==null){
438            log.warn("Insufficient Sprogslots available, command not sent - increase number of Sprogslots in preferences");
439        }else{
440            s.f29to36packet(address.getNumber(), address.isLongAddress(),
441                a, am, b, bm, c, cm, d, dm,
442                e, em, f, fm, g, gm, h, hm);
443            notifySlotListeners(s);
444        }
445    }
446
447    public void function37Through44Packet(DccLocoAddress address,
448            boolean a, boolean am,
449            boolean b, boolean bm,
450            boolean c, boolean cm,
451            boolean d, boolean dm,
452            boolean e, boolean em,
453            boolean f, boolean fm,
454            boolean g, boolean gm,
455            boolean h, boolean hm) {
456        SprogSlot s = this.findF37to44Packet(address);
457        if(s==null){
458            log.warn("Insufficient Sprogslots available, command not sent - increase number of Sprogslots in preferences");
459        }else{
460            s.f37to44packet(address.getNumber(), address.isLongAddress(),
461                a, am, b, bm, c, cm, d, dm,
462                e, em, f, fm, g, gm, h, hm);
463            notifySlotListeners(s);
464        }
465    }
466
467    public void function45Through52Packet(DccLocoAddress address,
468            boolean a, boolean am,
469            boolean b, boolean bm,
470            boolean c, boolean cm,
471            boolean d, boolean dm,
472            boolean e, boolean em,
473            boolean f, boolean fm,
474            boolean g, boolean gm,
475            boolean h, boolean hm) {
476        SprogSlot s = this.findF45to52Packet(address);
477        if(s==null){
478            log.warn("Insufficient Sprogslots available, command not sent - increase number of Sprogslots in preferences");
479        }else{
480            s.f45to52packet(address.getNumber(), address.isLongAddress(),
481                a, am, b, bm, c, cm, d, dm,
482                e, em, f, fm, g, gm, h, hm);
483            notifySlotListeners(s);
484        }
485    }
486
487    public void function53Through60Packet(DccLocoAddress address,
488            boolean a, boolean am,
489            boolean b, boolean bm,
490            boolean c, boolean cm,
491            boolean d, boolean dm,
492            boolean e, boolean em,
493            boolean f, boolean fm,
494            boolean g, boolean gm,
495            boolean h, boolean hm) {
496        SprogSlot s = this.findF53to60Packet(address);
497        if(s==null){
498            log.warn("Insufficient Sprogslots available, command not sent - increase number of Sprogslots in preferences");
499        }else{
500            s.f53to60packet(address.getNumber(), address.isLongAddress(),
501                a, am, b, bm, c, cm, d, dm,
502                e, em, f, fm, g, gm, h, hm);
503            notifySlotListeners(s);
504        }
505    }
506
507    public void function61Through68Packet(DccLocoAddress address,
508            boolean a, boolean am,
509            boolean b, boolean bm,
510            boolean c, boolean cm,
511            boolean d, boolean dm,
512            boolean e, boolean em,
513            boolean f, boolean fm,
514            boolean g, boolean gm,
515            boolean h, boolean hm) {
516        SprogSlot s = this.findF61to68Packet(address);
517        if(s==null){
518            log.warn("Insufficient Sprogslots available, command not sent - increase number of Sprogslots in preferences");
519        }else{
520            s.f61to68packet(address.getNumber(), address.isLongAddress(),
521                a, am, b, bm, c, cm, d, dm,
522                e, em, f, fm, g, gm, h, hm);
523            notifySlotListeners(s);
524        }
525    }
526
527    /**
528     * Handle speed changes from throttle.
529     * <p>
530     * As well as updating an existing slot,
531     * or creating a new on where necessary, the speed command is added to the
532     * queue of packets to be sent immediately.This ensures minimum latency
533     * between the user adjusting the throttle and a loco responding, rather
534     * than possibly waiting for a complete traversal of all slots before the
535     * new speed is actually sent to the hardware.
536     *
537     * @param mode speed step mode.
538     * @param address loco address.
539     * @param spd speed to send.
540     * @param isForward true if forward, else false.
541     */
542    public void setSpeed(jmri.SpeedStepMode mode, DccLocoAddress address, int spd, boolean isForward) {
543        SprogSlot s = this.findAddressSpeedPacket(address);
544        if (s != null) { // May need an error here - if all slots are full!
545            s.setSpeed(mode, address.getNumber(), address.isLongAddress(), spd, isForward);
546            notifySlotListeners(s);
547            log.debug("Registering new speed");
548            sendNow.add(s);
549        } else {
550            log.warn("Insufficient Sprogslots available, command not sent - increase number of Sprogslots in preferences");
551        }
552    }
553
554    public SprogSlot opsModepacket(int address, boolean longAddr, int cv, int val) {
555        SprogSlot s = findFree();
556        if (s != null) {
557            s.setOps(address, longAddr, cv, val);
558            if (log.isDebugEnabled()) {
559                log.debug("opsModePacket() Notify ops mode packet for address {}", address);
560            }
561            notifySlotListeners(s);
562            return (s);
563        } else {
564             return (null);
565        }
566    }
567
568    public void release(DccLocoAddress address) {
569        SprogSlot s;
570        while ((s = findAddress(address)) != null) {
571            s.clear();
572            notifySlotListeners(s);
573        }
574    }
575
576    /**
577     * Send emergency stop to all slots.
578     */
579    public void estopAll() {
580        slots.stream().filter((s) -> ((s.getRepeat() == -1)
581                && s.slotStatus() != SprogConstants.SLOT_FREE
582                && s.speed() != 1)).forEach((s) -> {
583                    eStopSlot(s);
584                });
585    }
586
587    /**
588     * Send emergency stop to a slot.
589     *
590     * @param s SprogSlot to eStop
591     */
592    protected void eStopSlot(SprogSlot s) {
593        log.debug("Estop slot: {} for address: {}", s.getSlotNumber(), s.getAddr());
594        s.eStop();
595        notifySlotListeners(s);
596    }
597
598    // data members to hold contact with the slot listeners
599    final private Vector<SprogSlotListener> slotListeners = new Vector<>();
600
601    public synchronized void addSlotListener(SprogSlotListener l) {
602        // add only if not already registered
603        slotListeners.addElement(l);
604    }
605
606    public synchronized void removeSlotListener(SprogSlotListener l) {
607        slotListeners.removeElement(l);
608    }
609
610    /**
611     * Trigger the notification of all SlotListeners.
612     *
613     * @param s The changed slot to notify.
614     */
615    private synchronized void notifySlotListeners(SprogSlot s) {
616        log.debug("notifySlotListeners() notify {} SlotListeners about slot for address {}",
617                    slotListeners.size(), s.getAddr());
618
619        // forward to all listeners
620        slotListeners.stream().forEach((client) -> {
621            client.notifyChangedSlot(s);
622        });
623    }
624
625    /**
626     * Set initial power state
627     * 
628     * If connection option is set for track power on the property change is sent
629     * before we are registered with the power manager so force a change in the
630     * slot thread
631     * 
632     * @param powerOption true if power on at startup
633     */
634    public void setPowerState(boolean powerOption) {
635        if (powerOption == true) {
636            powerChanged = true;
637            powerState = PowerManager.ON;
638        }
639    }
640    
641    @Override
642    /**
643     * The run() method will only be called (from SprogSystemConnectionMemo
644     * ConfigureCommandStation()) if the connected SPROG is in Command Station mode.
645     *
646     */
647    public void run() {
648        log.debug("Command station slot thread starts");
649        while(true) {
650            try {
651                synchronized(lock) {
652                    lock.wait(SprogConstants.CS_REPLY_TIMEOUT);
653                }
654            } catch (InterruptedException e) {
655               log.debug("Slot thread interrupted");
656               // We'll loop around if there's no reply available yet
657               // Save the interrupted status for anyone who may be interested
658               Thread.currentThread().interrupt();
659               // and exit
660               return;
661            }
662            log.debug("Slot thread wakes");
663
664            if (powerMgr == null) {
665                // Wait until power manager is available
666                powerMgr = InstanceManager.getNullableDefault(jmri.PowerManager.class);
667                if (powerMgr == null) {
668                    log.info("No power manager instance found");
669                } else {
670                    log.info("Registering with power manager");
671                    powerMgr.addPropertyChangeListener(this);
672                }
673            } else {
674                if (sendSprogAddress) {
675                    // If we need to change the SPROGs default address, do that immediately,
676                    // regardless of the power state.
677                    log.debug("Set new address");
678                    sendMessage(new SprogMessage("A " + currentSprogAddress + " 0"));
679                    replyAvailable = false;
680                    sendSprogAddress = false;
681                } else if (powerChanged && (powerState == PowerManager.ON) && !waitingForReply) {
682                    // Power has been turned on so send an idle packet to start the
683                    // message/reply handshake
684                    log.debug("Send idle to start message/reply handshake");
685                    sendPacket(jmri.NmraPacket.idlePacket(), SprogConstants.S_REPEATS);
686                    powerChanged = false;
687                    time = System.currentTimeMillis();
688                } else if (replyAvailable && (powerState == PowerManager.ON)) {
689                    log.debug("Reply available");
690                    // Received a reply whilst power is on, so send another packet
691                    // Get next packet to send if track power is on
692                    byte[] p;
693                    SprogSlot s = sendNow.poll();
694                    if (s != null) {
695                        // New throttle action to be sent immediately
696                        p = s.getPayload();
697                        log.debug("Packet from immediate send queue");
698                    } else {
699                        // Or take the next one from the stack
700                        p = getNextPacket();
701                        if (p != null) {
702                            log.debug("Packet from stack");
703                        }
704                    }
705                    replyAvailable = false;
706                    if (p != null) {
707                        // Send the packet
708                        sendPacket(p, SprogConstants.S_REPEATS);
709                        log.debug("Packet sent");
710                    } else {
711                        // Send a decoder idle packet to prompt a reply from hardware and keep things running
712                        log.debug("Idle sent");
713                        sendPacket(jmri.NmraPacket.idlePacket(), SprogConstants.S_REPEATS);
714                    }
715                    timeNow = System.currentTimeMillis();
716                    packetDelay = timeNow - time;
717                    time = timeNow;
718                    // Useful for debug if packets are being delayed
719                    if (packetDelay > SprogConstants.PACKET_DELAY_WARN_THRESHOLD) {
720                        log.warn("Packet delay was {} ms", packetDelay);
721                    }
722                } else {
723                    if (powerState == PowerManager.ON) {
724
725                        // Should never get here. Something is wrong so turn power off
726                        // Kill reply wait so send doesn't block
727                        log.warn("Slot thread timeout - removing power");
728                        waitingForReply = false;
729                        try {
730                            powerMgr.setPower(PowerManager.OFF);
731                        } catch (JmriException ex) {
732                            log.error("Exception turning power off", ex);
733                        }
734                        JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("CSErrorFrameDialogString"),
735                            Bundle.getMessage("SprogCSTitle"), JmriJOptionPane.ERROR_MESSAGE);
736                    }
737                }
738            }
739        }
740    }
741
742    /**
743     * Get the next packet to be transmitted.
744     *
745     * @return byte[] null if no packet
746     */
747    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "PZLA_PREFER_ZERO_LENGTH_ARRAYS",
748        justification = "API defined by Sprog docs")
749    private byte[] getNextPacket() {
750        SprogSlot s;
751
752        if (!isBusy()) {
753            return null;
754        }
755        while (slots.get(currentSlot).isFree()) {
756            currentSlot++;
757            currentSlot = currentSlot % numSlots;
758        }
759        s = slots.get(currentSlot);
760        byte[] ret = s.getPayload();
761        // Resend ops packets until repeat count is exhausted so that
762        // decoder receives contiguous identical packets, otherwsie find
763        // next packet to send
764        if (!s.isOpsPkt() || (s.getRepeat() == 0)) {
765            currentSlot++;
766            currentSlot = currentSlot % numSlots;
767        }
768
769        if (s.isFinished()) {
770            notifySlotListeners(s);
771            //return null;
772        }
773
774        return ret;
775    }
776
777    /*
778     *
779     * @param m the sprog message received
780     */
781    @Override
782    public void notifyMessage(SprogMessage m) {
783    }
784
785    /**
786     * Handle replies.
787     * <p>
788     * Handle replies from the hardware, ignoring those that were not sent from
789     * the command station.
790     *
791     * @param m The SprogReply to be handled
792     */
793    @Override
794    public void notifyReply(SprogReply m) {
795        if (m.getId() != lastId) {
796            // Not my id, so not interested, message send still blocked
797            log.debug("Ignore reply with mismatched id {} looking for {}", m.getId(), lastId);
798            return;
799        } else {
800            log.debug("Reply received [{}]", m.toString());
801            // Log the reply and wake the slot thread
802            synchronized (lock) {
803                replyAvailable = true;
804                lock.notifyAll();
805            }
806        }
807    }
808
809    /**
810     * implement a property change listener for power
811     */
812    @Override
813    public void propertyChange(java.beans.PropertyChangeEvent evt) {
814        log.debug("propertyChange {} = {}", evt.getPropertyName(), evt.getNewValue());
815        if (evt.getPropertyName().equals(PowerManager.POWER)) {
816            powerState = powerMgr.getPower();
817            powerChanged = true;
818        }
819    }
820
821    /**
822     * Provide a count of the slots in use.
823     *
824     * @return the number of slots in use
825     */
826    public int getInUseCount() {
827        int result = 0;
828        for (SprogSlot s : slots) {
829            if (!s.isFree()) {
830                result++;
831            }
832        }
833        return result;
834    }
835
836    /**
837     *
838     * @return a boolean if the command station is busy - i.e. it has at least
839     *         one occupied slot
840     */
841    public boolean isBusy() {
842        return slots.stream().anyMatch((s) -> (!s.isFree()));
843    }
844
845    public void setSystemConnectionMemo(SprogSystemConnectionMemo memo) {
846        adaptermemo = memo;
847    }
848
849    SprogSystemConnectionMemo adaptermemo;
850
851    /**
852     * Get user name.
853     *
854     * @return the user name
855     */
856    @Override
857    public String getUserName() {
858        if (adaptermemo == null) {
859            return "Sprog";
860        }
861        return adaptermemo.getUserName();
862    }
863
864    /**
865     * Get system prefix.
866     *
867     * @return the system prefix
868     */
869    @Override
870    public String getSystemPrefix() {
871        if (adaptermemo == null) {
872            return "S";
873        }
874        return adaptermemo.getSystemPrefix();
875    }
876
877    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SprogCommandStation.class);
878
879}