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