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        s.f0to4packet(address.getNumber(), address.isLongAddress(), f0, f0Momentary,
348                f1, f1Momentary,
349                f2, f2Momentary,
350                f3, f3Momentary,
351                f4, f4Momentary);
352        notifySlotListeners(s);
353    }
354
355    public void function5Through8Packet(DccLocoAddress address,
356            boolean f5, boolean f5Momentary,
357            boolean f6, boolean f6Momentary,
358            boolean f7, boolean f7Momentary,
359            boolean f8, boolean f8Momentary) {
360        SprogSlot s = this.findF5to8Packet(address);
361        s.f5to8packet(address.getNumber(), address.isLongAddress(), f5, f5Momentary, f6, f6Momentary, f7, f7Momentary, f8, f8Momentary);
362        notifySlotListeners(s);
363    }
364
365    public void function9Through12Packet(DccLocoAddress address,
366            boolean f9, boolean f9Momentary,
367            boolean f10, boolean f10Momentary,
368            boolean f11, boolean f11Momentary,
369            boolean f12, boolean f12Momentary) {
370        SprogSlot s = this.findF9to12Packet(address);
371        s.f9to12packet(address.getNumber(), address.isLongAddress(), f9, f9Momentary, f10, f10Momentary, f11, f11Momentary, f12, f12Momentary);
372        notifySlotListeners(s);
373    }
374
375    public void function13Through20Packet(DccLocoAddress address,
376            boolean f13, boolean f13Momentary,
377            boolean f14, boolean f14Momentary,
378            boolean f15, boolean f15Momentary,
379            boolean f16, boolean f16Momentary,
380            boolean f17, boolean f17Momentary,
381            boolean f18, boolean f18Momentary,
382            boolean f19, boolean f19Momentary,
383            boolean f20, boolean f20Momentary) {
384        SprogSlot s = this.findF13to20Packet(address);
385        s.f13to20packet(address.getNumber(), address.isLongAddress(),
386                f13, f13Momentary, f14, f14Momentary, f15, f15Momentary, f16, f16Momentary,
387                f17, f17Momentary, f18, f18Momentary, f19, f19Momentary, f20, f20Momentary);
388        notifySlotListeners(s);
389    }
390
391    public void function21Through28Packet(DccLocoAddress address,
392            boolean f21, boolean f21Momentary,
393            boolean f22, boolean f22Momentary,
394            boolean f23, boolean f23Momentary,
395            boolean f24, boolean f24Momentary,
396            boolean f25, boolean f25Momentary,
397            boolean f26, boolean f26Momentary,
398            boolean f27, boolean f27Momentary,
399            boolean f28, boolean f28Momentary) {
400        SprogSlot s = this.findF21to28Packet(address);
401        s.f21to28packet(address.getNumber(), address.isLongAddress(),
402                f21, f21Momentary, f22, f22Momentary, f23, f23Momentary, f24, f24Momentary,
403                f25, f25Momentary, f26, f26Momentary, f27, f27Momentary, f28, f28Momentary);
404        notifySlotListeners(s);
405    }
406
407    public void function29Through36Packet(DccLocoAddress address,
408            boolean a, boolean am,
409            boolean b, boolean bm,
410            boolean c, boolean cm,
411            boolean d, boolean dm,
412            boolean e, boolean em,
413            boolean f, boolean fm,
414            boolean g, boolean gm,
415            boolean h, boolean hm) {
416        SprogSlot s = this.findF29to36Packet(address);
417        s.f29to36packet(address.getNumber(), address.isLongAddress(),
418                a, am, b, bm, c, cm, d, dm,
419                e, em, f, fm, g, gm, h, hm);
420        notifySlotListeners(s);
421    }
422
423    public void function37Through44Packet(DccLocoAddress address,
424            boolean a, boolean am,
425            boolean b, boolean bm,
426            boolean c, boolean cm,
427            boolean d, boolean dm,
428            boolean e, boolean em,
429            boolean f, boolean fm,
430            boolean g, boolean gm,
431            boolean h, boolean hm) {
432        SprogSlot s = this.findF37to44Packet(address);
433        s.f37to44packet(address.getNumber(), address.isLongAddress(),
434                a, am, b, bm, c, cm, d, dm,
435                e, em, f, fm, g, gm, h, hm);
436        notifySlotListeners(s);
437    }
438
439    public void function45Through52Packet(DccLocoAddress address,
440            boolean a, boolean am,
441            boolean b, boolean bm,
442            boolean c, boolean cm,
443            boolean d, boolean dm,
444            boolean e, boolean em,
445            boolean f, boolean fm,
446            boolean g, boolean gm,
447            boolean h, boolean hm) {
448        SprogSlot s = this.findF45to52Packet(address);
449        s.f45to52packet(address.getNumber(), address.isLongAddress(),
450                a, am, b, bm, c, cm, d, dm,
451                e, em, f, fm, g, gm, h, hm);
452        notifySlotListeners(s);
453    }
454
455    public void function53Through60Packet(DccLocoAddress address,
456            boolean a, boolean am,
457            boolean b, boolean bm,
458            boolean c, boolean cm,
459            boolean d, boolean dm,
460            boolean e, boolean em,
461            boolean f, boolean fm,
462            boolean g, boolean gm,
463            boolean h, boolean hm) {
464        SprogSlot s = this.findF53to60Packet(address);
465        s.f53to60packet(address.getNumber(), address.isLongAddress(),
466                a, am, b, bm, c, cm, d, dm,
467                e, em, f, fm, g, gm, h, hm);
468        notifySlotListeners(s);
469    }
470
471    public void function61Through68Packet(DccLocoAddress address,
472            boolean a, boolean am,
473            boolean b, boolean bm,
474            boolean c, boolean cm,
475            boolean d, boolean dm,
476            boolean e, boolean em,
477            boolean f, boolean fm,
478            boolean g, boolean gm,
479            boolean h, boolean hm) {
480        SprogSlot s = this.findF61to68Packet(address);
481        s.f61to68packet(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     * Handle speed changes from throttle.
489     * <p>
490     * As well as updating an existing slot,
491     * or creating a new on where necessary, the speed command is added to the
492     * queue of packets to be sent immediately.This ensures minimum latency
493     * between the user adjusting the throttle and a loco responding, rather
494     * than possibly waiting for a complete traversal of all slots before the
495     * new speed is actually sent to the hardware.
496     *
497     * @param mode speed step mode.
498     * @param address loco address.
499     * @param spd speed to send.
500     * @param isForward true if forward, else false.
501     */
502    public void setSpeed(jmri.SpeedStepMode mode, DccLocoAddress address, int spd, boolean isForward) {
503        SprogSlot s = this.findAddressSpeedPacket(address);
504        if (s != null) { // May need an error here - if all slots are full!
505            s.setSpeed(mode, address.getNumber(), address.isLongAddress(), spd, isForward);
506            notifySlotListeners(s);
507            log.debug("Registering new speed");
508            sendNow.add(s);
509        }
510    }
511
512    public SprogSlot opsModepacket(int address, boolean longAddr, int cv, int val) {
513        SprogSlot s = findFree();
514        if (s != null) {
515            s.setOps(address, longAddr, cv, val);
516            if (log.isDebugEnabled()) {
517                log.debug("opsModePacket() Notify ops mode packet for address {}", address);
518            }
519            notifySlotListeners(s);
520            return (s);
521        } else {
522             return (null);
523        }
524    }
525
526    public void release(DccLocoAddress address) {
527        SprogSlot s;
528        while ((s = findAddress(address)) != null) {
529            s.clear();
530            notifySlotListeners(s);
531        }
532    }
533
534    /**
535     * Send emergency stop to all slots.
536     */
537    public void estopAll() {
538        slots.stream().filter((s) -> ((s.getRepeat() == -1)
539                && s.slotStatus() != SprogConstants.SLOT_FREE
540                && s.speed() != 1)).forEach((s) -> {
541                    eStopSlot(s);
542                });
543    }
544
545    /**
546     * Send emergency stop to a slot.
547     *
548     * @param s SprogSlot to eStop
549     */
550    protected void eStopSlot(SprogSlot s) {
551        log.debug("Estop slot: {} for address: {}", s.getSlotNumber(), s.getAddr());
552        s.eStop();
553        notifySlotListeners(s);
554    }
555
556    // data members to hold contact with the slot listeners
557    final private Vector<SprogSlotListener> slotListeners = new Vector<>();
558
559    public synchronized void addSlotListener(SprogSlotListener l) {
560        // add only if not already registered
561        slotListeners.addElement(l);
562    }
563
564    public synchronized void removeSlotListener(SprogSlotListener l) {
565        slotListeners.removeElement(l);
566    }
567
568    /**
569     * Trigger the notification of all SlotListeners.
570     *
571     * @param s The changed slot to notify.
572     */
573    private synchronized void notifySlotListeners(SprogSlot s) {
574        log.debug("notifySlotListeners() notify {} SlotListeners about slot for address {}",
575                    slotListeners.size(), s.getAddr());
576
577        // forward to all listeners
578        slotListeners.stream().forEach((client) -> {
579            client.notifyChangedSlot(s);
580        });
581    }
582
583    /**
584     * Set initial power state
585     * 
586     * If connection option is set for track power on the property change is sent
587     * before we are registered with the power manager so force a change in the
588     * slot thread
589     * 
590     * @param powerOption true if power on at startup
591     */
592    public void setPowerState(boolean powerOption) {
593        if (powerOption == true) {
594            powerChanged = true;
595            powerState = PowerManager.ON;
596        }
597    }
598    
599    @Override
600    /**
601     * The run() method will only be called (from SprogSystemConnectionMemo
602     * ConfigureCommandStation()) if the connected SPROG is in Command Station mode.
603     *
604     */
605    public void run() {
606        log.debug("Command station slot thread starts");
607        while(true) {
608            try {
609                synchronized(lock) {
610                    lock.wait(SprogConstants.CS_REPLY_TIMEOUT);
611                }
612            } catch (InterruptedException e) {
613               log.debug("Slot thread interrupted");
614               // We'll loop around if there's no reply available yet
615               // Save the interrupted status for anyone who may be interested
616               Thread.currentThread().interrupt();
617               // and exit
618               return;
619            }
620            log.debug("Slot thread wakes");
621
622            if (powerMgr == null) {
623                // Wait until power manager is available
624                powerMgr = InstanceManager.getNullableDefault(jmri.PowerManager.class);
625                if (powerMgr == null) {
626                    log.info("No power manager instance found");
627                } else {
628                    log.info("Registering with power manager");
629                    powerMgr.addPropertyChangeListener(this);
630                }
631            } else {
632                if (sendSprogAddress) {
633                    // If we need to change the SPROGs default address, do that immediately,
634                    // regardless of the power state.
635                    log.debug("Set new address");
636                    sendMessage(new SprogMessage("A " + currentSprogAddress + " 0"));
637                    replyAvailable = false;
638                    sendSprogAddress = false;
639                } else if (powerChanged && (powerState == PowerManager.ON) && !waitingForReply) {
640                    // Power has been turned on so send an idle packet to start the
641                    // message/reply handshake
642                    log.debug("Send idle to start message/reply handshake");
643                    sendPacket(jmri.NmraPacket.idlePacket(), SprogConstants.S_REPEATS);
644                    powerChanged = false;
645                    time = System.currentTimeMillis();
646                } else if (replyAvailable && (powerState == PowerManager.ON)) {
647                    log.debug("Reply available");
648                    // Received a reply whilst power is on, so send another packet
649                    // Get next packet to send if track power is on
650                    byte[] p;
651                    SprogSlot s = sendNow.poll();
652                    if (s != null) {
653                        // New throttle action to be sent immediately
654                        p = s.getPayload();
655                        log.debug("Packet from immediate send queue");
656                    } else {
657                        // Or take the next one from the stack
658                        p = getNextPacket();
659                        if (p != null) {
660                            log.debug("Packet from stack");
661                        }
662                    }
663                    replyAvailable = false;
664                    if (p != null) {
665                        // Send the packet
666                        sendPacket(p, SprogConstants.S_REPEATS);
667                        log.debug("Packet sent");
668                    } else {
669                        // Send a decoder idle packet to prompt a reply from hardware and keep things running
670                        log.debug("Idle sent");
671                        sendPacket(jmri.NmraPacket.idlePacket(), SprogConstants.S_REPEATS);
672                    }
673                    timeNow = System.currentTimeMillis();
674                    packetDelay = timeNow - time;
675                    time = timeNow;
676                    // Useful for debug if packets are being delayed
677                    if (packetDelay > SprogConstants.PACKET_DELAY_WARN_THRESHOLD) {
678                        log.warn("Packet delay was {} ms", packetDelay);
679                    }
680                } else {
681                    if (powerState == PowerManager.ON) {
682
683                        // Should never get here. Something is wrong so turn power off
684                        // Kill reply wait so send doesn't block
685                        log.warn("Slot thread timeout - removing power");
686                        waitingForReply = false;
687                        try {
688                            powerMgr.setPower(PowerManager.OFF);
689                        } catch (JmriException ex) {
690                            log.error("Exception turning power off", ex);
691                        }
692                        JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("CSErrorFrameDialogString"),
693                            Bundle.getMessage("SprogCSTitle"), JmriJOptionPane.ERROR_MESSAGE);
694                    }
695                }
696            }
697        }
698    }
699
700    /**
701     * Get the next packet to be transmitted.
702     *
703     * @return byte[] null if no packet
704     */
705    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "PZLA_PREFER_ZERO_LENGTH_ARRAYS",
706        justification = "API defined by Sprog docs")
707    private byte[] getNextPacket() {
708        SprogSlot s;
709
710        if (!isBusy()) {
711            return null;
712        }
713        while (slots.get(currentSlot).isFree()) {
714            currentSlot++;
715            currentSlot = currentSlot % numSlots;
716        }
717        s = slots.get(currentSlot);
718        byte[] ret = s.getPayload();
719        // Resend ops packets until repeat count is exhausted so that
720        // decoder receives contiguous identical packets, otherwsie find
721        // next packet to send
722        if (!s.isOpsPkt() || (s.getRepeat() == 0)) {
723            currentSlot++;
724            currentSlot = currentSlot % numSlots;
725        }
726
727        if (s.isFinished()) {
728            notifySlotListeners(s);
729            //return null;
730        }
731
732        return ret;
733    }
734
735    /*
736     *
737     * @param m the sprog message received
738     */
739    @Override
740    public void notifyMessage(SprogMessage m) {
741    }
742
743    /**
744     * Handle replies.
745     * <p>
746     * Handle replies from the hardware, ignoring those that were not sent from
747     * the command station.
748     *
749     * @param m The SprogReply to be handled
750     */
751    @Override
752    public void notifyReply(SprogReply m) {
753        if (m.getId() != lastId) {
754            // Not my id, so not interested, message send still blocked
755            log.debug("Ignore reply with mismatched id {} looking for {}", m.getId(), lastId);
756            return;
757        } else {
758            log.debug("Reply received [{}]", m.toString());
759            // Log the reply and wake the slot thread
760            synchronized (lock) {
761                replyAvailable = true;
762                lock.notifyAll();
763            }
764        }
765    }
766
767    /**
768     * implement a property change listener for power
769     */
770    @Override
771    public void propertyChange(java.beans.PropertyChangeEvent evt) {
772        log.debug("propertyChange {} = {}", evt.getPropertyName(), evt.getNewValue());
773        if (evt.getPropertyName().equals(PowerManager.POWER)) {
774            powerState = powerMgr.getPower();
775            powerChanged = true;
776        }
777    }
778
779    /**
780     * Provide a count of the slots in use.
781     *
782     * @return the number of slots in use
783     */
784    public int getInUseCount() {
785        int result = 0;
786        for (SprogSlot s : slots) {
787            if (!s.isFree()) {
788                result++;
789            }
790        }
791        return result;
792    }
793
794    /**
795     *
796     * @return a boolean if the command station is busy - i.e. it has at least
797     *         one occupied slot
798     */
799    public boolean isBusy() {
800        return slots.stream().anyMatch((s) -> (!s.isFree()));
801    }
802
803    public void setSystemConnectionMemo(SprogSystemConnectionMemo memo) {
804        adaptermemo = memo;
805    }
806
807    SprogSystemConnectionMemo adaptermemo;
808
809    /**
810     * Get user name.
811     *
812     * @return the user name
813     */
814    @Override
815    public String getUserName() {
816        if (adaptermemo == null) {
817            return "Sprog";
818        }
819        return adaptermemo.getUserName();
820    }
821
822    /**
823     * Get system prefix.
824     *
825     * @return the system prefix
826     */
827    @Override
828    public String getSystemPrefix() {
829        if (adaptermemo == null) {
830            return "S";
831        }
832        return adaptermemo.getSystemPrefix();
833    }
834
835    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SprogCommandStation.class);
836
837}