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