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