001package jmri.jmrix.loconet; 002 003import java.util.ArrayList; 004import java.util.Arrays; 005import java.util.Hashtable; 006import java.util.List; 007import java.util.Vector; 008import javax.annotation.Nonnull; 009import jmri.CommandStation; 010import jmri.ProgListener; 011import jmri.Programmer; 012import jmri.ProgrammingMode; 013import jmri.jmrix.AbstractProgrammer; 014import jmri.jmrix.loconet.SlotMapEntry.SlotType; 015 016/** 017 * Controls a collection of slots, acting as the counter-part of a LocoNet 018 * command station. 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 LocoNetSlot object itself. 024 * <p> 025 * Strictly speaking, functions 9 through 28 are not in the actual slot, but 026 * it's convenient to imagine there's an "extended slot" and keep track of them 027 * here. This is a partial implementation, though, because setting is still done 028 * directly in {@link LocoNetThrottle}. In particular, if this slot has not been 029 * read from the command station, the first message directly setting F9 through 030 * F28 will not have a place to store information. Instead, it will trigger a 031 * slot read, so the following messages will be properly handled. 032 * <p> 033 * Some of the message formats used in this class are Copyright Digitrax, Inc. 034 * and used with permission as part of the JMRI project. That permission does 035 * not extend to uses in other software products. If you wish to use this code, 036 * algorithm or these message formats outside of JMRI, please contact Digitrax 037 * Inc for separate permission. 038 * <p> 039 * This Programmer implementation is single-user only. It's not clear whether 040 * the command stations can have multiple programming requests outstanding (e.g. 041 * service mode and ops mode, or two ops mode) at the same time, but this code 042 * definitely can't. 043 * 044 * @author Bob Jacobsen Copyright (C) 2001, 2003, 2024 045 * @author B. Milhaupt, Copyright (C) 2018, 2024 046 */ 047public class SlotManager extends AbstractProgrammer implements LocoNetListener, CommandStation { 048 049 /** 050 * Time to wait after programming operation complete on LocoNet 051 * before reporting completion and hence starting next operation 052 */ 053 static public int postProgDelay = 50; // this is public to allow changes via script 054 055 public int slotScanInterval = 50; // this is public to allow changes via script and tests 056 057 public int serviceModeReplyDelay = 20; // this is public to allow changes via script and tests. Adjusted by UsbDcs210PlusAdapter 058 059 public int opsModeReplyDelay = 100; // this is public to allow changes via script and tests. 060 061 public boolean pmManagerGotReply = false; //this is public to allow changes via script and tests 062 063 public boolean supportsSlot250; 064 065 /** 066 * a Map of the CS slots. 067 */ 068 public List<SlotMapEntry> slotMap = new ArrayList<SlotMapEntry>(); 069 070 /** 071 * Constructor for a SlotManager on a given TrafficController. 072 * 073 * @param tc Traffic Controller to be used by SlotManager for communication 074 * with LocoNet 075 */ 076 public SlotManager(LnTrafficController tc) { 077 this.tc = tc; 078 079 // change timeout values from AbstractProgrammer superclass 080 LONG_TIMEOUT = 180000; // Fleischmann command stations take forever 081 SHORT_TIMEOUT = 8000; // DCS240 reads 082 083 // dummy slot map until command station set (if ever) 084 slotMap = Arrays.asList(new SlotMapEntry(0,0,SlotType.SYSTEM), 085 new SlotMapEntry(1,120,SlotType.LOCO), 086 new SlotMapEntry(121,127,SlotType.SYSTEM), 087 new SlotMapEntry(128,247,SlotType.UNKNOWN), 088 new SlotMapEntry(248,256,SlotType.SYSTEM), // potential stat slots 089 new SlotMapEntry(257,375,SlotType.UNKNOWN), 090 new SlotMapEntry(376,384,SlotType.SYSTEM), 091 new SlotMapEntry(385,432,SlotType.UNKNOWN)); 092 093 loadSlots(true); 094 095 // listen to the LocoNet 096 tc.addLocoNetListener(~0, this); 097 098 } 099 100 /** 101 * Initialize the slots array. 102 * @param initialize if true a new slot is created else it is just updated with type 103 * and protocol 104 */ 105 protected void loadSlots(boolean initialize) { 106 // initialize slot array 107 for (SlotMapEntry item : slotMap) { 108 for (int slotIx = item.getFrom(); slotIx <= item.getTo() ; slotIx++) { 109 if (initialize) { 110 _slots[slotIx] = new LocoNetSlot( slotIx,getLoconetProtocol(),item.getSlotType()); 111 } 112 else { 113 _slots[slotIx].setSlotType(item.getSlotType()); 114 } 115 } 116 } 117 } 118 119 protected LnTrafficController tc; 120 121 /** 122 * Send a DCC packet to the rails. This implements the CommandStation 123 * interface. This mechanism can pass any valid NMRA packet of up to 124 * 6 data bytes (including the error-check byte). 125 * 126 * When available, these messages are forwarded to LocoNet using a 127 * "throttledTransmitter". This decreases the speed with which these 128 * messages are sent, resulting in lower throughput, but fewer 129 * rejections by the command station on account of "buffer-overflow". 130 * 131 * @param packet the data bytes of the raw NMRA packet to be sent. The 132 * "error check" byte must be included, even though the LocoNet 133 * message will not include that byte; the command station 134 * will re-create the error byte from the bytes encoded in 135 * the LocoNet message. LocoNet is unable to propagate packets 136 * longer than 6 bytes (including the error-check byte). 137 * 138 * @param sendCount the total number of times the packet is to be 139 * sent on the DCC track signal (not LocoNet!). Valid range is 140 * between 1 and 8. sendCount will be forced to this range if it 141 * is outside of this range. 142 */ 143 @Override 144 public boolean sendPacket(byte[] packet, int sendCount) { 145 if (sendCount > 8) { 146 log.warn("Ops Mode Accessory Packet 'Send count' reduced from {} to 8.", sendCount); // NOI18N 147 sendCount = 8; 148 } 149 if (sendCount < 1) { 150 log.warn("Ops Mode Accessory Packet 'Send count' of {} is illegal and is forced to 1.", sendCount); // NOI18N 151 sendCount = 1; 152 } 153 if (packet.length <= 1) { 154 log.error("Invalid DCC packet length: {}", packet.length); // NOI18N 155 } 156 if (packet.length > 6) { 157 log.error("DCC packet length is too great: {} bytes were passed; ignoring the request. ", packet.length); // NOI18N 158 } 159 160 LocoNetMessage m = new LocoNetMessage(11); 161 m.setElement(0, LnConstants.OPC_IMM_PACKET); 162 m.setElement(1, 0x0B); 163 m.setElement(2, 0x7F); 164 // the incoming packet includes a check byte that's not included in LocoNet packet 165 int length = packet.length - 1; 166 167 m.setElement(3, ((sendCount - 1) & 0x7) + 16 * (length & 0x7)); 168 169 int highBits = 0; 170 if (length >= 1 && ((packet[0] & 0x80) != 0)) { 171 highBits |= 0x01; 172 } 173 if (length >= 2 && ((packet[1] & 0x80) != 0)) { 174 highBits |= 0x02; 175 } 176 if (length >= 3 && ((packet[2] & 0x80) != 0)) { 177 highBits |= 0x04; 178 } 179 if (length >= 4 && ((packet[3] & 0x80) != 0)) { 180 highBits |= 0x08; 181 } 182 if (length >= 5 && ((packet[4] & 0x80) != 0)) { 183 highBits |= 0x10; 184 } 185 m.setElement(4, highBits); 186 187 m.setElement(5, 0); 188 m.setElement(6, 0); 189 m.setElement(7, 0); 190 m.setElement(8, 0); 191 m.setElement(9, 0); 192 for (int i = 0; i < packet.length - 1; i++) { 193 m.setElement(5 + i, packet[i] & 0x7F); 194 } 195 196 if (throttledTransmitter != null) { 197 throttledTransmitter.sendLocoNetMessage(m); 198 } else { 199 tc.sendLocoNetMessage(m); 200 } 201 return true; 202 } 203 204 /* 205 * command station switches 206 */ 207 private final int SLOTS_DCS240 = 433; 208 private int numSlots = SLOTS_DCS240; // This is the largest number so far. 209 private int slot248CommandStationType; 210 private int slot248CommandStationSerial; 211 private int slot250InUseSlots; 212 private int slot250IdleSlots; 213 private int slot250FreeSlots; 214 215 /** 216 * The network protocol. 217 */ 218 private int loconetProtocol = LnConstants.LOCONETPROTOCOL_UNKNOWN; // defaults to unknown 219 220 /** 221 * 222 * @param value the loconet protocol supported 223 */ 224 public void setLoconet2Supported(int value) { 225 loconetProtocol = value; 226 } 227 228 /** 229 * Get the Command Station type reported in slot 248 message 230 * @return model 231 */ 232 public String getSlot248CommandStationType() { 233 return LnConstants.IPL_NAME(slot248CommandStationType); 234 } 235 236 /** 237 * Get the total number of slots reported in the slot250 message; 238 * @return number of slots 239 */ 240 public int getSlot250CSSlots() { 241 return slot250InUseSlots + slot250IdleSlots + slot250FreeSlots; 242 } 243 244 /** 245 * 246 * @return the loconet protocol supported 247 */ 248 public int getLoconetProtocol() { 249 return loconetProtocol; 250 } 251 252 /** 253 * Information on slot state is stored in an array of LocoNetSlot objects. 254 * This is declared final because we never need to modify the array itself, 255 * just its contents. 256 */ 257 protected LocoNetSlot _slots[] = new LocoNetSlot[getNumSlots()]; 258 259 /** 260 * Access the information in a specific slot. Note that this is a mutable 261 * access, so that the information in the LocoNetSlot object can be changed. 262 * 263 * @param i Specific slot, counted starting from zero. 264 * @return The Slot object 265 */ 266 public LocoNetSlot slot(int i) { 267 return _slots[i]; 268 } 269 270 public int getNumSlots() { 271 return numSlots; 272 } 273 /** 274 * Obtain a slot for a particular loco address. 275 * <p> 276 * This requires access to the command station, even if the locomotive 277 * address appears in the current contents of the slot array, to ensure that 278 * our local image is up-to-date. 279 * <p> 280 * This method sends an info request. When the echo of this is returned from 281 * the LocoNet, the next slot-read is recognized as the response. 282 * <p> 283 * The object that's looking for this information must provide a 284 * SlotListener to notify when the slot ID becomes available. 285 * <p> 286 * The SlotListener is not subscribed for slot notifications; it can do that 287 * later if it wants. We don't currently think that's a race condition. 288 * 289 * @param i Specific slot, counted starting from zero. 290 * @param l The SlotListener to notify of the answer. 291 */ 292 public void slotFromLocoAddress (int i, SlotListener l) { 293 // store connection between this address and listener for later 294 mLocoAddrHash.put(Integer.valueOf(i), l); 295 296 // send info request 297 LocoNetMessage m = new LocoNetMessage(4); 298 if (loconetProtocol != LnConstants.LOCONETPROTOCOL_TWO ) { 299 m.setOpCode(LnConstants.OPC_LOCO_ADR); // OPC_LOCO_ADR 300 } else { 301 m.setOpCode(LnConstants.OPC_EXP_REQ_SLOT); // Extended slot 302 } 303 m.setElement(1, (i / 128) & 0x7F); 304 m.setElement(2, i & 0x7F); 305 tc.sendLocoNetMessage(m); 306 } 307 308 javax.swing.Timer staleSlotCheckTimer = null; 309 310 /** 311 * Scan the slot array looking for slots that are in-use or common but have 312 * not had any updates in over 90s and issue a read slot request to update 313 * their state as the command station may have purged or stopped updating 314 * the slot without telling us via a LocoNet message. 315 * <p> 316 * This is intended to be called from the staleSlotCheckTimer 317 */ 318 private void checkStaleSlots() { 319 long staleTimeout = System.currentTimeMillis() - 90000; // 90 seconds ago 320 LocoNetSlot slot; 321 322 // We will just check the normal loco slots 1 to numSlots exclude systemslots 323 for (int i = 1; i < numSlots; i++) { 324 slot = _slots[i]; 325 if (!slot.isSystemSlot()) { 326 if ((slot.slotStatus() == LnConstants.LOCO_IN_USE || slot.slotStatus() == LnConstants.LOCO_COMMON) 327 && (slot.getLastUpdateTime() <= staleTimeout)) { 328 sendReadSlot(i); 329 break; // only send the first one found 330 } 331 } 332 } 333 } 334 335 336 java.util.TimerTask slot250Task = null; 337 /** 338 * Request slot data for 248 and 250 339 * Runs delayed 340 * <p> 341 * A call is trigger after the first slot response (PowerManager) received. 342 */ 343 private void pollSpecialSlots() { 344 sendReadSlot(248); 345 slot250Task = new java.util.TimerTask() { 346 @Override 347 public void run() { 348 try { 349 sendReadSlot(250); 350 } catch (Exception e) { 351 log.error("Exception occurred while checking slot250", e); 352 } 353 } 354 }; 355 jmri.util.TimerUtil.schedule(slot250Task,100); 356 } 357 358 /** 359 * Provide a mapping between locomotive addresses and the SlotListener 360 * that's interested in them. 361 */ 362 Hashtable<Integer, SlotListener> mLocoAddrHash = new Hashtable<>(); 363 364 // data members to hold contact with the slot listeners 365 final private Vector<SlotListener> slotListeners = new Vector<>(); 366 367 /** 368 * Add a slot listener, if it is not already registered 369 * <p> 370 * The slot listener will be invoked every time a slot changes state. 371 * 372 * @param l Slot Listener to be added 373 */ 374 public synchronized void addSlotListener(SlotListener l) { 375 // add only if not already registered 376 if (!slotListeners.contains(l)) { 377 slotListeners.addElement(l); 378 } 379 } 380 381 /** 382 * Add a slot listener, if it is registered. 383 * <p> 384 * The slot listener will be removed from the list of listeners which are 385 * invoked whenever a slot changes state. 386 * 387 * @param l Slot Listener to be removed 388 */ 389 public synchronized void removeSlotListener(SlotListener l) { 390 if (slotListeners.contains(l)) { 391 slotListeners.removeElement(l); 392 } 393 } 394 395 /** 396 * Trigger the notification of all SlotListeners. 397 * 398 * @param s The changed slot to notify. 399 */ 400 @SuppressWarnings("unchecked") 401 protected void notify(LocoNetSlot s) { 402 // make a copy of the listener vector to synchronized not needed for transmit 403 Vector<SlotListener> v; 404 synchronized (this) { 405 v = (Vector<SlotListener>) slotListeners.clone(); 406 } 407 log.debug("notify {} SlotListeners about slot {}", // NOI18N 408 v.size(), s.getSlot()); 409 // forward to all listeners 410 int cnt = v.size(); 411 for (int i = 0; i < cnt; i++) { 412 SlotListener client = v.elementAt(i); 413 client.notifyChangedSlot(s); 414 } 415 } 416 417 LocoNetMessage immedPacket; 418 419 /** 420 * Listen to the LocoNet. This is just a steering routine, which invokes 421 * others for the various processing steps. 422 * 423 * @param m incoming message 424 */ 425 @Override 426 public void message(LocoNetMessage m) { 427 if (m.getOpCode() == LnConstants.OPC_RE_LOCORESET_BUTTON) { 428 if (commandStationType.getSupportsLocoReset()) { 429 // Command station LocoReset button was triggered. 430 // 431 // Note that sending a LocoNet message using this OpCode to the command 432 // station does _not_ seem to trigger the equivalent effect; only 433 // pressing the button seems to do so. 434 // If the OpCode is received by JMRI, regardless of its source, 435 // JMRI will simply trigger a re-read of all slots. This will 436 // allow the JMRI slots to stay consistent with command station 437 // slot information, regardless of whether the command station 438 // just modified the slot information. 439 javax.swing.Timer t = new javax.swing.Timer(500, (java.awt.event.ActionEvent e) -> { 440 log.debug("Updating slots account received opcode 0x8a message"); // NOI18N 441 update(slotMap,slotScanInterval); 442 }); 443 t.stop(); 444 t.setInitialDelay(500); 445 t.setRepeats(false); 446 t.start(); 447 } 448 return; 449 } 450 451 // LACK processing for resend of immediate command 452 if (!mTurnoutNoRetry && immedPacket != null && 453 m.getOpCode() == LnConstants.OPC_LONG_ACK && 454 m.getElement(1) == 0x6D && m.getElement(2) == 0x00) { 455 // LACK reject, resend immediately 456 tc.sendLocoNetMessage(immedPacket); 457 immedPacket = null; 458 } 459 if (m.getOpCode() == LnConstants.OPC_IMM_PACKET && 460 m.getElement(1) == 0x0B && m.getElement(2) == 0x7F) { 461 immedPacket = m; 462 } else { 463 immedPacket = null; 464 } 465 466 // slot specific message? 467 int i = findSlotFromMessage(m); 468 if (i != -1) { 469 getMoreDetailsForSlot(m, i); 470 checkSpecialSlots(m, i); 471 forwardMessageToSlot(m, i); 472 respondToAddrRequest(m, i); 473 programmerOpMessage(m, i); 474 checkLoconetProtocol(m,i); 475 } 476 477 // LONG_ACK response? 478 if (m.getOpCode() == LnConstants.OPC_LONG_ACK) { 479 handleLongAck(m); 480 } 481 482 // see if extended function message 483 if (isExtFunctionMessage(m)) { 484 // yes, get address 485 int addr = getDirectFunctionAddress(m); 486 // find slot(s) containing this address 487 // and route message to them 488 boolean found = false; 489 for (int j = 0; j < 120; j++) { 490 LocoNetSlot slot = slot(j); 491 if (slot == null) { 492 continue; 493 } 494 if ((slot.locoAddr() != addr) 495 || (slot.slotStatus() == LnConstants.LOCO_FREE)) { 496 continue; 497 } 498 // found! 499 slot.functionMessage(getDirectDccPacket(m)); 500 found = true; 501 } 502 if (!found) { 503 // rats! Slot not loaded since program start. Request it be 504 // reloaded for later, but that'll be too late 505 // for this one. 506 LocoNetMessage mo = new LocoNetMessage(4); 507 mo.setOpCode(LnConstants.OPC_LOCO_ADR); // OPC_LOCO_ADR 508 mo.setElement(1, (addr / 128) & 0x7F); 509 mo.setElement(2, addr & 0x7F); 510 tc.sendLocoNetMessage(mo); 511 } 512 } 513 } 514 515 /* 516 * Collect data from specific slots 517 */ 518 void checkSpecialSlots(LocoNetMessage m, int slot) { 519 if (!pmManagerGotReply && slot == 0 && 520 (m.getOpCode() == LnConstants.OPC_EXP_RD_SL_DATA || m.getOpCode() == LnConstants.OPC_SL_RD_DATA)) { 521 pmManagerGotReply = true; 522 if (supportsSlot250) { 523 pollSpecialSlots(); 524 } 525 return; 526 } 527 528 if (m.getElement(1) != 0x15) { 529 // cannot check short slot messages. 530 return; 531 } 532 533 switch (slot) { 534 case 250: 535 // slot info if we have serial, the serial number in this slot 536 // does not indicate whether in booster or cs mode. 537 if (slot248CommandStationSerial == ((m.getElement(19) & 0x3F) * 128) + m.getElement(18)) { 538 slot250InUseSlots = (m.getElement(4) + ((m.getElement(5) & 0x03) * 128)); 539 slot250IdleSlots = (m.getElement(6) + ((m.getElement(7) & 0x03) * 128)); 540 slot250FreeSlots = (m.getElement(8) + ((m.getElement(9) & 0x03) * 128)); 541 } 542 break; 543 case 248: 544 // Base HW Information 545 // If a CS in CS mode then byte 19 bit 6 in on. else its in 546 // booster mode 547 // The device type is in byte 14 548 if ((m.getElement(19) & 0x40) == 0x40) { 549 slot248CommandStationSerial = ((m.getElement(19) & 0x3F) * 128) + m.getElement(18); 550 slot248CommandStationType = m.getElement(14); 551 } 552 break; 553 default: 554 } 555 } 556 557 /* 558 * If protocol not yet established use slot status for protocol support 559 * System slots , except zero, do not have this info 560 */ 561 void checkLoconetProtocol(LocoNetMessage m, int slot) { 562 // detect protocol if not yet set 563 if (getLoconetProtocol() == LnConstants.LOCONETPROTOCOL_UNKNOWN) { 564 if (_slots[slot].getSlotType() != SlotType.SYSTEM || slot == 0) { 565 if ((m.getOpCode() == LnConstants.OPC_EXP_RD_SL_DATA && m.getNumDataElements() == 21) || 566 (m.getOpCode() == LnConstants.OPC_SL_RD_DATA)) { 567 if ((m.getElement(7) & 0b01000000) == 0b01000000) { 568 log.info("Setting protocol Loconet 2"); 569 setLoconet2Supported(LnConstants.LOCONETPROTOCOL_TWO); 570 } else { 571 log.info("Setting protocol Loconet 1"); 572 setLoconet2Supported(LnConstants.LOCONETPROTOCOL_ONE); 573 } 574 } 575 } 576 } 577 } 578 579 /** 580 * Checks a LocoNet message to see if it encodes a DCC "direct function" packet. 581 * 582 * @param m a LocoNet Message 583 * @return the loco address if the LocoNet message encodes a "direct function" packet, 584 * else returns -1 585 */ 586 int getDirectFunctionAddress(LocoNetMessage m) { 587 if (m.getOpCode() != LnConstants.OPC_IMM_PACKET) { 588 return -1; 589 } 590 if (m.getElement(1) != 0x0B) { 591 return -1; 592 } 593 if (m.getElement(2) != 0x7F) { 594 return -1; 595 } 596 // Direct packet, check length 597 if ((m.getElement(3) & 0x70) < 0x20) { 598 return -1; 599 } 600 int addr = -1; 601 // check long address 602 if ((m.getElement(4) & 0x01) == 0) { //bit 7=0 short 603 addr = (m.getElement(5) & 0xFF); 604 if ((m.getElement(4) & 0x01) != 0) { 605 addr += 128; // and high bit 606 } 607 } else if ((m.getElement(5) & 0x40) == 0x40) { // bit 7 = 1 if bit 6 = 1 then long 608 addr = (m.getElement(5) & 0x3F) * 256 + (m.getElement(6) & 0xFF); 609 if ((m.getElement(4) & 0x02) != 0) { 610 addr += 128; // and high bit 611 } 612 } else { // accessory decoder or extended accessory decoder 613 addr = (m.getElement(5) & 0x3F); 614 } 615 return addr; 616 } 617 618 /** 619 * Extracts a DCC "direct packet" from a LocoNet message, if possible. 620 * <p> 621 * if this is a direct DCC packet, return as one long 622 * else return -1. Packet does not include address bytes. 623 * 624 * @param m a LocoNet message to be inspected 625 * @return an integer containing the bytes of the DCC packet, except the address bytes. 626 */ 627 int getDirectDccPacket(LocoNetMessage m) { 628 if (m.getOpCode() != LnConstants.OPC_IMM_PACKET) { 629 return -1; 630 } 631 if (m.getElement(1) != 0x0B) { 632 return -1; 633 } 634 if (m.getElement(2) != 0x7F) { 635 return -1; 636 } 637 // Direct packet, check length 638 if ((m.getElement(3) & 0x70) < 0x20) { 639 return -1; 640 } 641 int result = 0; 642 int n = (m.getElement(3) & 0xF0) / 16; 643 int start; 644 int high = m.getElement(4); 645 // check long or short address 646 if ((m.getElement(4) & 0x01) == 1 && (m.getElement(5) & 0x40) == 0x40 ) { //long address bit 7 im1 = 1 and bit6 im1 = 1 647 start = 7; 648 high = high >> 2; 649 n = n - 2; 650 } else { //short or accessory 651 start = 6; 652 high = high >> 1; 653 n = n - 1; 654 } 655 // get result 656 for (int i = 0; i < n; i++) { 657 result = result * 256 + (m.getElement(start + i) & 0x7F); 658 if ((high & 0x01) != 0) { 659 result += 128; 660 } 661 high = high >> 1; 662 } 663 return result; 664 } 665 666 /** 667 * Determines if a LocoNet message encodes a direct request to control 668 * DCC functions F9 thru F28 669 * 670 * @param m the LocoNet message to be evaluated 671 * @return true if the message is an external DCC packet request for F9-F28, 672 * else false. 673 */ 674 boolean isExtFunctionMessage(LocoNetMessage m) { 675 int pkt = getDirectDccPacket(m); 676 if (pkt < 0) { 677 return false; 678 } 679 // check F9-12 680 if ((pkt & 0xFFFFFF0) == 0xA0) { 681 return true; 682 } 683 // check F13-28 684 if ((pkt & 0xFFFFFE00) == 0xDE00) { 685 return true; 686 } 687 return false; 688 } 689 690 /** 691 * Extracts the LocoNet slot number from a LocoNet message, if possible. 692 * <p> 693 * Find the slot number that a message references 694 * <p> 695 * This routine only looks for explicit slot references; it does not, for example, 696 * identify a loco address in the message and then work thru the slots to find a 697 * slot which references that loco address. 698 * 699 * @param m LocoNet Message to be inspected 700 * @return an integer representing the slot number encoded in the LocoNet 701 * message, or -1 if the message does not contain a slot reference 702 */ 703 public int findSlotFromMessage(LocoNetMessage m) { 704 705 int i = -1; // find the slot index in the message and store here 706 707 // decode the specific message type and hence slot number 708 switch (m.getOpCode()) { 709 case LnConstants.OPC_WR_SL_DATA: 710 case LnConstants.OPC_SL_RD_DATA: 711 i = m.getElement(2); 712 break; 713 case LnConstants.OPC_EXP_SLOT_MOVE_RE_OPC_IB2_SPECIAL: 714 if ( m.getElement(1) == LnConstants.RE_IB2_SPECIAL_FUNCS_TOKEN) { 715 i = m.getElement(2); 716 break; 717 } 718 i = ( (m.getElement(1) & 0x03 ) *128) + m.getElement(2); 719 break; 720 case LnConstants.OPC_LOCO_DIRF: 721 case LnConstants.OPC_LOCO_SND: 722 case LnConstants.OPC_LOCO_SPD: 723 case LnConstants.OPC_SLOT_STAT1: 724 case LnConstants.OPC_LINK_SLOTS: 725 case LnConstants.OPC_UNLINK_SLOTS: 726 i = m.getElement(1); 727 break; 728 729 case LnConstants.OPC_MOVE_SLOTS: // No follow on for some moves 730 if (m.getElement(1) != 0) { 731 i = m.getElement(1); 732 return i; 733 } 734 break; 735 case LnConstants.OPC_EXP_SEND_FUNCTION_OR_SPEED_AND_DIR: 736 i = ( (m.getElement(1) & 0x03 ) *128) + m.getElement(2); 737 break; 738 case LnConstants.OPC_EXP_RD_SL_DATA: 739 case LnConstants.OPC_EXP_WR_SL_DATA: 740 //only certain lengths get passed to slot 741 if (m.getElement(1) == 21) { 742 i = ( (m.getElement(2) & 0x03 ) *128) + m.getElement(3); 743 } 744 return i; 745 default: 746 // nothing here for us 747 return i; 748 } 749 // break gets to here 750 return i; 751 } 752 753 /** 754 * Check CV programming LONG_ACK message byte 1 755 * <p> 756 * The following methods are for parsing LACK as response to CV programming. 757 * It is divided into numerous small methods so that each bit can be 758 * overridden for special parsing for individual command station types. 759 * 760 * @param byte1 from the LocoNet message 761 * @return true if byte1 encodes a response to a OPC_SL_WRITE or an 762 * Expanded Slot Write 763 */ 764 protected boolean checkLackByte1(int byte1) { 765 if ((byte1 & 0xEF) == 0x6F) { 766 return true; 767 } else { 768 return false; 769 } 770 } 771 772 /** 773 * Checks the status byte of an OPC_LONG_ACK when performing CV programming 774 * operations. 775 * 776 * @param byte2 status byte 777 * @return True if status byte indicates acceptance of the command, else false. 778 */ 779 protected boolean checkLackTaskAccepted(int byte2) { 780 if (byte2 == 1 // task accepted 781 || byte2 == 0x23 || byte2 == 0x2B || byte2 == 0x6B // added as DCS51 fix 782 // deliberately ignoring 0x7F varient, see okToIgnoreLack 783 ) { 784 return true; 785 } else { 786 return false; 787 } 788 } 789 790 /** 791 * Checks the OPC_LONG_ACK status byte response to a programming 792 * operation. 793 * 794 * @param byte2 from the OPC_LONG_ACK message 795 * @return true if the programmer returned "busy" else false 796 */ 797 protected boolean checkLackProgrammerBusy(int byte2) { 798 if (byte2 == 0) { 799 return true; 800 } else { 801 return false; 802 } 803 } 804 805 /** 806 * Checks the OPC_LONG_ACK status byte response to a programming 807 * operation to see if the programmer accepted the operation "blindly". 808 * 809 * @param byte2 from the OPC_LONG_ACK message 810 * @return true if the programmer indicated a "blind operation", else false 811 */ 812 protected boolean checkLackAcceptedBlind(int byte2) { 813 if (byte2 == 0x40) { 814 return true; 815 } else { 816 return false; 817 } 818 } 819 820 /** 821 * Some LACKs with specific OPC_LONG_ACK status byte values can just be ignored. 822 * 823 * @param byte2 from the OPC_LONG_ACK message 824 * @return true if this form of LACK can be ignored without a warning message 825 */ 826 protected boolean okToIgnoreLack(int byte2) { 827 if (byte2 == 0x7F ) { 828 return true; 829 } else { 830 return false; 831 } 832 } 833 834 private boolean acceptAnyLACK = false; 835 /** 836 * Indicate that the command station LONG_ACK response details can be ignored 837 * for this operation. Typically this is used when accessing Loconet-attached boards. 838 */ 839 public final void setAcceptAnyLACK() { 840 acceptAnyLACK = true; 841 } 842 843 /** 844 * Handles OPC_LONG_ACK replies to programming slot operations. 845 * 846 * LACK 0x6D00 which requests a retransmission is handled 847 * separately in the message(..) method. 848 * 849 * @param m LocoNet message being analyzed 850 */ 851 protected void handleLongAck(LocoNetMessage m) { 852 // handle if reply to slot. There's no slot number in the LACK, unfortunately. 853 // If this is a LACK to a Slot op, and progState is command pending, 854 // assume its for us... 855 log.debug("LACK in state {} message: {}", progState, m.toString()); // NOI18N 856 if (checkLackByte1(m.getElement(1)) && progState == 1) { 857 // in programming state 858 if (acceptAnyLACK) { 859 log.debug("accepted LACK {} via acceptAnyLACK", m.getElement(2)); 860 // Any form of LACK response from CS is accepted here. 861 // Loconet-attached decoders (LOCONETOPSBOARD) receive the program commands 862 // directly via loconet and respond as required without needing any CS action, 863 // making the details of the LACK response irrelevant. 864 if (_progRead || _progConfirm) { 865 // move to commandExecuting state 866 startShortTimer(); 867 progState = 2; 868 } else { 869 // move to not programming state 870 progState = 0; 871 stopTimer(); 872 // allow the target device time to execute then notify ProgListener 873 notifyProgListenerEndAfterDelay(); 874 } 875 acceptAnyLACK = false; // restore normal state for next operation 876 } 877 // check status byte 878 else if (checkLackTaskAccepted(m.getElement(2))) { // task accepted 879 // 'not implemented' (op on main) 880 // but BDL16 and other devices can eventually reply, so 881 // move to commandExecuting state 882 log.debug("checkLackTaskAccepted accepted, next state 2"); // NOI18N 883 if ((_progRead || _progConfirm) && mServiceMode) { 884 startLongTimer(); 885 } else { 886 startShortTimer(); 887 } 888 progState = 2; 889 } else if (checkLackProgrammerBusy(m.getElement(2))) { // task aborted as busy 890 // move to not programming state 891 progState = 0; 892 // notify user ProgListener 893 stopTimer(); 894 notifyProgListenerLack(jmri.ProgListener.ProgrammerBusy); 895 } else if (checkLackAcceptedBlind(m.getElement(2))) { // task accepted blind 896 if ((_progRead || _progConfirm) && !mServiceMode) { // incorrect Reserved OpSw setting can cause this response to OpsMode Read 897 // just treat it as a normal OpsMode Read response 898 // move to commandExecuting state 899 log.debug("LACK accepted (ignoring incorrect OpSw), next state 2"); // NOI18N 900 startShortTimer(); 901 progState = 2; 902 } else { 903 // move to not programming state 904 progState = 0; 905 stopTimer(); 906 // allow command station time to execute then notify ProgListener 907 notifyProgListenerEndAfterDelay(); 908 } 909 } else if (okToIgnoreLack(m.getElement(2))) { 910 // this form of LACK can be silently ignored 911 log.debug("Ignoring LACK with {}", m.getElement(2)); 912 } else { // not sure how to cope, so complain 913 log.warn("unexpected LACK reply code {}", m.getElement(2)); // NOI18N 914 // move to not programming state 915 progState = 0; 916 // notify user ProgListener 917 stopTimer(); 918 notifyProgListenerLack(jmri.ProgListener.UnknownError); 919 } 920 } 921 } 922 923 /** 924 * Internal method to notify ProgListener after a short delay that the operation is complete. 925 * The delay ensures that the target device has completed the operation prior to the notification. 926 */ 927 protected void notifyProgListenerEndAfterDelay() { 928 javax.swing.Timer timer = new javax.swing.Timer(postProgDelay, new java.awt.event.ActionListener() { 929 @Override 930 public void actionPerformed(java.awt.event.ActionEvent e) { 931 notifyProgListenerEnd(-1, 0); // no value (e.g. -1), no error status (e.g.0) 932 } 933 }); 934 timer.stop(); 935 timer.setInitialDelay(postProgDelay); 936 timer.setRepeats(false); 937 timer.start(); 938 } 939 940 /** 941 * Forward Slot-related LocoNet message to the slot. 942 * 943 * @param m a LocoNet message targeted at a slot 944 * @param i the slot number to which the LocoNet message is targeted. 945 */ 946 public void forwardMessageToSlot(LocoNetMessage m, int i) { 947 948 // if here, i holds the slot number, and we expect to be able to parse 949 // and have the slot handle the message 950 if (i >= _slots.length || i < 0) { 951 log.error("Received slot number {} is greater than array length {} Message was {}", // NOI18N 952 i, _slots.length, m.toString()); // NOI18N 953 return; // prevents array index out-of-bounds when referencing _slots[i] 954 } 955 956 if ( !validateSlotNumber(i)) { 957 log.warn("Received slot number {} is not in the slot map, have you defined the wrong cammand station type? Message was {}", 958 i, m.toString()); 959 } 960 961 try { 962 _slots[i].setSlot(m); 963 } catch (LocoNetException e) { 964 // must not have been interesting, or at least routed right 965 log.error("slot rejected LocoNetMessage {}", m); // NOI18N 966 return; 967 } catch (Exception e) { 968 log.error("Unexplained error _slots[{}].setSlot({})",i,m,e); 969 return; 970 } 971 // notify listeners that slot may have changed 972 notify(_slots[i]); 973 } 974 975 /** 976 * A sort of slot listener which handles loco address requests 977 * 978 * @param m a LocoNet message 979 * @param i the slot to which it is directed 980 */ 981 protected void respondToAddrRequest(LocoNetMessage m, int i) { 982 // is called any time a LocoNet message is received. Note that we do _NOT_ know why a given message happens! 983 984 // if this is OPC_SL_RD_DATA 985 if (m.getOpCode() == LnConstants.OPC_SL_RD_DATA || m.getOpCode() == LnConstants.OPC_EXP_RD_SL_DATA ) { 986 // yes, see if request exists 987 // note that the appropriate _slots[] entry has already been updated 988 // to reflect the content of the LocoNet message, so _slots[i] 989 // has the locomotive address of this request 990 int addr = _slots[i].locoAddr(); 991 log.debug("LOCO_ADR resp is slot {} for addr {}", i, addr); // NOI18N 992 SlotListener l = mLocoAddrHash.get(Integer.valueOf(addr)); 993 if (l != null) { 994 // only notify once per request 995 mLocoAddrHash.remove(Integer.valueOf(addr)); 996 // and send the notification 997 log.debug("notify listener"); // NOI18N 998 l.notifyChangedSlot(_slots[i]); 999 } else { 1000 log.debug("no request for addr {}", addr); // NOI18N 1001 } 1002 } 1003 } 1004 1005 /** 1006 * If it is a slot being sent COMMON, 1007 * after a delay, get the new status of the slot 1008 * If it is a true slot move, not dispatch or null 1009 * after a delay, get the new status of the from slot, which varies by CS. 1010 * the to slot should come in the reply. 1011 * @param m a LocoNet message 1012 * @param i the slot to which it is directed 1013 */ 1014 protected void getMoreDetailsForSlot(LocoNetMessage m, int i) { 1015 // is called any time a LocoNet message is received. 1016 // sets up delayed slot read to update our effected slots to match the CS 1017 if (m.getOpCode() == LnConstants.OPC_SLOT_STAT1 && 1018 ((m.getElement(2) & LnConstants.LOCOSTAT_MASK) == LnConstants.LOCO_COMMON ) ) { 1019 // Changing a slot to common. Depending on a CS and its OpSw, and throttle speed 1020 // it could have its status changed a number of ways. 1021 sendReadSlotDelayed(i,100); 1022 } else if (m.getOpCode() == LnConstants.OPC_EXP_SLOT_MOVE_RE_OPC_IB2_SPECIAL) { 1023 boolean isSettingStatus = ((m.getElement(3) & 0b01110000) == 0b01100000); 1024 if (isSettingStatus) { 1025 int stat = m.getElement(4); 1026 if ((stat & LnConstants.LOCOSTAT_MASK) == LnConstants.LOCO_COMMON) { 1027 sendReadSlotDelayed(i,100); 1028 } 1029 } 1030 boolean isUnconsisting = ((m.getElement(3) & 0b01110000) == 0b01010000); 1031 if (isUnconsisting) { 1032 // read lead slot 1033 sendReadSlotDelayed(slot(i).getLeadSlot(),100); 1034 } 1035 boolean isConsisting = ((m.getElement(3) & 0b01110000) == 0b01000000); 1036 if (isConsisting) { 1037 // read 2nd slot 1038 int slotTwo = ((m.getElement(3) & 0b00000011) * 128 )+ m.getElement(4); 1039 sendReadSlotDelayed(slotTwo,100); 1040 } 1041 } else if (m.getOpCode() == LnConstants.OPC_MOVE_SLOTS) { 1042 // if a true move get the new from slot status 1043 // the to slot status is sent in the reply, but not if dispatch or null 1044 // as those return slot info. 1045 int slotTwo; 1046 slotTwo = m.getElement(2); 1047 if (i != 0 && slotTwo != 0 && i != slotTwo) { 1048 sendReadSlotDelayed(i,100); 1049 } 1050 } else if (m.getOpCode() == LnConstants.OPC_LINK_SLOTS || 1051 m.getOpCode() == LnConstants.OPC_UNLINK_SLOTS ) { 1052 // unlink and link return first slot by not second (to or from) 1053 // the to slot status is sent in the reply 1054 int slotTwo; 1055 slotTwo = m.getElement(2); 1056 if (i != 0 && slotTwo != 0) { 1057 sendReadSlotDelayed(slotTwo,100); 1058 } 1059 } 1060 } 1061 1062 /** 1063 * Schedule a delayed slot read. 1064 * @param slotNo - the slot. 1065 * @param delay - delay in msecs. 1066 */ 1067 protected void sendReadSlotDelayed(int slotNo, long delay) { 1068 java.util.TimerTask meterTask = new java.util.TimerTask() { 1069 int slotNumber = slotNo; 1070 1071 @Override 1072 public void run() { 1073 try { 1074 sendReadSlot(slotNumber); 1075 } catch (Exception e) { 1076 log.error("Exception occurred sendReadSlotDelayed:", e); 1077 } 1078 } 1079 }; 1080 jmri.util.TimerUtil.schedule(meterTask, delay); 1081 } 1082 1083 /** 1084 * Handle LocoNet messages related to CV programming operations 1085 * 1086 * @param m a LocoNet message 1087 * @param i the slot toward which the message is destined 1088 */ 1089 protected void programmerOpMessage(LocoNetMessage m, int i) { 1090 1091 // start checking for programming operations in slot 124 1092 if (i == 124) { 1093 // here its an operation on the programmer slot 1094 log.debug("Prog Message {} for slot 124 in state {}", // NOI18N 1095 m.getOpCodeHex(), progState); // NOI18N 1096 switch (progState) { 1097 case 0: // notProgramming 1098 break; 1099 case 1: // commandPending: waiting for an (optional) LACK 1100 case 2: // commandExecuting 1101 // waiting for slot read, is it present? 1102 if (m.getOpCode() == LnConstants.OPC_SL_RD_DATA) { 1103 log.debug(" was OPC_SL_RD_DATA"); // NOI18N 1104 // yes, this is the end 1105 // move to not programming state 1106 stopTimer(); 1107 progState = 0; 1108 1109 // parse out value returned 1110 int value = -1; 1111 int status = 0; 1112 if (_progConfirm) { 1113 // read command, get value; check if OK 1114 value = _slots[i].cvval(); 1115 if (value != _confirmVal) { 1116 status = status | jmri.ProgListener.ConfirmFailed; 1117 } 1118 } 1119 if (_progRead) { 1120 // read command, get value 1121 value = _slots[i].cvval(); 1122 } 1123 // parse out status 1124 if ((_slots[i].pcmd() & LnConstants.PSTAT_NO_DECODER) != 0) { 1125 status = (status | jmri.ProgListener.NoLocoDetected); 1126 } 1127 if ((_slots[i].pcmd() & LnConstants.PSTAT_WRITE_FAIL) != 0) { 1128 status = (status | jmri.ProgListener.NoAck); 1129 } 1130 if ((_slots[i].pcmd() & LnConstants.PSTAT_READ_FAIL) != 0) { 1131 status = (status | jmri.ProgListener.NoAck); 1132 } 1133 if ((_slots[i].pcmd() & LnConstants.PSTAT_USER_ABORTED) != 0) { 1134 status = (status | jmri.ProgListener.UserAborted); 1135 } 1136 1137 // and send the notification 1138 notifyProgListenerEnd(value, status); 1139 } 1140 break; 1141 default: // error! 1142 log.error("unexpected programming state {}", progState); // NOI18N 1143 break; 1144 } 1145 } 1146 } 1147 1148 ProgrammingMode csOpSwProgrammingMode = new ProgrammingMode( 1149 "LOCONETCSOPSWMODE", 1150 Bundle.getMessage("LOCONETCSOPSWMODE")); 1151 1152 // members for handling the programmer interface 1153 1154 /** 1155 * Return a list of ProgrammingModes supported by this interface 1156 * Types implemented here. 1157 * 1158 * @return a List of ProgrammingMode objects containing the supported 1159 * programming modes. 1160 */ 1161 1162 @Override 1163 @Nonnull 1164 public List<ProgrammingMode> getSupportedModes() { 1165 List<ProgrammingMode> ret = new ArrayList<>(); 1166 ret.add(ProgrammingMode.DIRECTBYTEMODE); 1167 ret.add(ProgrammingMode.PAGEMODE); 1168 ret.add(ProgrammingMode.REGISTERMODE); 1169 ret.add(ProgrammingMode.ADDRESSMODE); 1170 ret.add(csOpSwProgrammingMode); 1171 1172 return ret; 1173 } 1174 1175 /** 1176 * Remember whether the attached command station needs a sequence sent after 1177 * programming. The default operation is implemented in doEndOfProgramming 1178 * and turns power back on by sending a GPON message. 1179 */ 1180 private boolean mProgEndSequence = false; 1181 1182 /** 1183 * Remember whether the attached command station can read from Decoders. 1184 */ 1185 private boolean mCanRead = true; 1186 1187 /** 1188 * Determine whether this Programmer implementation is capable of reading 1189 * decoder contents. This is entirely determined by the attached command 1190 * station, not the code here, so it refers to the mCanRead member variable 1191 * which is recording the known state of that. 1192 * 1193 * @return True if reads are possible 1194 */ 1195 @Override 1196 public boolean getCanRead() { 1197 return mCanRead; 1198 } 1199 1200 /** 1201 * Return the write confirm mode implemented by the command station. 1202 * <p> 1203 * Service mode always checks for DecoderReply. (The DCS240 also seems to do 1204 * ReadAfterWrite, but that's not fully understood yet) 1205 * 1206 * @param addr This implementation ignores this parameter 1207 * @return the supported WriteConfirmMode 1208 */ 1209 @Nonnull 1210 @Override 1211 public Programmer.WriteConfirmMode getWriteConfirmMode(String addr) { return WriteConfirmMode.DecoderReply; } 1212 1213 /** 1214 * Set the command station type to one of the known types in the 1215 * {@link LnCommandStationType} enum. 1216 * 1217 * @param value contains the command station type 1218 */ 1219 public void setCommandStationType(LnCommandStationType value) { 1220 commandStationType = value; 1221 mCanRead = value.getCanRead(); 1222 mProgEndSequence = value.getProgPowersOff(); 1223 slotMap = commandStationType.getSlotMap(); 1224 supportsSlot250 = value.getSupportsSlot250(); 1225 1226 loadSlots(false); 1227 1228 // We will scan the slot table every 0.3 s for in-use slots that are stale 1229 final int slotScanDelay = 300; // Must be short enough that 128 can be scanned in 90 seconds, see checkStaleSlots() 1230 staleSlotCheckTimer = new javax.swing.Timer(slotScanDelay, new java.awt.event.ActionListener() { 1231 @Override 1232 public void actionPerformed(java.awt.event.ActionEvent e) { 1233 checkStaleSlots(); 1234 } 1235 }); 1236 1237 staleSlotCheckTimer.setRepeats(true); 1238 staleSlotCheckTimer.setInitialDelay(30000); // wait a bit at startup 1239 staleSlotCheckTimer.start(); 1240 1241 } 1242 1243 LocoNetThrottledTransmitter throttledTransmitter = null; 1244 boolean mTurnoutNoRetry = false; 1245 1246 /** 1247 * Provide a ThrottledTransmitter for sending immediate packets. 1248 * 1249 * @param value contains a LocoNetThrottledTransmitter object 1250 * @param m contains a boolean value indicating mTurnoutNoRetry 1251 */ 1252 public void setThrottledTransmitter(LocoNetThrottledTransmitter value, boolean m) { 1253 throttledTransmitter = value; 1254 mTurnoutNoRetry = m; 1255 } 1256 1257 /** 1258 * Get the command station type. 1259 * 1260 * @return an LnCommandStationType object 1261 */ 1262 public LnCommandStationType getCommandStationType() { 1263 return commandStationType; 1264 } 1265 1266 protected LnCommandStationType commandStationType = null; 1267 1268 /** 1269 * Internal routine to handle a timeout. 1270 */ 1271 @Override 1272 synchronized protected void timeout() { 1273 log.debug("timeout fires in state {}", progState); // NOI18N 1274 1275 if (progState != 0) { 1276 // we're programming, time to stop 1277 log.debug("timeout while programming"); // NOI18N 1278 1279 // perhaps no communications present? Fail back to end of programming 1280 progState = 0; 1281 // and send the notification; error code depends on state 1282 if (progState == 2 && !mServiceMode) { // ops mode command executing, 1283 // so did talk to command station at first 1284 notifyProgListenerEnd(_slots[124].cvval(), jmri.ProgListener.NoAck); 1285 } else { 1286 // all others 1287 notifyProgListenerEnd(_slots[124].cvval(), jmri.ProgListener.FailedTimeout); 1288 // might be leaving power off, but that's currently up to user to fix 1289 } 1290 acceptAnyLACK = false; // ensure cleared if timed out without getting a LACK 1291 } 1292 } 1293 1294 int progState = 0; 1295 // 1 is commandPending 1296 // 2 is commandExecuting 1297 // 0 is notProgramming 1298 boolean _progRead = false; 1299 boolean _progConfirm = false; 1300 int _confirmVal; 1301 boolean mServiceMode = true; 1302 1303 /** 1304 * Write a CV via Ops Mode programming. 1305 * 1306 * @param CVname CV number 1307 * @param val value to write to the CV 1308 * @param p programmer 1309 * @param addr address of decoder 1310 * @param longAddr true if the address is a long address 1311 * @throws jmri.ProgrammerException if an unsupported programming mode is exercised 1312 */ 1313 public void writeCVOpsMode(String CVname, int val, jmri.ProgListener p, 1314 int addr, boolean longAddr) throws jmri.ProgrammerException { 1315 final int CV = Integer.parseInt(CVname); 1316 lopsa = addr & 0x7f; 1317 hopsa = (addr / 128) & 0x7f; 1318 mServiceMode = false; 1319 doWrite(CV, val, p, 0x67); // ops mode byte write, with feedback 1320 } 1321 1322 /** 1323 * Write a CV via the Service Mode programmer. 1324 * 1325 * @param cvNum CV id as String 1326 * @param val value to write to the CV 1327 * @param p programmer 1328 * @throws jmri.ProgrammerException if an unsupported programming mode is exercised 1329 */ 1330 @Override 1331 public void writeCV(String cvNum, int val, jmri.ProgListener p) throws jmri.ProgrammerException { 1332 log.debug("writeCV(string): cvNum={}, value={}", cvNum, val); 1333 if (getMode().equals(csOpSwProgrammingMode)) { 1334 log.debug("cvOpSw mode write!"); 1335 // handle Command Station OpSw programming here 1336 String[] parts = cvNum.split("\\."); 1337 if ((parts[0].equals("csOpSw")) && (parts.length==2)) { 1338 if (csOpSwAccessor == null) { 1339 csOpSwAccessor = new CsOpSwAccess(adaptermemo, p); 1340 } else { 1341 csOpSwAccessor.setProgrammerListener(p); 1342 } 1343 // perform the CsOpSwMode read access 1344 log.debug("going to try the opsw access"); 1345 csOpSwAccessor.writeCsOpSw(cvNum, val, p); 1346 return; 1347 1348 } else { 1349 log.warn("rejecting the cs opsw access account unsupported CV name format"); 1350 // unsupported format in "cv" name. Signal an error 1351 notifyProgListenerEnd(p, 1, ProgListener.SequenceError); 1352 return; 1353 1354 } 1355 } else { 1356 // regular CV case 1357 int CV = Integer.parseInt(cvNum); 1358 1359 lopsa = 0; 1360 hopsa = 0; 1361 mServiceMode = true; 1362 // parse the programming command 1363 int pcmd = 0x43; // LPE implies 0x40, but 0x43 is observed 1364 if (getMode().equals(ProgrammingMode.PAGEMODE)) { 1365 pcmd = pcmd | 0x20; 1366 } else if (getMode().equals(ProgrammingMode.DIRECTBYTEMODE)) { 1367 pcmd = pcmd | 0x28; 1368 } else if (getMode().equals(ProgrammingMode.REGISTERMODE) 1369 || getMode().equals(ProgrammingMode.ADDRESSMODE)) { 1370 pcmd = pcmd | 0x10; 1371 } else { 1372 throw new jmri.ProgrammerException("mode not supported"); // NOI18N 1373 } 1374 1375 doWrite(CV, val, p, pcmd); 1376 } 1377 } 1378 1379 /** 1380 * Perform a write a CV via the Service Mode programmer. 1381 * 1382 * @param CV CV number 1383 * @param val value to write to the CV 1384 * @param p programmer 1385 * @param pcmd programming command 1386 * @throws jmri.ProgrammerException if an unsupported programming mode is exercised 1387 */ 1388 public void doWrite(int CV, int val, jmri.ProgListener p, int pcmd) throws jmri.ProgrammerException { 1389 log.debug("writeCV: {}", CV); // NOI18N 1390 1391 stopEndOfProgrammingTimer(); // still programming, so no longer waiting for power off 1392 1393 useProgrammer(p); 1394 _progRead = false; 1395 _progConfirm = false; 1396 // set commandPending state 1397 progState = 1; 1398 1399 // format and send message 1400 startShortTimer(); 1401 tc.sendLocoNetMessage(progTaskStart(pcmd, val, CV, true)); 1402 } 1403 1404 /** 1405 * Confirm a CV via the OpsMode programmer. 1406 * 1407 * @param CVname a String containing the CV name 1408 * @param val expected value 1409 * @param p programmer 1410 * @param addr address of loco to write to 1411 * @param longAddr true if addr is a long address 1412 * @throws jmri.ProgrammerException if an unsupported programming mode is exercised 1413 */ 1414 public void confirmCVOpsMode(String CVname, int val, jmri.ProgListener p, 1415 int addr, boolean longAddr) throws jmri.ProgrammerException { 1416 int CV = Integer.parseInt(CVname); 1417 lopsa = addr & 0x7f; 1418 hopsa = (addr / 128) & 0x7f; 1419 mServiceMode = false; 1420 doConfirm(CV, val, p, 0x2F); // although LPE implies 0x2C, 0x2F is observed 1421 } 1422 1423 /** 1424 * Confirm a CV via the Service Mode programmer. 1425 * 1426 * @param CVname a String containing the CV name 1427 * @param val expected value 1428 * @param p programmer 1429 * @throws jmri.ProgrammerException if an unsupported programming mode is exercised 1430 */ 1431 @Override 1432 public void confirmCV(String CVname, int val, jmri.ProgListener p) throws jmri.ProgrammerException { 1433 int CV = Integer.parseInt(CVname); 1434 lopsa = 0; 1435 hopsa = 0; 1436 mServiceMode = true; 1437 if (getMode().equals(csOpSwProgrammingMode)) { 1438 log.debug("cvOpSw mode!"); 1439 //handle Command Station OpSw programming here 1440 String[] parts = CVname.split("\\."); 1441 if ((parts[0].equals("csOpSw")) && (parts.length==2)) { 1442 if (csOpSwAccessor == null) { 1443 csOpSwAccessor = new CsOpSwAccess(adaptermemo, p); 1444 } else { 1445 csOpSwAccessor.setProgrammerListener(p); 1446 } 1447 // perform the CsOpSwMode read access 1448 log.debug("going to try the opsw access"); 1449 csOpSwAccessor.readCsOpSw(CVname, p); 1450 return; 1451 } else { 1452 log.warn("rejecting the cs opsw access account unsupported CV name format"); 1453 // unsupported format in "cv" name. Signal an error. 1454 notifyProgListenerEnd(p, 1, ProgListener.SequenceError); 1455 return; 1456 } 1457 } 1458 1459 // parse the programming command 1460 int pcmd = 0x03; // LPE implies 0x00, but 0x03 is observed 1461 if (getMode().equals(ProgrammingMode.PAGEMODE)) { 1462 pcmd = pcmd | 0x20; 1463 } else if (getMode().equals(ProgrammingMode.DIRECTBYTEMODE)) { 1464 pcmd = pcmd | 0x28; 1465 } else if (getMode().equals(ProgrammingMode.REGISTERMODE) 1466 || getMode().equals(ProgrammingMode.ADDRESSMODE)) { 1467 pcmd = pcmd | 0x10; 1468 } else { 1469 throw new jmri.ProgrammerException("mode not supported"); // NOI18N 1470 } 1471 1472 doConfirm(CV, val, p, pcmd); 1473 } 1474 1475 /** 1476 * Perform a confirm operation of a CV via the Service Mode programmer. 1477 * 1478 * @param CV the CV number 1479 * @param val expected value 1480 * @param p programmer 1481 * @param pcmd programming command 1482 * @throws jmri.ProgrammerException if an unsupported programming mode is exercised 1483 */ 1484 public void doConfirm(int CV, int val, ProgListener p, 1485 int pcmd) throws jmri.ProgrammerException { 1486 1487 log.debug("confirmCV: {}, val: {}", CV, val); // NOI18N 1488 1489 stopEndOfProgrammingTimer(); // still programming, so no longer waiting for power off 1490 1491 useProgrammer(p); 1492 _progRead = false; 1493 _progConfirm = true; 1494 _confirmVal = val; 1495 1496 // set commandPending state 1497 progState = 1; 1498 1499 // format and send message 1500 startShortTimer(); 1501 tc.sendLocoNetMessage(progTaskStart(pcmd, val, CV, false)); 1502 } 1503 1504 int hopsa; // high address for CV read/write 1505 int lopsa; // low address for CV read/write 1506 1507 CsOpSwAccess csOpSwAccessor; 1508 1509 @Override 1510 public void readCV(String cvNum, jmri.ProgListener p) throws jmri.ProgrammerException { 1511 readCV(cvNum, p, 0); 1512 } 1513 1514 /** 1515 * Read a CV via the OpsMode programmer. 1516 * 1517 * @param cvNum a String containing the CV number 1518 * @param p programmer 1519 * @param startVal initial "guess" for value of CV, can improve speed if used 1520 * @throws jmri.ProgrammerException if an unsupported programming mode is exercised 1521 */ 1522 @Override 1523 public void readCV(String cvNum, jmri.ProgListener p, int startVal) throws jmri.ProgrammerException { 1524 log.debug("readCV(string): cvNum={}, startVal={}, mode={}", cvNum, startVal, getMode()); 1525 if (getMode().equals(csOpSwProgrammingMode)) { 1526 log.debug("cvOpSw mode!"); 1527 //handle Command Station OpSw programming here 1528 String[] parts = cvNum.split("\\."); 1529 if ((parts[0].equals("csOpSw")) && (parts.length==2)) { 1530 if (csOpSwAccessor == null) { 1531 csOpSwAccessor = new CsOpSwAccess(adaptermemo, p); 1532 } else { 1533 csOpSwAccessor.setProgrammerListener(p); 1534 } 1535 // perform the CsOpSwMode read access 1536 log.debug("going to try the opsw access"); 1537 csOpSwAccessor.readCsOpSw(cvNum, p); 1538 return; 1539 1540 } else { 1541 log.warn("rejecting the cs opsw access account unsupported CV name format"); 1542 // unsupported format in "cv" name. Signal an error. 1543 notifyProgListenerEnd(p, 1, ProgListener.SequenceError); 1544 return; 1545 1546 } 1547 } else { 1548 // regular integer address for DCC form 1549 int CV = Integer.parseInt(cvNum); 1550 1551 lopsa = 0; 1552 hopsa = 0; 1553 mServiceMode = true; 1554 // parse the programming command 1555 int pcmd = 0x03; // LPE implies 0x00, but 0x03 is observed 1556 if (getMode().equals(ProgrammingMode.PAGEMODE)) { 1557 pcmd = pcmd | 0x20; 1558 } else if (getMode().equals(ProgrammingMode.DIRECTBYTEMODE)) { 1559 pcmd = pcmd | 0x28; 1560 } else if (getMode().equals(ProgrammingMode.REGISTERMODE) 1561 || getMode().equals(ProgrammingMode.ADDRESSMODE)) { 1562 pcmd = pcmd | 0x10; 1563 } else { 1564 throw new jmri.ProgrammerException("mode not supported"); // NOI18N 1565 } 1566 1567 doRead(CV, p, pcmd, startVal); 1568 1569 } 1570 } 1571 1572 /** 1573 * Invoked by LnOpsModeProgrammer to start an ops-mode read operation. 1574 * 1575 * @param CVname Which CV to read 1576 * @param p Who to notify on complete 1577 * @param addr Address of the locomotive 1578 * @param longAddr true if a long address, false if short address 1579 * @throws jmri.ProgrammerException if an unsupported programming mode is exercised 1580 */ 1581 public void readCVOpsMode(String CVname, jmri.ProgListener p, int addr, boolean longAddr) throws jmri.ProgrammerException { 1582 final int CV = Integer.parseInt(CVname); 1583 lopsa = addr & 0x7f; 1584 hopsa = (addr / 128) & 0x7f; 1585 mServiceMode = false; 1586 doRead(CV, p, 0x2F, 0); // although LPE implies 0x2C, 0x2F is observed 1587 } 1588 1589 /** 1590 * Perform a CV Read. 1591 * 1592 * @param CV the CV number 1593 * @param p programmer 1594 * @param progByte programming command 1595 * @param startVal initial "guess" for value of CV, can improve speed if used 1596 * @throws jmri.ProgrammerException if an unsupported programming mode is exercised 1597 */ 1598 void doRead(int CV, jmri.ProgListener p, int progByte, int startVal) throws jmri.ProgrammerException { 1599 1600 log.debug("readCV: {} with startVal: {}", CV, startVal); // NOI18N 1601 1602 stopEndOfProgrammingTimer(); // still programming, so no longer waiting for power off 1603 1604 useProgrammer(p); 1605 _progRead = true; 1606 _progConfirm = false; 1607 // set commandPending state 1608 progState = 1; 1609 1610 // format and send message 1611 startShortTimer(); 1612// tc.sendLocoNetMessage(progTaskStart(progByte, 0, CV, false)); 1613 tc.sendLocoNetMessage(progTaskStart(progByte, startVal, CV, false)); 1614 } 1615 1616 private jmri.ProgListener _usingProgrammer = null; 1617 1618 // internal method to remember who's using the programmer 1619 protected void useProgrammer(jmri.ProgListener p) throws jmri.ProgrammerException { 1620 // test for only one! 1621 if (_usingProgrammer != null && _usingProgrammer != p) { 1622 1623 log.info("programmer already in use by {}", _usingProgrammer); // NOI18N 1624 1625 throw new jmri.ProgrammerException("programmer in use"); // NOI18N 1626 } else { 1627 _usingProgrammer = p; 1628 return; 1629 } 1630 } 1631 1632 /** 1633 * Internal method to create the LocoNetMessage for programmer task start. 1634 * 1635 * @param pcmd programmer command 1636 * @param val value to be used 1637 * @param cvnum CV number 1638 * @param write true if write, else false 1639 * @return a LocoNet message containing a programming task start operation 1640 */ 1641 protected LocoNetMessage progTaskStart(int pcmd, int val, int cvnum, boolean write) { 1642 1643 int addr = cvnum - 1; // cvnum is in human readable form; addr is what's sent over LocoNet 1644 1645 LocoNetMessage m = new LocoNetMessage(14); 1646 1647 m.setOpCode(LnConstants.OPC_WR_SL_DATA); 1648 m.setElement(1, 0x0E); 1649 m.setElement(2, LnConstants.PRG_SLOT); 1650 1651 m.setElement(3, pcmd); 1652 1653 // set zero, then HOPSA, LOPSA, TRK 1654 m.setElement(4, 0); 1655 m.setElement(5, hopsa); 1656 m.setElement(6, lopsa); 1657 m.setElement(7, 0); // TRK was 0, then 7 for PR2, now back to zero 1658 1659 // store address in CVH, CVL. Note CVH format is truely wierd... 1660 m.setElement(8, ((addr & 0x300)>>4) | ((addr & 0x80) >> 7) | ((val & 0x80) >> 6)); 1661 m.setElement(9, addr & 0x7F); 1662 1663 // store low bits of CV value 1664 m.setElement(10, val & 0x7F); 1665 1666 // throttle ID 1667 m.setElement(11, 0x7F); 1668 m.setElement(12, 0x7F); 1669 return m; 1670 } 1671 1672 /** 1673 * Internal method to notify of the final result. 1674 * 1675 * @param value The cv value to be returned 1676 * @param status The error code, if any 1677 */ 1678 protected void notifyProgListenerEnd(int value, int status) { 1679 log.debug(" notifyProgListenerEnd with {}, {} and _usingProgrammer = {}", value, status, _usingProgrammer); // NOI18N 1680 // (re)start power timer 1681 restartEndOfProgrammingTimer(); 1682 // and send the reply 1683 ProgListener p = _usingProgrammer; 1684 _usingProgrammer = null; 1685 if (p != null) { 1686 sendProgrammingReply(p, value, status); 1687 } 1688 } 1689 1690 /** 1691 * Internal method to notify of the LACK result. This is a separate routine 1692 * from nPLRead in case we need to handle something later. 1693 * 1694 * @param status The error code, if any 1695 */ 1696 protected void notifyProgListenerLack(int status) { 1697 // (re)start power timer 1698 restartEndOfProgrammingTimer(); 1699 // and send the reply 1700 sendProgrammingReply(_usingProgrammer, -1, status); 1701 _usingProgrammer = null; 1702 } 1703 1704 /** 1705 * Internal routine to forward a programming reply. This is delayed to 1706 * prevent overruns of the command station. 1707 * 1708 * @param p a ProgListener object 1709 * @param value the value to return 1710 * @param status The error code, if any 1711 */ 1712 protected void sendProgrammingReply(ProgListener p, int value, int status) { 1713 int delay = serviceModeReplyDelay; // value in service mode 1714 if (!mServiceMode) { 1715 delay = opsModeReplyDelay; // value in ops mode 1716 } 1717 1718 // delay and run on GUI thread 1719 javax.swing.Timer timer = new javax.swing.Timer(delay, new java.awt.event.ActionListener() { 1720 @Override 1721 public void actionPerformed(java.awt.event.ActionEvent e) { 1722 notifyProgListenerEnd(p, value, status); 1723 } 1724 }); 1725 timer.setInitialDelay(delay); 1726 timer.setRepeats(false); 1727 timer.start(); 1728 } 1729 1730 /** 1731 * Internal routine to stop end-of-programming timer, as another programming 1732 * operation has happened. 1733 */ 1734 protected void stopEndOfProgrammingTimer() { 1735 if (mPowerTimer != null) { 1736 mPowerTimer.stop(); 1737 } 1738 } 1739 1740 /** 1741 * Internal routine to handle timer restart if needed to restore power. This 1742 * is only needed in service mode. 1743 */ 1744 protected void restartEndOfProgrammingTimer() { 1745 final int delay = 10000; 1746 if (mProgEndSequence) { 1747 if (mPowerTimer == null) { 1748 mPowerTimer = new javax.swing.Timer(delay, new java.awt.event.ActionListener() { 1749 @Override 1750 public void actionPerformed(java.awt.event.ActionEvent e) { 1751 doEndOfProgramming(); 1752 } 1753 }); 1754 } 1755 mPowerTimer.stop(); 1756 mPowerTimer.setInitialDelay(delay); 1757 mPowerTimer.setRepeats(false); 1758 mPowerTimer.start(); 1759 } 1760 } 1761 1762 /** 1763 * Internal routine to handle a programming timeout by turning power off. 1764 */ 1765 synchronized protected void doEndOfProgramming() { 1766 if (progState == 0) { 1767 if ( mServiceMode ) { 1768 // finished service-track programming, time to power on 1769 log.debug("end service-mode programming: turn power on"); // NOI18N 1770 try { 1771 jmri.InstanceManager.getDefault(jmri.PowerManager.class).setPower(jmri.PowerManager.ON); 1772 } catch (jmri.JmriException e) { 1773 log.error("exception during power on at end of programming", e); // NOI18N 1774 } 1775 } else { 1776 log.debug("end ops-mode programming: no power change"); // NOI18N 1777 } 1778 } 1779 } 1780 1781 javax.swing.Timer mPowerTimer = null; 1782 1783 ReadAllSlots_Helper _rAS = null; 1784 1785 /** 1786 * Start the process of checking each slot for contents. 1787 * <p> 1788 * This is not invoked by this class, but can be invoked from elsewhere to 1789 * start the process of scanning all slots to update their contents. 1790 * 1791 * If an instance is already running then the request is ignored 1792 * 1793 * @param inputSlotMap array of from to pairs 1794 * @param interval ms between slt rds 1795 */ 1796 public synchronized void update(List<SlotMapEntry> inputSlotMap, int interval) { 1797 if (_rAS == null) { 1798 _rAS = new ReadAllSlots_Helper( inputSlotMap, interval); 1799 jmri.util.ThreadingUtil.newThread(_rAS, getUserName() + READ_ALL_SLOTS_THREADNAME).start(); 1800 } else { 1801 if (!_rAS.isRunning()) { 1802 jmri.util.ThreadingUtil.newThread(_rAS, getUserName() + READ_ALL_SLOTS_THREADNAME).start(); 1803 } 1804 } 1805 } 1806 1807 /** 1808 * String with name for Read all slots thread. 1809 * Requires getUserName prepending. 1810 */ 1811 public static final String READ_ALL_SLOTS_THREADNAME = " Read All Slots "; 1812 1813 /** 1814 * Checks slotNum valid for slot map 1815 * 1816 * @param slotNum the slot number 1817 * @return true if it is 1818 */ 1819 private boolean validateSlotNumber(int slotNum) { 1820 for (SlotMapEntry item : slotMap) { 1821 if (slotNum >= item.getFrom() && slotNum <= item.getTo()) { 1822 return true; 1823 } 1824 } 1825 return false; 1826 } 1827 1828 public void update() { 1829 update(slotMap, slotScanInterval); 1830 } 1831 1832 /** 1833 * Send a message requesting the data from a particular slot. 1834 * 1835 * @param slot Slot number 1836 */ 1837 public void sendReadSlot(int slot) { 1838 LocoNetMessage m = new LocoNetMessage(4); 1839 m.setOpCode(LnConstants.OPC_RQ_SL_DATA); 1840 m.setElement(1, slot & 0x7F); 1841 // one is always short 1842 // THis gets a little akward, slots 121 thru 127 incl. seem to always old slots. 1843 // All slots gt 127 are always expanded format. 1844 if ( slot > 127 || ( ( slot > 0 && slot < 121 ) && loconetProtocol == LnConstants.LOCONETPROTOCOL_TWO ) ) { 1845 m.setElement(2, (slot / 128 ) & 0b00000111 | 0x40 ); 1846 } 1847 tc.sendLocoNetMessage(m); 1848 } 1849 1850 protected int nextReadSlot = 0; 1851 1852 /** 1853 * Continue the sequence of reading all slots. 1854 * @param toSlot index of the next slot to read 1855 * @param interval wait time before operation, milliseconds 1856 */ 1857 synchronized protected void readNextSlot(int toSlot, int interval) { 1858 // send info request 1859 sendReadSlot(nextReadSlot++); 1860 1861 // schedule next read if needed 1862 if (nextReadSlot < toSlot) { 1863 javax.swing.Timer t = new javax.swing.Timer(interval, new java.awt.event.ActionListener() { 1864 @Override 1865 public void actionPerformed(java.awt.event.ActionEvent e) { 1866 readNextSlot(toSlot,interval); 1867 } 1868 }); 1869 t.setRepeats(false); 1870 t.start(); 1871 } 1872 } 1873 1874 /** 1875 * Provide a snapshot of the slots in use. 1876 * <p> 1877 * Note that the count of "in-use" slots may be somewhat misleading, 1878 * as slots in the "common" state can be controlled and are occupying 1879 * a slot in a meaningful way. 1880 * 1881 * @return the count of in-use LocoNet slots 1882 */ 1883 public int getInUseCount() { 1884 int result = 0; 1885 for (int i = 0; i <= 120; i++) { 1886 if (slot(i).slotStatus() == LnConstants.LOCO_IN_USE) { 1887 result++; 1888 } 1889 } 1890 return result; 1891 } 1892 1893 /** 1894 * Set the system connection memo. 1895 * 1896 * @param memo a LocoNetSystemConnectionMemo 1897 */ 1898 public void setSystemConnectionMemo(LocoNetSystemConnectionMemo memo) { 1899 adaptermemo = memo; 1900 } 1901 1902 LocoNetSystemConnectionMemo adaptermemo; 1903 1904 /** 1905 * Get the "user name" for the slot manager connection, from the memo. 1906 * 1907 * @return the connection's user name or "LocoNet" if the memo 1908 * does not exist 1909 */ 1910 @Override 1911 public String getUserName() { 1912 if (adaptermemo == null) { 1913 return "LocoNet"; // NOI18N 1914 } 1915 return adaptermemo.getUserName(); 1916 } 1917 1918 /** 1919 * Return the memo "system prefix". 1920 * 1921 * @return the system prefix or "L" if the memo 1922 * does not exist 1923 */ 1924 @Override 1925 public String getSystemPrefix() { 1926 if (adaptermemo == null) { 1927 return "L"; 1928 } 1929 return adaptermemo.getSystemPrefix(); 1930 } 1931 1932 boolean transpondingAvailable = false; 1933 public void setTranspondingAvailable(boolean val) { transpondingAvailable = val; } 1934 public boolean getTranspondingAvailable() { return transpondingAvailable; } 1935 1936 /** 1937 * 1938 * @param val If false then we only use protocol one. 1939 */ 1940 public void setLoconetProtocolAutoDetect(boolean val) { 1941 if (!val) { 1942 loconetProtocol = LnConstants.LOCONETPROTOCOL_ONE; 1943 // slots would have been created with unknown for auto detect 1944 for( int ix = 0; ix < 128; ix++ ) { 1945 slot(ix).setProtocol(loconetProtocol); 1946 } 1947 } 1948 } 1949 1950 /** 1951 * Get the memo. 1952 * 1953 * @return the memo 1954 */ 1955 public LocoNetSystemConnectionMemo getSystemConnectionMemo() { 1956 return adaptermemo; 1957 } 1958 1959 /** 1960 * Dispose of this by stopped it's ongoing actions 1961 */ 1962 @Override 1963 public void dispose() { 1964 if (staleSlotCheckTimer != null) { 1965 staleSlotCheckTimer.stop(); 1966 } 1967 if ( _rAS != null ) { 1968 _rAS.setAbort(); 1969 } 1970 } 1971 1972 // initialize logging 1973 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SlotManager.class); 1974 1975 // Read all slots 1976 class ReadAllSlots_Helper implements Runnable { 1977 1978 ReadAllSlots_Helper(List<SlotMapEntry> inputSlotMap, int interval) { 1979 this.interval = interval; 1980 } 1981 1982 private int interval; 1983 private boolean abort = false; 1984 private boolean isRunning = false; 1985 1986 /** 1987 * Aborts current run 1988 */ 1989 public void setAbort() { 1990 abort = true; 1991 } 1992 1993 /** 1994 * Gets the current stae of the run. 1995 * @return true if running 1996 */ 1997 public boolean isRunning() { 1998 return isRunning; 1999 } 2000 2001 @Override 2002 public void run() { 2003 abort = false; 2004 isRunning = true; 2005 // read all slots that are not of unknown type 2006 for (int slot = 0; slot < getNumSlots() && !abort; slot++) { 2007 if (_slots[slot].getSlotType() != SlotType.UNKNOWN) { 2008 sendReadSlot(slot); 2009 try { 2010 Thread.sleep(this.interval); 2011 } catch (Exception ex) { 2012 // just abort 2013 abort = true; 2014 break; 2015 } 2016 } 2017 } 2018 isRunning = false; 2019 } 2020 } 2021 2022}