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