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