001package jmri.jmrix.loconet;
002
003import java.util.ArrayList;
004
005import jmri.Consist;
006import jmri.ConsistListener;
007import jmri.LocoAddress;
008import jmri.DccLocoAddress;
009import jmri.ThrottleListener;
010
011/**
012 * LocoNetConsist.java
013 * This is the Consist definition for a consist on a LocoNet system.
014 * It uses the LocoNet specific commands to build a consist.
015 * @author Paul Bender Copyright (C) 2011
016 */
017public class LocoNetConsist extends jmri.implementation.DccConsist implements SlotListener, ThrottleListener {
018
019    private SlotManager slotManager = null;
020    private LnTrafficController trafficController = null;
021    private jmri.jmrix.AbstractThrottleManager throttleManager = null;
022    private LocoNetSlot leadSlot = null;
023
024    private ArrayList<DccLocoAddress> needToWrite = null;
025    // Mirrors needToWrite, but for pending removals -- see the field
026    // report on removeFromCSConsist() below for why this exists.
027    private ArrayList<DccLocoAddress> needToRemove = null;
028
029    // Address the current pending slot request is for. notifyChangedSlot()
030    // checks a response against this before acting on it -- a single
031    // throttle can hold more than one address on a live LocoNet bus, so a
032    // mismatched response must never be linked in as the requested engine.
033    private DccLocoAddress pendingSlotAddress = null;
034    // Bounds the mismatch-triggered retry in unexpectedSlotAddress()
035    // below so a genuinely dead/unreachable address can't retry
036    // forever. Reset to 0 on any correctly-matched response.
037    private int consecutiveSlotMismatches = 0;
038    private static final int MAX_CONSECUTIVE_SLOT_MISMATCHES = 5;
039
040    // State Machine states
041    static final int IDLESTATE = 0;
042    static final int LEADREQUESTSTATE = 1;
043    static final int LINKSTAGEONESTATE = 2;
044    static final int LINKSTAGETWOSTATE = 4;
045    static final int LINKSTAGETHREESTATE = 8;
046    static final int UNLINKSTAGEONESTATE = 16;
047    static final int FREESLOTSTATE = 32;
048
049    private int consistRequestState = IDLESTATE;
050
051    // Initialize a consist for the specific address
052    // the Default consist type for LocoNet is a Command
053    // Station Consist.
054    public LocoNetConsist(int address, LocoNetSystemConnectionMemo lm) {
055        super(address);
056        this.slotManager = lm.getSlotManager();
057        this.trafficController = lm.getLnTrafficController();
058        this.throttleManager = (jmri.jmrix.AbstractThrottleManager) lm.getThrottleManager();
059        consistRequestState = LEADREQUESTSTATE;
060        consistType = Consist.CS_CONSIST;
061        needToWrite = new ArrayList<>();
062        needToRemove = new ArrayList<>();
063        throttleManager.requestThrottle(consistAddress, LocoNetConsist.this, false);
064    }
065
066    // Initialize a consist for the specific address
067    // the Default consist type for LocoNet is a Command
068    // Station Consist.
069    public LocoNetConsist(DccLocoAddress address, LocoNetSystemConnectionMemo lm) {
070        super(address);
071        this.slotManager = lm.getSlotManager();
072        this.trafficController = lm.getLnTrafficController();
073        this.throttleManager = (jmri.jmrix.AbstractThrottleManager) lm.getThrottleManager();
074        consistRequestState = LEADREQUESTSTATE;
075        consistType = Consist.CS_CONSIST;
076        needToWrite = new ArrayList<>();
077        needToRemove = new ArrayList<>();
078        throttleManager.requestThrottle(consistAddress, LocoNetConsist.this, false);
079    }
080
081    // Set the Consist Type
082    @Override
083    public void setConsistType(int type) {
084        switch (type) {
085            case Consist.ADVANCED_CONSIST:
086            case Consist.CS_CONSIST:
087                consistType = type;
088                break;
089            default:
090                log.error("Consist Type Not Supported");
091                notifyConsistListeners(new DccLocoAddress(0, false), ConsistListener.NotImplemented);
092                break;
093        }
094    }
095
096    /**
097     * Is this address allowed?
098     * On LocoNet systems, All addresses can be used in a Universal Consist
099     * and only 0 is not allowed in Advanced Consists.
100     * {@inheritDoc}
101     */
102    @Override
103    public boolean isAddressAllowed(DccLocoAddress address) {
104        return consistType == Consist.CS_CONSIST || (address.getNumber() != 0);
105    }
106
107    /**
108     * Is there a size limit for this consist?
109     * @return -1 (no limit) for
110     * both CS and Advanced Consists,
111     * 0 for any other consist type.
112     */
113    @Override
114    public int sizeLimit() {
115        switch (consistType) {
116            case ADVANCED_CONSIST:
117            case CS_CONSIST:
118                return -1;
119            default:
120                return 0;
121        }
122    }
123
124    // does the consist contain the specified address?
125    @Override
126    public boolean contains(DccLocoAddress address) {
127        if (consistType == ADVANCED_CONSIST || consistType == CS_CONSIST) {
128            return consistList.contains(address);
129        } else {
130            log.error("Consist Type Not Supported");
131            notifyConsistListeners(address, ConsistListener.NotImplemented);
132        }
133        return false;
134    }
135
136    // get the relative direction setting for a specific
137    // locomotive in the consist
138    @Override
139    public boolean getLocoDirection(DccLocoAddress address) {
140        log.debug("consist {} obtaining direction for {} Consist List Size {}",
141            consistAddress, address, consistList.size());
142        if (consistType == ADVANCED_CONSIST || consistType == CS_CONSIST) {
143            if (address == consistAddress) {
144                return true;
145            }
146            if (consistList.contains(address)) {
147                return consistDir.getOrDefault(address, false);
148            } else {
149                return (true);
150            }
151        } else {
152            log.error("Consist Type Not Supported");
153            notifyConsistListeners(address, ConsistListener.NotImplemented);
154        }
155        return false;
156    }
157
158    /**
159     * Add an Address to the internal Consist list object.
160     */
161    private synchronized void addToConsistList(DccLocoAddress locoAddress, boolean directionNormal) {
162        if (!(consistList.contains(locoAddress))) {
163            consistList.add(locoAddress);
164        }
165        if (consistDir.containsKey(locoAddress)) {
166            consistDir.remove(locoAddress);
167        }
168        consistDir.put(locoAddress, directionNormal);
169    }
170
171    /**
172     * Remove an address from the internal Consist list object.
173     */
174    private synchronized void removeFromConsistList(DccLocoAddress locoAddress) {
175        // Can already be null if dispose() ran first -- this is also
176        // reached asynchronously from LnThrottleManager's retry thread.
177        if (consistDir == null || consistList == null) {
178            return;
179        }
180        consistDir.remove(locoAddress);
181        consistList.remove(locoAddress);
182    }
183
184    /**
185     * Add a Locomotive to a Consist.
186     *
187     * @param locoAddress     the Locomotive address to add to the locomotive
188     * @param directionNormal if the locomotive is traveling
189     *        the same direction as the consist, false otherwise
190     */
191    @Override
192    public synchronized void add(DccLocoAddress locoAddress, boolean directionNormal) {
193        if (locoAddress == consistAddress) {
194            // this is required for command station consists on LocoNet.
195            addToConsistList(locoAddress, directionNormal);
196            notifyConsistListeners(locoAddress, ConsistListener.OPERATION_SUCCESS);
197        } else if (consistType == ADVANCED_CONSIST) {
198            if (consistList.contains(locoAddress)) {
199                // we are changing the direction, so remove first,
200                // then add
201                removeFromAdvancedConsist(locoAddress);
202            }
203            addToConsistList(locoAddress, directionNormal);
204            if (leadSlot == null || consistRequestState != IDLESTATE) {
205                needToWrite.add(locoAddress);
206            } else {
207                addToAdvancedConsist(locoAddress, directionNormal);
208            }
209        } else if (consistType == CS_CONSIST) {
210            if (consistList.contains(locoAddress)) {
211                // we are changing the direction, so remove first,
212                // then add
213                removeFromCSConsist(locoAddress);
214            }
215            addToConsistList(locoAddress, directionNormal);
216            if (leadSlot == null || consistRequestState != IDLESTATE) {
217                needToWrite.add(locoAddress);
218            } else {
219                addToCSConsist(locoAddress, directionNormal);
220            }
221        } else {
222            log.error("Consist Type Not Supported");
223            notifyConsistListeners(locoAddress, ConsistListener.NotImplemented);
224        }
225    }
226
227    private synchronized void delayedAdd() {
228        if (consistList == null) {
229            // This consist was dispose()'d (deleted) while this add was
230            // still queued behind an in-flight slot request -- nothing
231            // left to add it to. See the field report on the
232            // "missing lead slot" branch in unlinkSlots().
233            return;
234        }
235        DccLocoAddress locoAddress = needToWrite.get(0);
236        if (consistType == ADVANCED_CONSIST) {
237            addToAdvancedConsist(locoAddress, getLocoDirection(locoAddress));
238        } else if (consistType == CS_CONSIST) {
239            addToCSConsist(locoAddress, getLocoDirection(locoAddress));
240        }
241        needToWrite.remove(locoAddress);
242    }
243
244    /**
245     * Process one queued removal. See the field report on
246     * removeFromCSConsist() for why removals need to be queued rather
247     * than fired immediately -- this drains that queue the same way
248     * delayedAdd() drains needToWrite.
249     */
250    private synchronized void delayedRemove() {
251        if (needToRemove.isEmpty()) {
252            return;
253        }
254        // The consist's own address, if queued, must always drain LAST --
255        // freeing its slot before every other queued follower is actually
256        // unlinked (not just requested) would orphan their real LocoNet
257        // link. Prefer any non-lead entry first.
258        DccLocoAddress locoAddress = needToRemove.get(0);
259        if (locoAddress.equals(consistAddress) && needToRemove.size() > 1) {
260            for (DccLocoAddress candidate : needToRemove) {
261                if (!candidate.equals(consistAddress)) {
262                    locoAddress = candidate;
263                    break;
264                }
265            }
266        }
267        needToRemove.remove(locoAddress);
268        if (consistType == ADVANCED_CONSIST) {
269            removeFromAdvancedConsist(locoAddress);
270        } else if (consistType == CS_CONSIST) {
271            removeFromCSConsist(locoAddress);
272        }
273    }
274
275    /**
276     * Called at every point where a slot operation has just finished and
277     * consistRequestState is (or is about to become) IDLESTATE. Drains
278     * needToRemove ahead of needToWrite -- queued deletes should not sit
279     * behind queued adds -- and only leaves the state machine idle once
280     * both queues are empty.
281     */
282    private synchronized void processQueuedWork() {
283        if (needToRemove.isEmpty() && needToWrite.isEmpty()) {
284            consistRequestState = IDLESTATE;
285            return;
286        }
287        // Deferred via invokeLater() -- draining synchronously here can
288        // recurse indefinitely and stack-overflow, since requestThrottle()
289        // calls back synchronously when a throttle is already cached, with
290        // no base case to stop the chain.
291        javax.swing.SwingUtilities.invokeLater(() -> {
292            synchronized (LocoNetConsist.this) {
293                // Must reset to IDLESTATE before draining the next item --
294                // otherwise it sees "still busy" and re-queues instead of
295                // processing, looping forever without ever completing.
296                consistRequestState = IDLESTATE;
297                if (!needToRemove.isEmpty()) {
298                    delayedRemove();
299                } else if (!needToWrite.isEmpty()) {
300                    delayedAdd();
301                }
302            }
303        });
304    }
305
306    /**
307     * Restore a Locomotive to a Consist, but don't write to
308     * the command station.
309     * This is used for restoring the consist
310     * from a file or adding a consist read from the command station.
311     *
312     * @param locoAddress     the Locomotive address to add to the locomotive
313     * @param directionNormal True if the locomotive is traveling
314     *        the same direction as the consist, false otherwise
315     */
316    @Override
317    public synchronized void restore(DccLocoAddress locoAddress, boolean directionNormal) {
318        switch (consistType) {
319            case ADVANCED_CONSIST:
320            case CS_CONSIST:
321                addToConsistList(locoAddress, directionNormal);
322                break;
323            default:
324                log.error("Consist Type Not Supported");
325                notifyConsistListeners(locoAddress, ConsistListener.NotImplemented);
326                break;
327        }
328    }
329
330    /**
331     *  Remove a Locomotive from this Consist.
332     *
333     *  @param locoAddress is the Locomotive address to add to the locomotive
334     */
335    @Override
336    public synchronized void remove(DccLocoAddress locoAddress) {
337        switch (consistType) {
338            case ADVANCED_CONSIST:
339                removeFromAdvancedConsist(locoAddress);
340                removeFromConsistList(locoAddress);
341                break;
342            case CS_CONSIST:
343                removeFromCSConsist(locoAddress);
344                removeFromConsistList(locoAddress);
345                break;
346            default:
347                log.error("Consist Type Not Supported");
348                notifyConsistListeners(locoAddress, ConsistListener.NotImplemented);
349                break;
350        }
351    }
352
353    /**
354     *  Add a Locomotive to an Advanced Consist.
355     *
356     *  @param locoAddress     the Locomotive address to add to the locomotive
357     *  @param directionNormal True if the locomotive is traveling
358     *        the same direction as the consist, false otherwise
359     */
360    @Override
361    protected synchronized void addToAdvancedConsist(DccLocoAddress locoAddress, boolean directionNormal) {
362        log.debug("Add Locomotive {} to advanced consist {} With Direction Normal {}.",
363            locoAddress, consistAddress, directionNormal);
364        //set the value in the roster entry for CV19
365        setRosterEntryCVValue(locoAddress);
366        consistRequestState = LINKSTAGEONESTATE;
367        throttleManager.requestThrottle(locoAddress, this, false);
368    }
369
370    /**
371     *  Remove a Locomotive from an Advanced Consist
372     *  @param locoAddress is the Locomotive address to add to the locomotive
373     */
374    @Override
375    protected synchronized void removeFromAdvancedConsist(DccLocoAddress locoAddress) {
376        log.debug(" Remove Locomotive {} from advanced consist {}", locoAddress, consistAddress);
377        //reset the value in the roster entry for CV19
378        resetRosterEntryCVValue(locoAddress);
379        slotManager.slotFromLocoAddress(locoAddress.getNumber(), this);
380        consistRequestState = UNLINKSTAGEONESTATE;
381    }
382
383    /**
384     *  Add a Locomotive to a LocoNet Universal Consist.
385     *  @param locoAddress is the Locomotive address to add to the locomotive
386     *  @param directionNormal is True if the locomotive is traveling
387     *        the same direction as the consist, or false otherwise.
388     */
389    private synchronized void addToCSConsist(DccLocoAddress locoAddress, boolean directionNormal) {
390        log.debug("Add Locomotive {} to Standard Consist {} With Direction Normal {}.",
391            locoAddress, consistAddress, directionNormal);
392        if(consistList.size()<=1 && locoAddress.equals(consistAddress)){
393          // there is only one address in this consist, no reason to link.
394          // Must still call processQueuedWork() here -- add()'s reference
395          // equality check means the lead's own address reaches this
396          // no-op branch almost every time, and without this the queue
397          // chain dead-ends before any real follower is ever processed.
398          notifyConsistListeners(locoAddress,ConsistListener.OPERATION_SUCCESS);
399          processQueuedWork();
400          return;
401        }
402        pendingSlotAddress = locoAddress;
403        throttleManager.requestThrottle(locoAddress, this, false);
404        // skip right to stage 2, we do not need to status edit.
405        consistRequestState = LINKSTAGETWOSTATE;
406    }
407
408    /**
409     *  Remove a Locomotive from a LocoNet Universal Consist.
410     *  @param locoAddress is the Locomotive address to add to the locomotive.
411     */
412    public synchronized void removeFromCSConsist(DccLocoAddress locoAddress) {
413        log.debug("Remove Locomotive {} from Standard Consist {}.", locoAddress, consistAddress);
414        // dispose() removes every member synchronously with no pacing, all
415        // sharing this one consistRequestState field -- without queuing
416        // here (like add() already does), concurrent slot requests
417        // overwrite each other and most removals are silently lost.
418        if (consistRequestState != IDLESTATE) {
419            needToRemove.add(locoAddress);
420            return;
421        }
422        if(consistList == null || (locoAddress.equals(consistAddress) && (consistList.isEmpty() || consistList.size()==1))){
423          // there is only one address in this consist, no reason to
424          // unlink -- but the slot is still marked In Use/Common from
425          // when the consist was built, so fetch it and free it
426          // directly rather than leaving it stuck (see FREESLOTSTATE
427          // in notifyChangedSlot()).
428          //
429          // consistList == null covers the lead's own deferred removal
430          // arriving after dispose()'s loop has already finished and
431          // nulled it out; consistList.isEmpty() covers the same "nothing
432          // left to unlink against" case but before the null reset --
433          // removeFromConsistList() strips an address synchronously the
434          // instant remove() is called, regardless of whether the real
435          // async unlink completed, so this can read back empty before
436          // the lead's own deferred removal is retried. Both need the
437          // same "just free the slot" treatment as the sole-member case.
438          pendingSlotAddress = locoAddress;
439          slotManager.slotFromLocoAddress(locoAddress.getNumber(), this);
440          consistRequestState = FREESLOTSTATE;
441          return;
442        }
443        if (locoAddress.equals(consistAddress)) {
444          // leadSlot is cached once at construction and never updated, so
445          // removing the lead's own address while other members are still
446          // tracked would fall through to the generic unlink path below
447          // and resolve BOTH lead and follow to that same slot object --
448          // tripping unlinkSlots()'s lead==follow guard, which sends no
449          // LocoNet message and never retries, permanently stranding the
450          // slot as "In Use". Deferring here until the lead is genuinely
451          // the last member left (the branch above) is the only safe
452          // order: freeing its slot while followers are still linked to
453          // it by slot number on the real hardware would corrupt the
454          // consist there even though JMRI's own bookkeeping looks fine.
455          needToRemove.add(locoAddress);
456          return;
457        }
458        pendingSlotAddress = locoAddress;
459        slotManager.slotFromLocoAddress(locoAddress.getNumber(), this);
460        consistRequestState = UNLINKSTAGEONESTATE;
461    }
462
463    /**
464     * create and send a message to link two slots
465     * @param lead is the slot which is the leader
466     * @param follow is the slot which will follow the leader
467     */
468    private void linkSlots(LocoNetSlot lead, LocoNetSlot follow) {
469        LocoNetMessage msg;
470        if (lead != follow) {
471            if (slotManager.getLoconetProtocol() == LnConstants.LOCONETPROTOCOL_TWO) {
472                msg = new LocoNetMessage(6);
473                int dest1 = follow.getSlot() / 128;
474                int dest2 = follow.getSlot() % 128;
475                int src1 = lead.getSlot() / 128;
476                int src2 = lead.getSlot() % 128;
477                msg.setOpCode(LnConstants.OPC_EXP_SLOT_MOVE_RE_OPC_IB2_SPECIAL);
478                msg.setElement(1, dest1 | 0b00111000);
479                msg.setElement(2, dest2 & 0x7F);
480                msg.setElement(3, src1  | 0b01000000);
481                msg.setElement(4, src2 & 0x7F);
482            } else {
483                msg = new LocoNetMessage(4);
484                msg.setOpCode(LnConstants.OPC_LINK_SLOTS);
485                msg.setElement(1, follow.getSlot());
486                msg.setElement(2, lead.getSlot());
487            }
488            trafficController.sendLocoNetMessage(msg);
489        } else {
490          // lead == follow
491          // this is an error, notify the consist listeners.
492          follow.removeSlotListener(this);
493          notifyConsistListeners(new DccLocoAddress(follow.locoAddr(),
494                throttleManager.canBeLongAddress(follow.locoAddr())),
495                ConsistListener.CONSIST_ERROR);
496        }
497        processQueuedWork();
498    }
499
500    /**
501     * create and send a message to unlink two slots
502     * @param lead is the slot which is the leader
503     * @param follow is the slot which was following the leader
504     */
505    private void unlinkSlots(LocoNetSlot lead, LocoNetSlot follow) {
506        if (lead == null || follow == null) {
507            // Can happen for a consist JMRI rediscovered from the layout
508            // (requestUpdateFromLayout()) whose lead slot never resolved
509            // -- e.g. the command station never answered the lead's slot
510            // request. There's nothing to build a valid unlink message
511            // with, but still free whatever slot IS known rather than
512            // crashing with an NPE and leaving it stuck In Use.
513            log.warn("unlinkSlots(): missing {} slot for consist {} -- cannot send unlink, freeing what's known",
514                    lead == null ? "lead" : "follow", consistAddress);
515            if (follow != null) {
516                trafficController.sendLocoNetMessage(follow.writeStatus(LnConstants.LOCO_FREE));
517                follow.removeSlotListener(this);
518            }
519            processQueuedWork();
520            return;
521        }
522        LocoNetMessage msg;
523        if (lead != follow) {
524            if (slotManager.getLoconetProtocol() == LnConstants.LOCONETPROTOCOL_TWO) {
525                msg = new LocoNetMessage(6);
526                int src1 = lead.getSlot() / 128;
527                int src2 = lead.getSlot() % 128;
528                int dest1 = follow.getSlot() / 128;
529                int dest2 = follow.getSlot() % 128;
530                msg.setOpCode(LnConstants.OPC_EXP_SLOT_MOVE_RE_OPC_IB2_SPECIAL);
531                msg.setElement(3, src1 | 0b01010000);
532                msg.setElement(4, src2 & 0x7F);
533                msg.setElement(1, dest1 | 0b00111000);
534                msg.setElement(2, dest2 & 0x7F);
535            } else {
536                msg = new LocoNetMessage(4);
537                msg.setOpCode(LnConstants.OPC_UNLINK_SLOTS);
538                msg.setElement(1, follow.getSlot());
539                msg.setElement(2, lead.getSlot());
540            }
541            trafficController.sendLocoNetMessage(msg);
542            // Unlinking only breaks the LocoNet slot link -- it does not
543            // touch the follower's own slot status, which otherwise stays
544            // In Use/Common forever after being removed from a consist.
545            // Match what the Slot Monitor's own manual Free button sends
546            // (see SlotMonDataModel.setValueAt(), DISPCOLUMN case).
547            trafficController.sendLocoNetMessage(follow.writeStatus(LnConstants.LOCO_FREE));
548        } else {
549          // lead == follow
550          // this is an error, notify the consist listeners.
551          follow.removeSlotListener(this);
552          notifyConsistListeners(new DccLocoAddress(follow.locoAddr(),
553                throttleManager.canBeLongAddress(follow.locoAddr())),
554                ConsistListener.CONSIST_ERROR | ConsistListener.DELETE_ERROR );
555        }
556        processQueuedWork();
557    }
558
559    /**
560     * Free a slot directly, with no unlink -- used when deleting a
561     * command-station consist down to its last (lead) engine, where
562     * there's nothing left linked to unlink but the slot itself is
563     * still marked In Use/Common from when the consist was built.
564     * @param s the slot to free
565     */
566    private void freeSlot(LocoNetSlot s) {
567        trafficController.sendLocoNetMessage(s.writeStatus(LnConstants.LOCO_FREE));
568        s.removeSlotListener(this);
569        notifyConsistListeners(new DccLocoAddress(s.locoAddr(),
570                throttleManager.canBeLongAddress(s.locoAddr())),
571                ConsistListener.OPERATION_SUCCESS);
572        processQueuedWork();
573    }
574
575    private void setDirection(LocoNetThrottle t) {
576        log.debug("consist {} set direction for {}", consistAddress, t.getLocoAddress());
577        // send a command to set the direction
578        // of the locomotive in the slot.
579        boolean directionNormal = getLocoDirection((DccLocoAddress) t.getLocoAddress());
580        if (directionNormal) {
581            t.setIsForward(leadSlot.isForward());
582        } else {
583            t.setIsForward(!leadSlot.isForward());
584        }
585
586        consistRequestState = LINKSTAGETWOSTATE;
587    }
588
589    private void setSlotModeAdvanced(LocoNetSlot s) {
590        // set the slot so that it can be an advanced consist
591        int oldstatus = s.slotStatus();
592        int newstatus = oldstatus | LnConstants.STAT1_SL_SPDEX;
593        trafficController.sendLocoNetMessage(s.writeStatus(newstatus));
594    }
595
596    /**
597     * Verify a slot response's actual address matches what was
598     * requested (pendingSlotAddress) before acting on it as though it
599     * were the follower/lead engine we asked for.
600     *
601     * A single physical throttle can hold more than one address on a live
602     * LocoNet bus, so a slot response isn't guaranteed to match what was
603     * requested -- acting on it unchecked can link the wrong engine in.
604     *
605     * @return true if the response was unexpected and should NOT be
606     *         acted on (already cleaned up and queue continued)
607     */
608    private boolean unexpectedSlotAddress(LocoNetSlot s) {
609        if (pendingSlotAddress == null) {
610            return false;
611        }
612        if (s.locoAddr() != pendingSlotAddress.getNumber()) {
613            // Re-queue rather than drop on a mismatch -- for removals
614            // specifically, dropping left JMRI's bookkeeping (already
615            // updated synchronously in remove()) out of sync with the
616            // real hardware, reporting an engine freed while it stayed
617            // linked. Bounded by consecutiveSlotMismatches so a dead
618            // address can't retry forever.
619            DccLocoAddress missed = pendingSlotAddress;
620            boolean wasRemoval = (consistRequestState == UNLINKSTAGEONESTATE || consistRequestState == FREESLOTSTATE);
621            log.warn("notifyChangedSlot(): requested slot for {} but got slot {} for address {} instead -- ignoring rather than linking/freeing the wrong engine",
622                    missed, s.getSlot(), s.locoAddr());
623            s.removeSlotListener(this);
624            pendingSlotAddress = null;
625            if (consecutiveSlotMismatches < MAX_CONSECUTIVE_SLOT_MISMATCHES) {
626                consecutiveSlotMismatches++;
627                if (wasRemoval) {
628                    needToRemove.add(0, missed);
629                } else {
630                    needToWrite.add(0, missed);
631                }
632            } else {
633                log.error("notifyChangedSlot(): giving up on {} after {} consecutive mismatched slot responses",
634                        missed, consecutiveSlotMismatches);
635                notifyConsistListeners(missed, ConsistListener.CONSIST_ERROR);
636            }
637            processQueuedWork();
638            return true;
639        }
640        pendingSlotAddress = null;
641        consecutiveSlotMismatches = 0;
642        return false;
643    }
644
645    // slot listener interface functions
646    @Override
647    public void notifyChangedSlot(LocoNetSlot s) {
648        log.debug("Notified slot {} changed with mode {} slot consist state: {}",
649            s.getSlot(), consistRequestState, LnConstants.CONSIST_STAT(s.consistStatus()));
650        switch (consistRequestState) {
651            case LEADREQUESTSTATE:
652                leadSlot = s;
653                consistRequestState = IDLESTATE;
654                break;
655            case LINKSTAGEONESTATE:
656                s.addSlotListener(this);
657                setSlotModeAdvanced(s);
658                consistRequestState = LINKSTAGETWOSTATE;
659                break;
660            case LINKSTAGETWOSTATE:
661                if (!unexpectedSlotAddress(s)) {
662                    linkSlots(leadSlot, s);
663                }
664                break;
665            case UNLINKSTAGEONESTATE:
666                if (!unexpectedSlotAddress(s)) {
667                    unlinkSlots(leadSlot, s);
668                }
669                break;
670            case FREESLOTSTATE:
671                if (!unexpectedSlotAddress(s)) {
672                    freeSlot(s);
673                }
674                break;
675            default:
676                s.removeSlotListener(this);
677                notifyConsistListeners(new DccLocoAddress(s.locoAddr(),
678                        throttleManager.canBeLongAddress(s.locoAddr())),
679                        ConsistListener.OPERATION_SUCCESS);
680                processQueuedWork();
681        }
682    }
683
684    // Throttle listener interface functions
685    @Override
686    public void notifyThrottleFound(jmri.DccThrottle t) {
687        log.debug("notified Throttle {} found with mode {}", t.getLocoAddress(), consistRequestState);
688        try {
689            if (consistRequestState == LEADREQUESTSTATE) {
690                ((LocoNetThrottle) t).setIsForward(true);
691                leadSlot = ((LocoNetThrottle) t).getLocoNetSlot();
692                processQueuedWork();
693            } else {
694                LocoNetSlot tempSlot = ((LocoNetThrottle) t).getLocoNetSlot();
695                if (tempSlot != null) {
696                    tempSlot.addSlotListener(this);
697                    if (consistRequestState == LINKSTAGEONESTATE) {
698                        notifyChangedSlot(tempSlot);
699                        setDirection(((LocoNetThrottle) t));
700                        consistRequestState = LINKSTAGETWOSTATE;
701                    } else {
702                        setDirection(((LocoNetThrottle) t));
703                    }
704                } else {
705                    log.error("Cannot notify a throttle's slot if the slot is null!");
706                }
707            }
708        } catch (java.lang.ClassCastException cce) {
709            // if the simulator is in use, we will
710            // get a ClassCastException.
711            if (consistRequestState == LEADREQUESTSTATE) {
712                t.setIsForward(true);
713                processQueuedWork();
714            } else {
715                if (t instanceof LocoNetThrottle) {
716                    LocoNetThrottle lt = (LocoNetThrottle)t;
717                    setDirection(lt);
718                }
719            }
720        }
721    }
722
723    @Override
724    public void notifyFailedThrottleRequest(LocoAddress address, String reason) {
725        if (! (address instanceof DccLocoAddress)) {
726            throw new IllegalArgumentException("address is not a DccLocoAddress object");
727        }
728        notifyConsistListeners((DccLocoAddress) address,
729                ConsistListener.CONSIST_ERROR);
730        removeFromConsistList((DccLocoAddress) address);
731        pendingSlotAddress = null;
732        processQueuedWork();
733    }
734
735    /**
736     * No steal or share decisions made locally
737     * <p>
738     * {@inheritDoc}
739     */
740    @Override
741    public void notifyDecisionRequired(jmri.LocoAddress address, DecisionType question) {
742        log.debug("notifydecisionrequired {} {}", address, question);
743    }
744
745    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LocoNetConsist.class);
746
747}