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