001package jmri.jmrix.nce; 002 003import java.util.ArrayList; 004 005import org.slf4j.Logger; 006import org.slf4j.LoggerFactory; 007 008import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 009import jmri.Consist; 010import jmri.ConsistListener; 011import jmri.DccLocoAddress; 012import jmri.implementation.DccConsist; 013 014/** 015 * The Consist definition for a consist on an NCE system. It uses the NCE 016 * specific commands to build a consist. 017 * 018 * @author Paul Bender Copyright (C) 2011 019 * @author Daniel Boudreau Copyright (C) 2012 020 * @author Ken Cameron Copyright (C) 2023 021 */ 022public class NceConsist extends jmri.implementation.DccConsist implements jmri.jmrix.nce.NceListener { 023 024 public static final int CONSIST_MIN = 1; // NCE doesn't use consist 0 025 public static final int CONSIST_MAX = 127; 026 private NceTrafficController tc = null; 027 private boolean _valid = false; 028 029 // state machine stuff 030 private int _busy = 0; 031 private int _replyLen = 0; // expected byte length 032 private static final int REPLY_1 = 1; // reply length of 16 bytes expected 033 private byte _consistNum = 0; // consist number (short address of consist) 034 035 // Initialize a consist for the specific address 036 // the Default consist type is an advanced consist 037 public NceConsist(int address, NceSystemConnectionMemo m) { 038 super(address); 039 tc = m.getNceTrafficController(); 040 loadConsist(address); 041 } 042 043 // Initialize a consist for the specific address 044 // the Default consist type is an advanced consist 045 public NceConsist(DccLocoAddress locoAddress, NceSystemConnectionMemo m) { 046 super(locoAddress); 047 tc = m.getNceTrafficController(); 048 loadConsist(locoAddress.getNumber()); 049 } 050 051 // Clean Up local storage 052 @Override 053 public void dispose() { 054 if(consistList == null) { 055 // already disposed; 056 return; 057 } 058 if (consistList.size() > 0) { 059 // kill this consist 060 DccLocoAddress locoAddress = consistList.get(0); 061 killConsist(locoAddress.getNumber(), locoAddress.isLongAddress()); 062 } 063 stopReadNCEconsistThread(); 064 super.dispose(); 065 consistList = null; 066 } 067 068 // Set the Consist Type 069 @Override 070 public void setConsistType(int consist_type) { 071 if (consist_type == Consist.ADVANCED_CONSIST) { 072 consistType = consist_type; 073 } else { 074 log.error("Consist Type Not Supported"); 075 notifyConsistListeners(new DccLocoAddress(0, false), ConsistListener.NotImplemented); 076 } 077 } 078 079 /* is there a size limit for this consist? 080 */ 081 @Override 082 public int sizeLimit() { 083 return 6; 084 } 085 086 /** 087 * Add a Locomotive to a Consist 088 * 089 * @param locoAddress is the Locomotive address to add to the consist 090 * @param directionNormal is True if the locomotive is traveling the same 091 * direction as the consist, or false otherwise. 092 */ 093 @Override 094 public synchronized void add(DccLocoAddress locoAddress, boolean directionNormal) { 095 if (!contains(locoAddress)) { 096 // NCE has 6 commands for adding a loco to a consist, lead, rear, and mid, plus direction 097 // First loco to consist? 098 if (consistList.size() == 0) { 099 // add lead loco 100 byte command = NceMessage.LOCO_CMD_FWD_CONSIST_LEAD; 101 if (!directionNormal) { 102 command = NceMessage.LOCO_CMD_REV_CONSIST_LEAD; 103 } 104 addLocoToConsist(locoAddress.getNumber(), locoAddress.isLongAddress(), command); 105 consistPosition.put(locoAddress, DccConsist.POSITION_LEAD); 106 } // Second loco to consist? 107 else if (consistList.size() == 1) { 108 // add rear loco 109 byte command = NceMessage.LOCO_CMD_FWD_CONSIST_REAR; 110 if (!directionNormal) { 111 command = NceMessage.LOCO_CMD_REV_CONSIST_REAR; 112 } 113 addLocoToConsist(locoAddress.getNumber(), locoAddress.isLongAddress(), command); 114 consistPosition.put(locoAddress, DccConsist.POSITION_TRAIL); 115 } else { 116 // add mid loco 117 byte command = NceMessage.LOCO_CMD_FWD_CONSIST_MID; 118 if (!directionNormal) { 119 command = NceMessage.LOCO_CMD_REV_CONSIST_MID; 120 } 121 addLocoToConsist(locoAddress.getNumber(), locoAddress.isLongAddress(), command); 122 consistPosition.put(locoAddress, consistPosition.size()); 123 } 124 // add loco to lists 125 consistList.add(locoAddress); 126 consistDir.put(locoAddress, Boolean.valueOf(directionNormal)); 127 } else { 128 log.error("Loco {} is already part of this consist {}", locoAddress, getConsistAddress()); 129 } 130 131 } 132 133 public void restore(DccLocoAddress locoAddress, boolean directionNormal, int position) { 134 consistPosition.put(locoAddress, position); 135 super.restore(locoAddress, directionNormal); 136 //notifyConsistListeners(locoAddress, ConsistListener.OPERATION_SUCCESS); 137 } 138 139 /** 140 * Remove a locomotive from this consist 141 * 142 * @param locoAddress is the locomotive address to remove from this consist 143 */ 144 @Override 145 public synchronized void remove(DccLocoAddress locoAddress) { 146 if (contains(locoAddress)) { 147 // can not delete the lead or rear loco from a NCE consist 148 int position = getPosition(locoAddress); 149 if (position == DccConsist.POSITION_LEAD || position == DccConsist.POSITION_TRAIL) { 150 log.info("Can not delete lead or rear loco from a NCE consist!"); 151 notifyConsistListeners(locoAddress, ConsistListener.DELETE_ERROR); 152 return; 153 } 154 // send remove loco from consist to NCE command station 155 removeLocoFromConsist(locoAddress.getNumber(), locoAddress.isLongAddress()); 156 //reset the value in the roster entry for CV19 157 resetRosterEntryCVValue(locoAddress); 158 159 // remove from lists 160 consistRoster.remove(locoAddress); 161 consistPosition.remove(locoAddress); 162 consistDir.remove(locoAddress); 163 consistList.remove(locoAddress); 164 notifyConsistListeners(locoAddress, ConsistListener.OPERATION_SUCCESS); 165 } else { 166 log.error("Loco {} is not part of this consist {}", locoAddress, getConsistAddress()); 167 } 168 } 169 170 private void loadConsist(int consistNum) { 171 if (consistNum > CONSIST_MAX || consistNum < CONSIST_MIN) { 172 log.error("Requesting consist {} out of range", consistNum); 173 return; 174 } 175 _consistNum = (byte) consistNum; 176 startReadNCEconsistThread(false); 177 } 178 179 public void checkConsist() { 180 if (!isValid()) { 181 return; // already checking the consist 182 } 183 setValid(false); 184 startReadNCEconsistThread(true); 185 } 186 187 private NceReadConsist mb = null; 188 189 private synchronized void startReadNCEconsistThread(boolean check) { 190 // read command station memory to get the current consist (can't be a USB, only PH) 191 if (tc.getUsbSystem() == NceTrafficController.USB_SYSTEM_NONE) { 192 mb = new NceReadConsist(); 193 mb.setName("Read Consist " + _consistNum); 194 mb.setConsist(_consistNum); 195 mb.setCheck(check); 196 mb.start(); 197 } 198 } 199 200 private synchronized void stopReadNCEconsistThread() { 201 if (mb != null) { 202 try { 203 mb.interrupt(); 204 mb.join(); 205 } catch (InterruptedException ex) { 206 log.warn("stopReadNCEconsistThread interrupted"); 207 } catch (Throwable t) { 208 log.error("stopReadNCEconsistThread caught ", t); 209 throw t; 210 } finally { 211 mb = null; 212 } 213 } 214 } 215 216 public DccLocoAddress getLocoAddressByPosition(int position) { 217 DccLocoAddress locoAddress; 218 ArrayList<DccLocoAddress> list = getConsistList(); 219 for (int i = 0; i < list.size(); i++) { 220 locoAddress = list.get(i); 221 if (getPosition(locoAddress) == position) { 222 return locoAddress; 223 } 224 } 225 return null; 226 } 227 228 /** 229 * Used to determine if consist has been initialized properly. 230 * 231 * @return true if command station memory has been read for this consist 232 * number. 233 */ 234 public boolean isValid() { 235 return _valid; 236 } 237 238 private void setValid(boolean valid) { 239 _valid = valid; 240 } 241 242 /** 243 * Adds a loco to the consist 244 * 245 * @param address The address of the loco to be added 246 * @param command There are six NCE commands to add a loco to a consist. Add 247 * Lead, Rear, Mid, and the loco direction 3x2 = 6 commands. 248 */ 249 private void addLocoToConsist(int address, boolean isLong, byte command) { 250 if (isLong) { 251 address += 0xC000; // set the upper two bits for long addresses 252 } 253 sendNceBinaryCommand(address, command, _consistNum); 254 } 255 256 /** 257 * Remove a loco from any consist. The consist number is not supplied to 258 * NCE. 259 * 260 * @param address The address of the loco to be removed 261 * @param isLong true if long address 262 */ 263 private void removeLocoFromConsist(int address, boolean isLong) { 264 if (isLong) { 265 address += 0xC000; // set the upper two bits for long addresses 266 } 267 sendNceBinaryCommand(address, NceMessage.LOCO_CMD_DELETE_LOCO_CONSIST, (byte) 0); 268 } 269 270 /** 271 * Kills consist using lead loco address 272 * @param address loco address 273 * @param isLong true if long address 274 */ 275 void killConsist(int address, boolean isLong) { 276 if (isLong) { 277 address += 0xC000; // set the upper two bits for long addresses 278 } 279 sendNceBinaryCommand(address, NceMessage.LOCO_CMD_KILL_CONSIST, (byte) 0); 280 } 281 282 private void sendNceBinaryCommand(int nceAddress, byte nceLocoCmd, byte consistNumber) { 283 byte[] bl = NceBinaryCommand.nceLocoCmd(nceAddress, nceLocoCmd, consistNumber); 284 sendNceMessage(bl, REPLY_1); 285 } 286 287 private void sendNceMessage(byte[] b, int replyLength) { 288 NceMessage m = NceMessage.createBinaryMessage(tc, b, replyLength); 289 _busy++; 290 _replyLen = replyLength; // Expect n byte response 291 tc.sendNceMessage(m, this); 292 } 293 294 @Override 295 public void message(NceMessage m) { 296 // not used 297 } 298 299 @Override 300 public void reply(NceReply r) { 301 if (_busy == 0) { 302 log.debug("Consist {} read reply not for this consist", _consistNum); 303 return; 304 } 305 if (r.getNumDataElements() != _replyLen) { 306 log.error("reply length error, expecting: {} got: {}", _replyLen, r.getNumDataElements()); 307 return; 308 } 309 if (_replyLen == 1 && r.getElement(0) == NceMessage.NCE_OKAY) { 310 log.debug("Command complete okay for consist {}", getConsistAddress()); 311 } else { 312 log.error("Error, command failed for consist {}", getConsistAddress()); 313 } 314 } 315 316 public class NceReadConsist extends Thread implements jmri.jmrix.nce.NceListener { 317 318 // state machine stuff 319 private int _consistNum = 0; 320 private int _busy = 0; 321 private boolean _validConsist = false; // true when there's a lead and rear loco in the consist 322 private boolean _check = false; // when true update consist to match NCE CS 323 324 private int _replyLen = 0; // expected byte length 325 private static final int REPLY_16 = 16; // reply length of 16 bytes expected 326 327 private int _locoNum = LEAD; // which loco, 0 = lead, 1 = rear, 2 = mid 328 private static final int LEAD = 0; 329 private static final int REAR = 1; 330 private static final int MID = 2; 331 332 public void setConsist(int number) { 333 _consistNum = number; 334 } 335 336 public void setCheck(boolean check) { 337 _check = check; 338 } 339 340 // load up the consist lists by lead, rear, and then mid 341 @Override 342 public void run() { 343 try{ 344 readConsistMemory(_consistNum, LEAD); 345 readConsistMemory(_consistNum, REAR); 346 readConsistMemory(_consistNum, MID); 347 setValid(true); 348 } catch (InterruptedException e) { 349 return; // we're done! 350 } catch (Throwable t) { 351 if ( ! (t instanceof java.lang.ThreadDeath) ) { 352 log.error("NceReadConsist.run caught ", t); 353 } 354 throw t; 355 } 356 } 357 358 /** 359 * Reads 16 bytes of NCE consist memory based on consist number and loco 360 * number 0=lead 1=rear 2=mid 361 */ 362 private void readConsistMemory(int consistNum, int eNum) throws InterruptedException { // throw interrupt upward 363 if (consistNum > CONSIST_MAX || consistNum < CONSIST_MIN) { 364 log.error("Requesting consist {} out of range", consistNum); 365 return; 366 } 367 // if busy wait 368 if (!readWait()) { 369 log.error("Time out reading NCE command station consist memory"); 370 return; 371 } 372 _locoNum = eNum; 373 int nceMemAddr = (consistNum * 2) + tc.csm.getConsistHeadAddr(); 374 if (eNum == REAR) { 375 nceMemAddr = (consistNum * 2) + tc.csm.getConsistTailAddr(); 376 } 377 if (eNum == MID) { 378 nceMemAddr = (consistNum * 8) + tc.csm.getConsistMidAddr(); 379 } 380 if (eNum == LEAD || _validConsist) { 381 byte[] bl = NceBinaryCommand.accMemoryRead(nceMemAddr); 382 sendNceMessage(bl, REPLY_16); 383 } 384 } 385 386 private void sendNceMessage(byte[] b, int replyLength) { 387 NceMessage m = NceMessage.createBinaryMessage(tc, b, replyLength); 388 _busy++; 389 _replyLen = replyLength; // Expect n byte response 390 tc.sendNceMessage(m, this); 391 } 392 393 // wait up to 30 sec per read 394 private boolean readWait() throws InterruptedException { // throw interrupt upward 395 int waitcount = 30; 396 while (_busy > 0) { 397 synchronized (this) { 398 wait(1000); 399 } 400 if (waitcount-- < 0) { 401 log.error("read timeout"); 402 return false; 403 } 404 } 405 return true; 406 } 407 408 @Override 409 public void message(NceMessage m) { 410 // not used 411 } 412 413 @SuppressFBWarnings(value = "NN_NAKED_NOTIFY") // notify not naked 414 @Override 415 public void reply(NceReply r) { 416 if (_busy == 0) { 417 log.debug("Consist {} read reply not for this consist", _consistNum); 418 return; 419 } 420 log.debug("Consist {} read reply number {}", _consistNum, _locoNum); 421 if (r.getNumDataElements() != _replyLen) { 422 log.error("reply length error, expecting: {} got: {}", _replyLen, r.getNumDataElements()); 423 return; 424 } 425 426 // are we checking to see if the consist matches CS memory? 427 if (_check) { 428 log.debug("Checking {}", _consistNum); 429 if (_locoNum == LEAD) { 430 _validConsist = checkLocoConsist(r, 0, DccConsist.POSITION_LEAD); // consist is valid if there's at least a lead & rear loco 431 } 432 if (_validConsist && _locoNum == REAR) { 433 _validConsist = checkLocoConsist(r, 0, DccConsist.POSITION_TRAIL); 434 } 435 436 if (_validConsist && _locoNum == MID) { 437 for (int index = 0; index < 8; index = index + 2) { 438 checkLocoConsist(r, index, consistPosition.size()); 439 } 440 } 441 442 } else { 443 if (_locoNum == LEAD) { 444 _validConsist = addLocoConsist(r, 0, DccConsist.POSITION_LEAD); // consist is valid if there's at least a lead & rear loco 445 } 446 if (_validConsist && _locoNum == REAR) { 447 _validConsist = addLocoConsist(r, 0, DccConsist.POSITION_TRAIL); 448 } 449 450 if (_validConsist && _locoNum == MID) { 451 for (int index = 0; index < 8; index = index + 2) { 452 addLocoConsist(r, index, consistPosition.size()); 453 } 454 } 455 } 456 457 _busy--; 458 459 // wake up thread 460 synchronized (this) { 461 notify(); 462 } 463 } 464 465 /* 466 * Returns true if loco added to consist 467 */ 468 private boolean addLocoConsist(NceReply r, int index, int position) { 469 int address = getLocoAddrText(r, index); 470 boolean isLong = getLocoAddressType(r, index); // Long (true) or short (false) address? 471 if (address != 0) { 472 log.debug("Add loco address {} to consist {}", address, _consistNum); 473 restore(new DccLocoAddress(address, isLong), true, position); // we don't know the direction of the loco 474 return true; 475 } 476 return false; 477 } 478 479 private boolean checkLocoConsist(NceReply r, int index, int position) { 480 int address = getLocoAddrText(r, index); 481 boolean isLong = getLocoAddressType(r, index); // Long (true) or short (false) address? 482 DccLocoAddress locoAddress = new DccLocoAddress(address, isLong); 483 if (contains(locoAddress)) { 484 log.debug("Loco address {} found match for consist {}", locoAddress, _consistNum); 485 } else if (address != 0) { 486 log.debug("New loco address {} found for consist {}", locoAddress, _consistNum); 487 restore(locoAddress, true, position); // we don't know the direction of the loco 488 } else { 489 log.debug("Found loco address 0 for consist {} index {} position {}", _consistNum, index, position); 490 // remove loco by position in consist 491 locoAddress = getLocoAddressByPosition(position); 492 if (locoAddress != null) { 493 remove(locoAddress); 494 } 495 } 496 return true; 497 } 498 499 private int getLocoAddrText(NceReply r, int index) { 500 int rC = r.getElement(index++); 501 rC = (rC << 8) & 0x3F00; // Mask off upper two bits 502 int rC_l = r.getElement(index); 503 rC_l = rC_l & 0xFF; 504 rC = rC + rC_l; 505 return rC; 506 } 507 508 // get loco address type, returns true if long 509 private boolean getLocoAddressType(NceReply r, int index) { 510 int rC = r.getElement(index); 511 rC = rC & 0xC0; // long address if 2 msb are set 512 if (rC == 0xC0) { 513 return true; 514 } else { 515 return false; 516 } 517 } 518 } 519 520 private final static Logger log = LoggerFactory.getLogger(NceConsist.class); 521 522}