001package jmri.jmrit.withrottle; 002 003// WiThrottle 004// 005// 006/** 007 * ThrottleController.java Sends commands to appropriate throttle component. 008 * <p> 009 * Original version sorting codes for received string from client: 'V'elocity 010 * followed by 0 - 126 'X'stop 'F'unction (1-button down, 0-button up) (0-28) 011 * e.g. F14 indicates function 4 button is pressed ` F04 indicates function 4 012 * button is released di'R'ection (0=reverse, 1=forward) 'L'ong address #, 013 * 'S'hort address # e.g. L1234 'r'elease, 'd'ispatch 'C'consist lead address, 014 * e.g. CL1235 'I'dle Idle needs to be called specifically 'Q'uit 015 * <p> 016 * Anything using added codes needs to verify version number for compatibility. 017 * Added in v1.7: 'E'ntry from roster, e.g. ESpiffy Loco 'c'consist lead from 018 * roster ID, e.g. cSpiffy Loco 019 * <p> 020 * Added in v2.0: If sent through MultiThrottle 'M' in DeviceServer, earlier 021 * versions will automatically ignore these. ('M' code did not exist prior to 022 * v2.0, so it will not forward to here) If sent through a 'T' or 'S', need to 023 * verify version number for compatibility. 'f' set a function directly. 024 * 's'peedStepMode - 1-128, 2-28, 4-27, 8-14 re'q'uest information, add the 025 * following: 'V' getSpeedSetting 'R' getIsForward 's' getSpeedStepMode 'm' 026 * getF#Momentary for all functions 027 * 028 * 029 * @author Brett Hoffman Copyright (C) 2009, 2010, 2011 030 * @author Created by Brett Hoffman on: 8/23/09. 031 */ 032import java.beans.PropertyChangeEvent; 033import java.beans.PropertyChangeListener; 034import java.util.ArrayList; 035import java.util.List; 036import java.util.LinkedList; 037import java.util.Queue; 038import jmri.DccLocoAddress; 039import jmri.DccThrottle; 040import jmri.InstanceManager; 041import jmri.LocoAddress; 042import jmri.SpeedStepMode; 043import jmri.ThrottleListener; 044import jmri.jmrit.roster.Roster; 045import jmri.jmrit.roster.RosterEntry; 046import org.slf4j.Logger; 047import org.slf4j.LoggerFactory; 048 049public class ThrottleController implements ThrottleListener, PropertyChangeListener { 050 051 DccThrottle throttle; 052 DccThrottle functionThrottle; 053 RosterEntry rosterLoco = null; 054 DccLocoAddress leadAddress; 055 char whichThrottle; 056 float speedMultiplier; 057 protected Queue<Float> lastSentSpeed; 058 protected float newSpeed; 059 boolean isAddressSet; 060 protected ArrayList<ThrottleControllerListener> listeners; 061 protected ArrayList<ControllerInterface> controllerListeners; 062 boolean useLeadLocoF; 063 ConsistFunctionController leadLocoF = null; 064 String locoKey = ""; 065 066 final boolean isMomF2 = InstanceManager.getDefault(WiThrottlePreferences.class).isUseMomF2(); 067 068 public ThrottleController() { 069 speedMultiplier = 1.0f / 126.0f; 070 lastSentSpeed = new LinkedList<Float>(); 071 } 072 073 public ThrottleController(char whichThrottleChar, ThrottleControllerListener tcl, ControllerInterface cl) { 074 this(); 075 setWhichThrottle(whichThrottleChar); 076 addThrottleControllerListener(tcl); 077 addControllerListener(cl); 078 } 079 080 public void setWhichThrottle(char c) { 081 whichThrottle = c; 082 } 083 084 public void addThrottleControllerListener(ThrottleControllerListener l) { 085 if (listeners == null) { 086 listeners = new ArrayList<>(1); 087 } 088 if (!listeners.contains(l)) { 089 listeners.add(l); 090 } 091 } 092 093 public void removeThrottleControllerListener(ThrottleControllerListener l) { 094 if (listeners == null) { 095 return; 096 } 097 if (listeners.contains(l)) { 098 listeners.remove(l); 099 } 100 } 101 102 /** 103 * Add a listener to handle: listener.sendPacketToDevice(message); 104 * 105 * @param listener handle of listener to add 106 * 107 */ 108 public void addControllerListener(ControllerInterface listener) { 109 if (controllerListeners == null) { 110 controllerListeners = new ArrayList<>(1); 111 } 112 if (!controllerListeners.contains(listener)) { 113 controllerListeners.add(listener); 114 } 115 } 116 117 public void removeControllerListener(ControllerInterface listener) { 118 if (controllerListeners == null) { 119 return; 120 } 121 if (controllerListeners.contains(listener)) { 122 controllerListeners.remove(listener); 123 } 124 } 125 126 /** 127 * Receive notification that an address has been released/dispatched 128 */ 129 public void addressRelease() { 130 isAddressSet = false; 131 jmri.InstanceManager.throttleManagerInstance().releaseThrottle(throttle, this); 132 throttle.removePropertyChangeListener(this); 133 throttle = null; 134 rosterLoco = null; 135 sendAddress(); 136 clearLeadLoco(); 137 for (int i = 0; i < listeners.size(); i++) { 138 ThrottleControllerListener l = listeners.get(i); 139 l.notifyControllerAddressReleased(this); 140 log.debug("Notify TCListener address released: {}", l.getClass()); 141 } 142 } 143 144 public void addressDispatch() { 145 isAddressSet = false; 146 jmri.InstanceManager.throttleManagerInstance().dispatchThrottle(throttle, this); 147 throttle.removePropertyChangeListener(this); 148 throttle = null; 149 rosterLoco = null; 150 sendAddress(); 151 clearLeadLoco(); 152 for (int i = 0; i < listeners.size(); i++) { 153 ThrottleControllerListener l = listeners.get(i); 154 l.notifyControllerAddressReleased(this); 155 log.debug("Notify TCListener address dispatched: {}", l.getClass()); 156 } 157 } 158 159 /** 160 * Receive notification that a DccThrottle has been found and is in use. 161 * 162 * @param t The throttle which has been found 163 */ 164 @Override 165 public void notifyThrottleFound(DccThrottle t) { 166 if (isAddressSet) { 167 log.debug("Throttle: {} is already set. (Found is: {})", getCurrentAddressString(), t.getLocoAddress()); 168 return; 169 } 170 if (t != null) { 171 throttle = t; 172 setFunctionThrottle(throttle); // adds Property Change Listener 173 isAddressSet = true; 174 log.debug("DccThrottle found for: {}", throttle.getLocoAddress()); 175 } else { 176 log.error("*throttle is null!*"); 177 return; 178 } 179 for (int i = 0; i < listeners.size(); i++) { 180 ThrottleControllerListener l = listeners.get(i); 181 l.notifyControllerAddressFound(this); 182 log.debug("Notify TCListener address found: {}", l.getClass()); 183 } 184 185 if (rosterLoco == null) { 186 rosterLoco = findRosterEntry(throttle); 187 } 188 189 syncThrottleFunctions(throttle, rosterLoco); 190 191 sendAddress(); 192 193 sendFunctionLabels(rosterLoco); 194 195 sendAllFunctionStates(throttle); 196 197 sendCurrentSpeed(throttle); 198 199 sendCurrentDirection(throttle); 200 201 sendSpeedStepMode(throttle); 202 203 } 204 205 @Override 206 public void notifyFailedThrottleRequest(LocoAddress address, String reason) { 207 log.warn("Throttle request failed for {} because {}.", address, reason); 208 if (!(address instanceof DccLocoAddress)){ 209 log.warn("Throttle address {} is not a DccLocoAddress", address); 210 return; 211 } 212 for (ThrottleControllerListener l : listeners) { 213 l.notifyControllerAddressDeclined(this, (DccLocoAddress) address, reason); 214 log.debug("Notify TCListener address declined in-use: {}", l.getClass()); 215 } 216 } 217 218 /** 219 * calls notifyFailedThrottleRequest, Steal Required 220 * <p> 221 * {@inheritDoc} 222 */ 223 @Override 224 public void notifyDecisionRequired(jmri.LocoAddress address, DecisionType question) { 225 notifyFailedThrottleRequest(address, "Steal Required"); 226 } 227 228 229 /* 230 * Current Format: RPF}|{whichThrottle]\[eventName}|{newValue 231 * This format may be used to send multiple function status, for initial values. 232 * 233 * Event may be from regular throttle or consist throttle, but is handled the same. 234 * 235 * Bound params: SpeedSteps, IsForward, SpeedSetting, F##, F##Momentary 236 */ 237 @Override 238 public void propertyChange(PropertyChangeEvent event) { 239 String eventName = event.getPropertyName(); 240 log.debug("property change: {}", eventName); 241 242 if (eventName.startsWith("F")) { 243 244 if (eventName.contains("Momentary")) { 245 return; 246 } 247 StringBuilder message = new StringBuilder("RPF}|{"); 248 message.append(whichThrottle); 249 message.append("]\\["); 250 message.append(eventName); 251 message.append("}|{"); 252 message.append(event.getNewValue()); 253 254 for (ControllerInterface listener : controllerListeners) { 255 listener.sendPacketToDevice(message.toString()); 256 } 257 } 258 259 } 260 261 public RosterEntry findRosterEntry(DccThrottle t) { 262 RosterEntry re = null; 263 if (t.getLocoAddress() != null) { 264 List<RosterEntry> l = Roster.getDefault().matchingList(null, null, "" + ((DccLocoAddress) t.getLocoAddress()).getNumber(), null, null, null, null); 265 if (l.size() > 0) { 266 log.debug("Roster Loco found: {}", l.get(0).getDccAddress()); 267 re = l.get(0); 268 } 269 } 270 return re; 271 } 272 273 public void syncThrottleFunctions(DccThrottle t, RosterEntry re) { 274 if (re != null) { 275 int highestCommon = Math.min(t.getFunctions().length, re.getMaxFnNumAsInt()+1); 276 for (int funcNum = 0; funcNum < highestCommon; funcNum++) { 277 t.setFunctionMomentary(funcNum, !(re.getFunctionLockable(funcNum))); 278 } 279 } 280 } 281 282 283 /** 284 * Send function labels for a roster entry, using old format. 285 * 286 * This implementation is legacy and should not change from the limit of 29 functions. 287 * 288 * @param re The roster entry to get the labels from. 289 */ 290 public void sendFunctionLabels(RosterEntry re) { 291 292 if (re != null) { 293 StringBuilder functionString = new StringBuilder(); 294 if (whichThrottle == 'S') { 295 functionString.append("RS29}|{"); 296 } else { 297 // I know, it should have been 'RT' but this was before there were two throttles. 298 functionString.append("RF29}|{"); 299 } 300 functionString.append(getCurrentAddressString()); 301 302 int i; 303 for (i = 0; i < 29; i++) { 304 functionString.append("]\\["); 305 if ((re.getFunctionLabel(i) != null)) { 306 functionString.append(re.getFunctionLabel(i)); 307 } 308 } 309 for (ControllerInterface listener : controllerListeners) { 310 listener.sendPacketToDevice(functionString.toString()); 311 } 312 } 313 314 } 315 316 /** 317 * send all function states, primarily for initial status Current Format: 318 * RPF}|{whichThrottle]\[function}|{state]\[function}|{state... 319 * 320 * This implementation is legacy and should not change from the limit of 29 functions. 321 * 322 * @param t throttle to send functions to 323 */ 324 public void sendAllFunctionStates(DccThrottle t) { 325 326 log.debug("Sending state of all functions"); 327 StringBuilder message = new StringBuilder(buildFStatesHeader()); 328 329 for (int cnt = 0; cnt < 29; cnt++) { 330 message.append("]\\[F"); 331 message.append(cnt); 332 message.append("}|{"); 333 message.append(t.getFunction(cnt) ); 334 } 335 336 for (ControllerInterface listener : controllerListeners) { 337 listener.sendPacketToDevice(message.toString()); 338 } 339 340 } 341 342 protected String buildFStatesHeader() { 343 return ("RPF}|{" + whichThrottle); 344 } 345 346 synchronized protected void sendCurrentSpeed(DccThrottle t) { 347 } 348 349 protected void sendCurrentDirection(DccThrottle t) { 350 } 351 352 protected void sendSpeedStepMode(DccThrottle t) { 353 } 354 355 protected void sendAllMomentaryStates(DccThrottle t) { 356 } 357 358 /** 359 * Figure out what the received command means, where it has to go, and 360 * translate to a jmri method. 361 * 362 * @param inPackage The package minus its prefix which steered it here. 363 * @return true to keep reading in run loop. 364 */ 365 public boolean sort(String inPackage) { 366 if (inPackage.charAt(0) == 'Q') {// If device has Quit. 367 shutdownThrottle(); 368 return false; 369 } 370 if (isAddressSet) { 371 372 try { 373 switch (inPackage.charAt(0)) { 374 case 'V': // Velocity 375 setSpeed(Integer.parseInt(inPackage.substring(1))); 376 377 break; 378 379 case 'X': 380 eStop(); 381 382 break; 383 384 case 'F': // Function 385 386 handleFunction(inPackage); 387 388 break; 389 390 case 'f': //v>=2.0 Force function 391 392 forceFunction(inPackage.substring(1)); 393 394 break; 395 396 case 'R': // Direction 397 setDirection(!inPackage.endsWith("0")); // 0 sets to reverse, all others forward 398 break; 399 400 case 'r': // Release 401 addressRelease(); 402 break; 403 404 case 'd': // Dispatch 405 addressDispatch(); 406 break; 407 408 case 'L': // Set a Long address. 409 addressRelease(); 410 int addr = Integer.parseInt(inPackage.substring(1)); 411 setAddress(addr, true); 412 break; 413 414 case 'S': // Set a Short address. 415 addressRelease(); 416 addr = Integer.parseInt(inPackage.substring(1)); 417 setAddress(addr, false); 418 break; 419 420 case 'E': //v>=1.7 Address from RosterEntry 421 addressRelease(); 422 requestEntryFromID(inPackage.substring(1)); 423 break; 424 425 case 'C': 426 setLocoForConsistFunctions(inPackage.substring(1)); 427 428 break; 429 430 case 'c': //v>=1.7 Consist Lead from RosterEntry 431 setRosterLocoForConsistFunctions(inPackage.substring(1)); 432 break; 433 434 case 'I': 435 idle(); 436 break; 437 438 case 's': //v>=2.0 439 handleSpeedStepMode(decodeSpeedStepMode(inPackage.substring(1))); 440 break; 441 442 case 'm': //v>=2.0 443 handleMomentary(inPackage.substring(1)); 444 break; 445 446 case 'q': //v>=2.0 447 handleRequest(inPackage.substring(1)); 448 break; 449 default: 450 log.warn("Unhandled code: {}", inPackage.charAt(0)); 451 break; 452 } 453 } catch (NullPointerException e) { 454 log.warn("No throttle frame to receive: {}", inPackage); 455 return false; 456 } 457 try { // Some layout connections cannot handle rapid inputs 458 Thread.sleep(20); 459 } catch (java.lang.InterruptedException ex) { 460 } 461 } else { // Address not set 462 switch (inPackage.charAt(0)) { 463 case 'L': // Set a Long address. 464 int addr = Integer.parseInt(inPackage.substring(1)); 465 setAddress(addr, true); 466 break; 467 468 case 'S': // Set a Short address. 469 addr = Integer.parseInt(inPackage.substring(1)); 470 setAddress(addr, false); 471 break; 472 473 case 'E': //v>=1.7 Address from RosterEntry 474 requestEntryFromID(inPackage.substring(1)); 475 break; 476 477 case 'C': 478 setLocoForConsistFunctions(inPackage.substring(1)); 479 480 break; 481 482 case 'c': //v>=1.7 Consist Lead from RosterEntry 483 setRosterLocoForConsistFunctions(inPackage.substring(1)); 484 break; 485 486 default: 487 break; 488 } 489 } 490 return true; 491 492 } 493 494 private void clearLeadLoco() { 495 if (useLeadLocoF) { 496 leadLocoF.dispose(); 497 functionThrottle.removePropertyChangeListener(this); 498 if (throttle != null) { 499 setFunctionThrottle(throttle); 500 } 501 502 leadLocoF = null; 503 useLeadLocoF = false; 504 } 505 } 506 507 public void setFunctionThrottle(DccThrottle t) { 508 functionThrottle = t; 509 functionThrottle.addPropertyChangeListener(this); 510 } 511 512 public void setLocoForConsistFunctions(String inPackage) { 513 /* 514 * This is used to control speed and direction on the 515 * consist address, but have functions mapped to lead. 516 * Consist address must be set first! 517 */ 518 519 leadAddress = new DccLocoAddress(Integer.parseInt(inPackage.substring(1)), (inPackage.charAt(0) != 'S')); 520 log.debug("Setting lead loco address: {}, for consist: {}", leadAddress, getCurrentAddressString()); 521 clearLeadLoco(); 522 leadLocoF = new ConsistFunctionController(this); 523 useLeadLocoF = leadLocoF.requestThrottle(leadAddress); 524 525 if (!useLeadLocoF) { 526 log.warn("Lead loco address not available."); 527 leadLocoF = null; 528 } 529 } 530 531 public void setRosterLocoForConsistFunctions(String id) { 532 RosterEntry re; 533 List<RosterEntry> l = Roster.getDefault().matchingList(null, null, null, null, null, null, id); 534 if (l.size() > 0) { 535 log.debug("Consist Lead Roster Loco found: {} for ID: {}", l.get(0).getDccAddress(), id); 536 re = l.get(0); 537 clearLeadLoco(); 538 leadLocoF = new ConsistFunctionController(this, re); 539 useLeadLocoF = leadLocoF.requestThrottle(re.getDccLocoAddress()); 540 541 if (!useLeadLocoF) { 542 log.warn("Lead loco address not available."); 543 leadLocoF = null; 544 } 545 } else { 546 log.debug("No Roster Loco found for: {}", id); 547 } 548 } 549 550// Device is quitting or has lost connection 551 public void shutdownThrottle() { 552 553 try { 554 if (isAddressSet) { 555 throttle.setSpeedSetting(0); 556 addressRelease(); 557 } 558 } catch (NullPointerException e) { 559 log.warn("No throttle to shutdown"); 560 } 561 clearLeadLoco(); 562 } 563 564 /** 565 * handle the conversion from rawSpeed to the float value needed in the 566 * DccThrottle 567 * 568 * @param rawSpeed Value sent from mobile device, range 0 - 126 569 */ 570 synchronized protected void setSpeed(int rawSpeed) { 571 572 float newSpeed = (rawSpeed * speedMultiplier); 573 574 log.debug("raw: {}, NewSpd: {}", rawSpeed, newSpeed); 575 while(lastSentSpeed.offer(Float.valueOf(newSpeed))==false){ 576 log.debug("failed attempting to add speed to queue"); 577 } 578 throttle.setSpeedSetting(newSpeed); 579 } 580 581 protected void setDirection(boolean isForward) { 582 log.debug("set direction to: {}", (isForward ? "Fwd" : "Rev")); 583 throttle.setIsForward(isForward); 584 } 585 586 protected void eStop() { 587 throttle.setSpeedSetting(-1); 588 } 589 590 protected void idle() { 591 throttle.setSpeedSetting(0); 592 } 593 594 protected void setAddress(int number, boolean isLong) { 595 log.debug("setAddress: {}, isLong: {}", number, isLong); 596 if (rosterLoco != null) { 597 jmri.InstanceManager.throttleManagerInstance().requestThrottle(rosterLoco, this, true); 598 } else { 599 jmri.InstanceManager.throttleManagerInstance().requestThrottle(new DccLocoAddress(number, isLong), this, true); 600 601 } 602 } 603 604 public void requestEntryFromID(String id) { 605 RosterEntry re; 606 List<RosterEntry> l = Roster.getDefault().matchingList(null, null, null, null, null, null, id); 607 if (l.size() > 0) { 608 log.debug("Roster Loco found: {} for ID: {}", l.get(0).getDccAddress(), id); 609 re = l.get(0); 610 rosterLoco = re; 611 setAddress(Integer.parseInt(re.getDccAddress()), re.isLongAddress()); 612 } else { 613 log.debug("No Roster Loco found for: {}", id); 614 } 615 } 616 617 public DccThrottle getThrottle() { 618 return throttle; 619 } 620 621 public DccThrottle getFunctionThrottle() { 622 return functionThrottle; 623 } 624 625 public DccLocoAddress getCurrentAddress() { 626 return (DccLocoAddress) throttle.getLocoAddress(); 627 } 628 629 /** 630 * Get the string representation of this throttles address. Returns 'Not 631 * Set' if no address in use. 632 * 633 * @return string value of throttle address 634 */ 635 public String getCurrentAddressString() { 636 if (isAddressSet) { 637 return ((DccLocoAddress) throttle.getLocoAddress()).toString(); 638 } else { 639 return "Not Set"; 640 } 641 } 642 643 /** 644 * Get the string representation of this Roster ID. Returns empty string 645 * if no address in use. 646 * since 4.15.4 647 * 648 * @return string value of throttle Roster ID 649 */ 650 public String getCurrentRosterIdString() { 651 if (rosterLoco != null) { 652 return rosterLoco.getId() ; 653 } else { 654 return " "; 655 } 656 } 657 658 public void sendAddress() { 659 for (ControllerInterface listener : controllerListeners) { 660 listener.sendPacketToDevice(whichThrottle + getCurrentAddressString()); 661 } 662 } 663 664// Function methods 665 protected void handleFunction(String inPackage) { 666 // get the function # sent from device 667 int receivedFunction = Integer.parseInt(inPackage.substring(2)); 668 if (inPackage.charAt(1) == '1') { // Function Button down 669 log.debug("Trying to set function {}", receivedFunction); 670 // Toggle button state: 671 boolean state = functionThrottle.getFunction(receivedFunction); 672 functionThrottle.setFunction(receivedFunction, !state); 673 log.debug("Throttle: {}, Function: {}, set state: {}", functionThrottle.getLocoAddress(), receivedFunction, !state); 674 } else { // Function Button up 675 676 // F2 is momentary for horn, unless prefs are set to follow roster entry 677 if ((isMomF2) && (receivedFunction==2)) { 678 functionThrottle.setFunction(2, false); 679 return; 680 } 681 682 // Do nothing if lockable, turn off if momentary 683 if (functionThrottle.getFunctionMomentary(receivedFunction)) { 684 functionThrottle.setFunction(receivedFunction, false); 685 log.debug("Throttle: {}, Momentary Function: {}, set false", functionThrottle.getLocoAddress(), receivedFunction); 686 } 687 } 688 } 689 690 protected void forceFunction(String inPackage) { 691 int receivedFunction = Integer.parseInt(inPackage.substring(1)); 692 boolean newVal = inPackage.charAt(0) == '1'; 693 log.debug("Trying to set function {} to {}", receivedFunction,newVal); 694 throttle.setFunction(receivedFunction, newVal); 695 } 696 697 protected void handleSpeedStepMode(SpeedStepMode newMode) { 698 throttle.setSpeedStepMode(newMode); 699 } 700 701 protected void handleMomentary(String inPackage) { 702 int receivedFunction = Integer.parseInt(inPackage.substring(1)); 703 boolean newVal = inPackage.charAt(0) == '1'; 704 log.debug("Trying to set function {} to {}", receivedFunction,newVal ? "Momentary":"Locking"); 705 throttle.setFunctionMomentary(receivedFunction, newVal); 706 } 707 708 protected void handleRequest(String inPackage) { 709 switch (inPackage.charAt(0)) { 710 case 'V': { 711 sendCurrentSpeed(throttle); 712 break; 713 } 714 case 'R': { 715 sendCurrentDirection(throttle); 716 break; 717 } 718 case 's': { 719 sendSpeedStepMode(throttle); 720 break; 721 } 722 case 'm': { 723 sendAllMomentaryStates(throttle); 724 break; 725 } 726 default: 727 log.warn("Unhandled code: {}", inPackage.charAt(0)); 728 break; 729 } 730 731 } 732 733 734 private static SpeedStepMode decodeSpeedStepMode(String mode) { 735 // NOTE: old speed step modes use the original numeric values 736 // from when speed step modes were in DccThrottle. If the input does not match 737 // any of the old modes, decode based on the new speed step names. 738 if(mode.equals("1")) { 739 return SpeedStepMode.NMRA_DCC_128; 740 } else if(mode.equals("2")) { 741 return SpeedStepMode.NMRA_DCC_28; 742 } else if(mode.equals("4")) { 743 return SpeedStepMode.NMRA_DCC_27; 744 } else if(mode.equals("8")) { 745 return SpeedStepMode.NMRA_DCC_14; 746 } else if(mode.equals("16")) { 747 return SpeedStepMode.MOTOROLA_28; 748 } 749 return SpeedStepMode.getByName(mode); 750 } 751 752 private final static Logger log = LoggerFactory.getLogger(ThrottleController.class); 753 754}