001package jmri.implementation; 002 003import java.io.IOException; 004import java.util.ArrayList; 005import java.util.HashMap; 006import java.util.List; 007import jmri.AddressedProgrammer; 008import jmri.AddressedProgrammerManager; 009import jmri.Consist; 010import jmri.ConsistListener; 011import jmri.jmrit.consisttool.ConsistPreferencesManager; 012import jmri.DccLocoAddress; 013import jmri.InstanceManager; 014import jmri.ProgListener; 015import jmri.ProgrammerException; 016import jmri.jmrit.decoderdefn.DecoderFile; 017import jmri.jmrit.decoderdefn.DecoderIndexFile; 018import jmri.jmrit.roster.Roster; 019import jmri.jmrit.roster.RosterEntry; 020import jmri.jmrit.symbolicprog.CvTableModel; 021import jmri.jmrit.symbolicprog.CvValue; 022import jmri.jmrit.symbolicprog.VariableTableModel; 023import org.jdom2.*; 024import org.slf4j.Logger; 025import org.slf4j.LoggerFactory; 026 027/** 028 * This is the Default DCC consist. It utilizes the fact that IF a Command 029 * Station supports OpsMode Programming, you can write the consist information 030 * to CV19, so ANY Command Station that supports Ops Mode Programming can write 031 * this address to a Command Station that supports it. 032 * 033 * @author Paul Bender Copyright (C) 2003-2008 034 */ 035public class DccConsist implements Consist, ProgListener { 036 037 protected ArrayList<DccLocoAddress> consistList = null; // A List of Addresses in the consist 038 protected HashMap<DccLocoAddress, Boolean> consistDir = null; // A Hash table 039 // containing the directions of 040 // each locomotive in the consist, 041 // keyed by Loco Address. 042 protected HashMap<DccLocoAddress, Integer> consistPosition = null; // A Hash table 043 // containing the position of 044 // each locomotive in the consist, 045 // keyed by Loco Address. 046 protected HashMap<DccLocoAddress, String> consistRoster = null; // A Hash table 047 // containing the Roster Identifier of 048 // each locomotive in the consist, 049 // keyed by Loco Address. 050 protected int consistType = ADVANCED_CONSIST; 051 protected DccLocoAddress consistAddress = null; 052 protected String consistID = null; 053 // data member to hold the throttle listener objects 054 private final ArrayList<ConsistListener> listeners; 055 056 057 private AddressedProgrammerManager opsProgManager = null; 058 059 // Initialize a consist for the specific address. 060 // In this implementation, we can safely assume the address is a 061 // short address, since Advanced Consisting is only possible with 062 // a short address. 063 // The Default consist type is an advanced consist 064 public DccConsist(int address) { 065 this(new DccLocoAddress(address, false)); 066 } 067 068 // Initialize a consist for a specific DccLocoAddress. 069 // The Default consist type is an advanced consist 070 public DccConsist(DccLocoAddress address) { 071 this(address,jmri.InstanceManager.getDefault(AddressedProgrammerManager.class)); 072 } 073 074 // Initialize a consist for a specific DccLocoAddress. 075 // The Default consist type is an advanced consist 076 public DccConsist(DccLocoAddress address,AddressedProgrammerManager apm) { 077 opsProgManager = apm; 078 this.listeners = new ArrayList<>(); 079 consistAddress = address; 080 consistDir = new HashMap<>(); 081 consistList = new ArrayList<>(); 082 consistPosition = new HashMap<>(); 083 consistRoster = new HashMap<>(); 084 consistID = consistAddress.toString(); 085 } 086 087 // Clean Up local Storage. 088 @Override 089 public void dispose() { 090 if (consistList == null) { 091 return; 092 } 093 for (int i = (consistList.size() - 1); i >= 0; i--) { 094 DccLocoAddress loco = consistList.get(i); 095 if (log.isDebugEnabled()) { 096 log.debug("Deleting Locomotive: {}",loco); 097 } 098 try { 099 remove(loco); 100 } catch (Exception ex) { 101 log.error("Error removing loco: {} from consist: {}", loco, consistAddress); 102 } 103 } 104 consistList = null; 105 consistDir = null; 106 consistPosition = null; 107 consistRoster = null; 108 } 109 110 // Set the Consist Type 111 @Override 112 public void setConsistType(int consist_type) { 113 if (consist_type == ADVANCED_CONSIST) { 114 consistType = consist_type; 115 } else { 116 notifyUnsupportedConsistType(); 117 } 118 } 119 120 private void notifyUnsupportedConsistType(){ 121 log.error("Consist Type Not Supported"); 122 notifyConsistListeners(new DccLocoAddress(0, false), ConsistListener.NotImplemented); 123 } 124 125 // get the Consist Type 126 @Override 127 public int getConsistType() { 128 return consistType; 129 } 130 131 // get the Consist Address 132 @Override 133 public DccLocoAddress getConsistAddress() { 134 return consistAddress; 135 } 136 137 /* is this address allowed? 138 * Since address 00 is an analog locomotive, we can't program CV19 139 * to include it in a consist, but all other addresses are ok. 140 */ 141 @Override 142 public boolean isAddressAllowed(DccLocoAddress address) { 143 if (address.getNumber() != 0) { 144 return (true); 145 } else { 146 return (false); 147 } 148 } 149 150 /* is there a size limit for this consist? 151 * For Decoder Assisted Consists, returns -1 (no limit) 152 * return 0 for any other consist type. 153 */ 154 @Override 155 public int sizeLimit() { 156 if (consistType == ADVANCED_CONSIST) { 157 return -1; 158 } else { 159 return 0; 160 } 161 } 162 163 // get a list of the locomotives in the consist 164 @Override 165 public ArrayList<DccLocoAddress> getConsistList() { 166 return consistList; 167 } 168 169 // does the consist contain the specified address? 170 @Override 171 public boolean contains(DccLocoAddress address) { 172 if (consistType == ADVANCED_CONSIST) { 173 return (consistList.contains(address)); 174 } else { 175 notifyUnsupportedConsistType(); 176 } 177 return false; 178 } 179 180 // get the relative direction setting for a specific 181 // locomotive in the consist 182 @Override 183 public boolean getLocoDirection(DccLocoAddress address) { 184 if (consistType == ADVANCED_CONSIST) { 185 Boolean Direction = consistDir.get(address); 186 return (Direction); 187 } else { 188 notifyUnsupportedConsistType(); 189 } 190 return false; 191 } 192 193 /* 194 * Add a Locomotive to an Advanced Consist 195 * @param address is the Locomotive address to add to the locomotive 196 * @param directionNormal is True if the locomotive is traveling 197 * the same direction as the consist, or false otherwise. 198 */ 199 @Override 200 public void add(DccLocoAddress LocoAddress, boolean directionNormal) { 201 if (consistType == ADVANCED_CONSIST) { 202 Boolean Direction = directionNormal; 203 if (!(consistList.contains(LocoAddress))) { 204 consistList.add(LocoAddress); 205 } 206 consistDir.put(LocoAddress, Direction); 207 addToAdvancedConsist(LocoAddress, directionNormal); 208 //set the value in the roster entry for CV19 209 setRosterEntryCVValue(LocoAddress); 210 } else { 211 notifyUnsupportedConsistType(); 212 } 213 } 214 215 /* 216 * Restore a Locomotive to an Advanced Consist, but don't write to 217 * the command station. This is used for restoring the consist 218 * from a file or adding a consist read from the command station. 219 * @param address is the Locomotive address to add to the locomotive 220 * @param directionNormal is True if the locomotive is traveling 221 * the same direction as the consist, or false otherwise. 222 */ 223 @Override 224 public void restore(DccLocoAddress LocoAddress, boolean directionNormal) { 225 if (consistType == ADVANCED_CONSIST) { 226 Boolean Direction = directionNormal; 227 if (!(consistList.contains(LocoAddress))) { 228 consistList.add(LocoAddress); 229 } 230 consistDir.put(LocoAddress, Direction); 231 } else { 232 notifyUnsupportedConsistType(); 233 } 234 } 235 236 /* 237 * Remove a Locomotive from this Consist 238 * @param address is the Locomotive address to add to the locomotive 239 */ 240 @Override 241 public void remove(DccLocoAddress LocoAddress) { 242 if (consistType == ADVANCED_CONSIST) { 243 //reset the value in the roster entry for CV19 244 resetRosterEntryCVValue(LocoAddress); 245 consistDir.remove(LocoAddress); 246 consistList.remove(LocoAddress); 247 consistPosition.remove(LocoAddress); 248 consistRoster.remove(LocoAddress); 249 removeFromAdvancedConsist(LocoAddress); 250 } else { 251 notifyUnsupportedConsistType(); 252 } 253 } 254 255 256 /* 257 * Add a Locomotive to an Advanced Consist 258 * @param address is the Locomotive address to add to the locomotive 259 * @param directionNormal is True if the locomotive is traveling 260 * the same direction as the consist, or false otherwise. 261 */ 262 protected void addToAdvancedConsist(DccLocoAddress LocoAddress, boolean directionNormal) { 263 AddressedProgrammer opsProg = opsProgManager 264 .getAddressedProgrammer(LocoAddress.isLongAddress(), 265 LocoAddress.getNumber()); 266 if (opsProg == null) { 267 log.error("Can't make consisting change because no programmer exists; this is probably a configuration error in the preferences"); 268 return; 269 } 270 271 if (directionNormal) { 272 try { 273 opsProg.writeCV("19", consistAddress.getNumber(), this); 274 } catch (ProgrammerException e) { 275 // Don't do anything with this yet 276 log.warn("Exception writing CV19 while adding from consist", e); 277 } 278 } else { 279 try { 280 opsProg.writeCV("19", consistAddress.getNumber() + 128, this); 281 } catch (ProgrammerException e) { 282 // Don't do anything with this yet 283 log.warn("Exception writing CV19 while adding to consist", e); 284 } 285 } 286 287 InstanceManager.getDefault(jmri.AddressedProgrammerManager.class) 288 .releaseAddressedProgrammer(opsProg); 289 } 290 291 /* 292 * Remove a Locomotive from an Advanced Consist 293 * @param address is the Locomotive address to remove from the consist 294 */ 295 protected void removeFromAdvancedConsist(DccLocoAddress LocoAddress) { 296 AddressedProgrammer opsProg = InstanceManager.getDefault(jmri.AddressedProgrammerManager.class) 297 .getAddressedProgrammer(LocoAddress.isLongAddress(), 298 LocoAddress.getNumber()); 299 if (opsProg == null) { 300 log.error("Can't make consisting change because no programmer exists; this is probably a configuration error in the preferences"); 301 return; 302 } 303 304 try { 305 opsProg.writeCV("19", 0, this); 306 } catch (ProgrammerException e) { 307 // Don't do anything with this yet 308 log.warn("Exception writing CV19 while removing from consist", e); 309 } 310 311 InstanceManager.getDefault(jmri.AddressedProgrammerManager.class) 312 .releaseAddressedProgrammer(opsProg); 313 } 314 315 /* 316 * Set the position of a locomotive within the consist 317 * @param address is the Locomotive address 318 * @param position is a constant representing the position within 319 * the consist. 320 */ 321 @Override 322 public void setPosition(DccLocoAddress address, int position) { 323 consistPosition.put(address, position); 324 } 325 326 /* 327 * Get the position of a locomotive within the consist 328 * @param address is the Locomotive address of interest 329 */ 330 @Override 331 public int getPosition(DccLocoAddress address) { 332 if (consistPosition.containsKey(address)) { 333 return (consistPosition.get(address)); 334 } 335 // if the consist order hasn't been set, we'll use default 336 // positioning based on index in the arraylist. Lead locomotive 337 // is position 0 in the list and the trail is the last locomtoive 338 // in the list. 339 int index = consistList.indexOf(address); 340 if (index == 0) { 341 return (Consist.POSITION_LEAD); 342 } else if (index == (consistList.size() - 1)) { 343 return (Consist.POSITION_TRAIL); 344 } else { 345 return index; 346 } 347 } 348 349 /** 350 * Set the roster entry of a locomotive within the consist 351 * 352 * @param address is the Locomotive address 353 * @param rosterId is the roster Identifer of the associated roster entry. 354 */ 355 @Override 356 public void setRosterId(DccLocoAddress address, String rosterId) { 357 consistRoster.put(address, rosterId); 358 if (consistType == ADVANCED_CONSIST) { 359 //set the value in the roster entry for CV19 360 setRosterEntryCVValue(address); 361 } 362 } 363 364 /** 365 * Get the rosterId of a locomotive within the consist 366 * 367 * @param address is the Locomotive address of interest 368 * @return string roster Identifier associated with the given address in the 369 * consist. Returns null if no roster entry is associated with this 370 * entry. 371 */ 372 @Override 373 public String getRosterId(DccLocoAddress address) { 374 if (consistRoster.containsKey(address)) { 375 return (consistRoster.get(address)); 376 } else { 377 return null; 378 } 379 } 380 381 /** 382 * Update the value in the roster entry for CV19 for the specified 383 * address 384 * 385 * @param address is the Locomotive address we are updating. 386 */ 387 protected void setRosterEntryCVValue(DccLocoAddress address){ 388 updateRosterCV(address,getLocoDirection(address),this.consistAddress.getNumber()); 389 } 390 391 /** 392 * Set the value in the roster entry's value for for CV19 to 0 393 * 394 * @param address is the Locomotive address we are updating. 395 */ 396 protected void resetRosterEntryCVValue(DccLocoAddress address){ 397 updateRosterCV(address,getLocoDirection(address),0); 398 } 399 400 /** 401 * If allowed by the preferences, Update the CV19 value in the 402 * specified address's roster entry, if the roster entry is known. 403 * 404 * @param address is the Locomotive address we are updating. 405 * @param direction the direction to set. 406 * @param value the numeric value of the consist address. 407 */ 408 protected void updateRosterCV(DccLocoAddress address,Boolean direction,int value){ 409 if(!InstanceManager.getDefault(ConsistPreferencesManager.class).isUpdateCV19()){ 410 log.trace("Consist Manager updates of CV19 are disabled in preferences"); 411 return; 412 } 413 if(getRosterId(address)==null){ 414 // roster entry unknown. 415 log.trace("No RosterID for address {} in consist {}. Skipping CV19 update.",address,consistAddress); 416 return; 417 } 418 RosterEntry entry = Roster.getDefault().getEntryForId(getRosterId(address)); 419 420 if(entry==null || entry.getFileName()==null || entry.getFileName().equals("")){ 421 // roster entry unknown. 422 log.trace("No file name available for RosterID {},address {}, in consist {}. Skipping CV19 update.",getRosterId(address),address,consistAddress); 423 return; 424 } 425 CvTableModel cvTable = new CvTableModel(null, null); // will hold CV objects 426 VariableTableModel varTable = new VariableTableModel(null, new String[]{"Name", "Value"}, cvTable); // NOI18N 427 entry.readFile(); // read, but don't yet process 428 429 // load from decoder file 430 loadDecoderFromLoco(entry,varTable); 431 432 entry.loadCvModel(varTable, cvTable); 433 CvValue cv19Value = cvTable.getCvByNumber("19"); 434 cv19Value.setValue((value & 0xff) | (direction.booleanValue()?0x00:0x80 )); 435 436 entry.writeFile(cvTable,varTable); 437 } 438 439 // copied from PaneProgFrame 440 protected void loadDecoderFromLoco(RosterEntry r,VariableTableModel varTable) { 441 // get a DecoderFile from the locomotive xml 442 String decoderModel = r.getDecoderModel(); 443 String decoderFamily = r.getDecoderFamily(); 444 if (log.isDebugEnabled()) { 445 log.debug("selected loco uses decoder {} {}",decoderFamily,decoderModel); 446 } 447 // locate a decoder like that. 448 List<DecoderFile> l = InstanceManager.getDefault(DecoderIndexFile.class).matchingDecoderList(null, decoderFamily, null, null, null, decoderModel); 449 if (log.isDebugEnabled()) { 450 log.debug("found {} matches",l.size()); 451 } 452 if (l.isEmpty()) { 453 log.debug("Loco uses {} {} decoder, but no such decoder defined",decoderFamily,decoderModel ); 454 // fall back to use just the decoder name, not family 455 l = InstanceManager.getDefault(DecoderIndexFile.class).matchingDecoderList(null, null, null, null, null, decoderModel); 456 if (log.isDebugEnabled()) { 457 log.debug("found {} matches without family key",l.size()); 458 } 459 } 460 if (!l.isEmpty()) { 461 DecoderFile d = l.get(0); 462 loadDecoderFile(d, r, varTable); 463 } else { 464 if (decoderModel.equals("")) { 465 log.debug("blank decoderModel requested, so nothing loaded"); 466 } else { 467 log.warn("no matching \"{}\" decoder found for loco, no decoder info loaded",decoderModel ); 468 } 469 } 470 } 471 472 protected void loadDecoderFile(DecoderFile df, RosterEntry re,VariableTableModel variableModel) { 473 if (df == null) { 474 log.warn("loadDecoder file invoked with null object"); 475 return; 476 } 477 if (log.isDebugEnabled()) { 478 log.debug("loadDecoderFile from {} {}", DecoderFile.fileLocation, df.getFileName()); 479 } 480 481 Element decoderRoot = null; 482 483 try { 484 decoderRoot = df.rootFromName(DecoderFile.fileLocation + df.getFileName()); 485 } catch (JDOMException | IOException e) { 486 log.error("Exception while loading decoder XML file: {}", df.getFileName(), e); 487 } 488 // load variables from decoder tree 489 df.getProductID(); 490 if(decoderRoot!=null) { 491 df.loadVariableModel(decoderRoot.getChild("decoder"), variableModel); 492 // load function names 493 re.loadFunctions(decoderRoot.getChild("decoder").getChild("family").getChild("functionlabels")); 494 } 495 } 496 497 /* 498 * Add a Listener for consist events 499 * @param Listener is a consistListener object 500 */ 501 @Override 502 public void addConsistListener(ConsistListener Listener) { 503 if (!listeners.contains(Listener)) { 504 listeners.add(Listener); 505 } 506 } 507 508 /* 509 * Remove a Listener for consist events 510 * @param Listener is a consistListener object 511 */ 512 @Override 513 public void removeConsistListener(ConsistListener Listener) { 514 if (listeners.contains(Listener)) { 515 listeners.remove(Listener); 516 } 517 } 518 519 // Get and set the 520 /* 521 * Set the text ID associated with the consist 522 * @param String is a string identifier for the consist 523 */ 524 @Override 525 public void setConsistID(String ID) { 526 consistID = ID; 527 } 528 529 /* 530 * Get the text ID associated with the consist 531 * @return String identifier for the consist 532 * default value is the string Identifier for the 533 * consist address. 534 */ 535 @Override 536 public String getConsistID() { 537 return consistID; 538 } 539 540 /* 541 * Reverse the order of locomotives in the consist and flip 542 * the direction bits of each locomotive. 543 */ 544 @Override 545 public void reverse() { 546 // save the old lead locomotive direction. 547 Boolean oldDir = consistDir.get(consistList.get(0)); 548 // reverse the direction of the list 549 java.util.Collections.reverse(consistList); 550 // and then save the new lead locomotive direction 551 Boolean newDir = consistDir.get(consistList.get(0)); 552 // and itterate through the list to reverse the directions of the 553 // individual elements of the list. 554 java.util.Iterator<DccLocoAddress> i = consistList.iterator(); 555 while (i.hasNext()) { 556 DccLocoAddress locoaddress = i.next(); 557 if (oldDir.equals(newDir)) { 558 add(locoaddress, getLocoDirection(locoaddress)); 559 } else { 560 add(locoaddress, !getLocoDirection(locoaddress)); 561 } 562 if (consistPosition.containsKey(locoaddress)) { 563 switch (getPosition(locoaddress)) { 564 case Consist.POSITION_LEAD: 565 setPosition(locoaddress, Consist.POSITION_TRAIL); 566 break; 567 case Consist.POSITION_TRAIL: 568 setPosition(locoaddress, Consist.POSITION_LEAD); 569 break; 570 default: 571 setPosition(locoaddress, consistList.size() - getPosition(locoaddress)); 572 break; 573 } 574 } 575 } 576 // notify any listeners that the consist changed 577 this.notifyConsistListeners(consistAddress, ConsistListener.OK); 578 } 579 580 /* 581 * Restore the consist to the command station. 582 */ 583 @Override 584 public void restore() { 585 // itterate through the list to re-add the addresses to the 586 // command station. 587 java.util.Iterator<DccLocoAddress> i = consistList.iterator(); 588 while (i.hasNext()) { 589 DccLocoAddress locoaddress = i.next(); 590 add(locoaddress, getLocoDirection(locoaddress)); 591 } 592 // notify any listeners that the consist changed 593 this.notifyConsistListeners(consistAddress, ConsistListener.OK); 594 } 595 596 /* 597 * Notify all listener objects of a status change. 598 * @param LocoAddress is the address of any specific locomotive the 599 * status refers to. 600 * @param ErrorCode is the status code to send to the 601 * consistListener objects 602 */ 603 @SuppressWarnings("unchecked") 604 protected void notifyConsistListeners(DccLocoAddress LocoAddress, int ErrorCode) { 605 // make a copy of the listener vector to notify. 606 ArrayList<ConsistListener> v; 607 synchronized (this) { 608 v = (ArrayList<ConsistListener>) listeners.clone(); 609 } 610 log.debug("Sending Status code: {} to {} listeners for Address {}", 611 ErrorCode, 612 v.size(), LocoAddress); 613 // forward to all listeners 614 v.forEach(client -> { 615 client.consistReply(LocoAddress, ErrorCode); 616 }); 617 } 618 619 // This class is to be registered as a programmer listener, so we 620 // include the programmingOpReply() function 621 @Override 622 public void programmingOpReply(int value, int status) { 623 log.debug("Programming Operation reply received, value is {}, status is {}", value, status); 624 notifyConsistListeners(new DccLocoAddress(0, false), ConsistListener.OPERATION_SUCCESS); 625 } 626 627 private static final Logger log = LoggerFactory.getLogger(DccConsist.class); 628 629}