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}