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 for (int funcNum = 0; funcNum < 29; funcNum++) { 276 t.setFunctionMomentary(funcNum, !(re.getFunctionLockable(funcNum))); 277 } 278 } 279 } 280 281 public void sendFunctionLabels(RosterEntry re) { 282 283 if (re != null) { 284 StringBuilder functionString = new StringBuilder(); 285 if (whichThrottle == 'S') { 286 functionString.append("RS29}|{"); 287 } else { 288 // I know, it should have been 'RT' but this was before there were two throttles. 289 functionString.append("RF29}|{"); 290 } 291 functionString.append(getCurrentAddressString()); 292 293 int i; 294 for (i = 0; i < 29; i++) { 295 functionString.append("]\\["); 296 if ((re.getFunctionLabel(i) != null)) { 297 functionString.append(re.getFunctionLabel(i)); 298 } 299 } 300 for (ControllerInterface listener : controllerListeners) { 301 listener.sendPacketToDevice(functionString.toString()); 302 } 303 } 304 305 } 306 307 /** 308 * send all function states, primarily for initial status Current Format: 309 * RPF}|{whichThrottle]\[function}|{state]\[function}|{state... 310 * 311 * @param t throttle to send functions to 312 */ 313 public void sendAllFunctionStates(DccThrottle t) { 314 315 log.debug("Sending state of all functions"); 316 StringBuilder message = new StringBuilder(buildFStatesHeader()); 317 318 for (int cnt = 0; cnt < 29; cnt++) { 319 message.append("]\\[F"); 320 message.append(cnt); 321 message.append("}|{"); 322 message.append(t.getFunction(cnt) ); 323 } 324 325 for (ControllerInterface listener : controllerListeners) { 326 listener.sendPacketToDevice(message.toString()); 327 } 328 329 } 330 331 protected String buildFStatesHeader() { 332 return ("RPF}|{" + whichThrottle); 333 } 334 335 synchronized protected void sendCurrentSpeed(DccThrottle t) { 336 } 337 338 protected void sendCurrentDirection(DccThrottle t) { 339 } 340 341 protected void sendSpeedStepMode(DccThrottle t) { 342 } 343 344 protected void sendAllMomentaryStates(DccThrottle t) { 345 } 346 347 /** 348 * Figure out what the received command means, where it has to go, and 349 * translate to a jmri method. 350 * 351 * @param inPackage The package minus its prefix which steered it here. 352 * @return true to keep reading in run loop. 353 */ 354 public boolean sort(String inPackage) { 355 if (inPackage.charAt(0) == 'Q') {// If device has Quit. 356 shutdownThrottle(); 357 return false; 358 } 359 if (isAddressSet) { 360 361 try { 362 switch (inPackage.charAt(0)) { 363 case 'V': // Velocity 364 setSpeed(Integer.parseInt(inPackage.substring(1))); 365 366 break; 367 368 case 'X': 369 eStop(); 370 371 break; 372 373 case 'F': // Function 374 375 handleFunction(inPackage); 376 377 break; 378 379 case 'f': //v>=2.0 Force function 380 381 forceFunction(inPackage.substring(1)); 382 383 break; 384 385 case 'R': // Direction 386 setDirection(!inPackage.endsWith("0")); // 0 sets to reverse, all others forward 387 break; 388 389 case 'r': // Release 390 addressRelease(); 391 break; 392 393 case 'd': // Dispatch 394 addressDispatch(); 395 break; 396 397 case 'L': // Set a Long address. 398 addressRelease(); 399 int addr = Integer.parseInt(inPackage.substring(1)); 400 setAddress(addr, true); 401 break; 402 403 case 'S': // Set a Short address. 404 addressRelease(); 405 addr = Integer.parseInt(inPackage.substring(1)); 406 setAddress(addr, false); 407 break; 408 409 case 'E': //v>=1.7 Address from RosterEntry 410 addressRelease(); 411 requestEntryFromID(inPackage.substring(1)); 412 break; 413 414 case 'C': 415 setLocoForConsistFunctions(inPackage.substring(1)); 416 417 break; 418 419 case 'c': //v>=1.7 Consist Lead from RosterEntry 420 setRosterLocoForConsistFunctions(inPackage.substring(1)); 421 break; 422 423 case 'I': 424 idle(); 425 break; 426 427 case 's': //v>=2.0 428 handleSpeedStepMode(decodeSpeedStepMode(inPackage.substring(1))); 429 break; 430 431 case 'm': //v>=2.0 432 handleMomentary(inPackage.substring(1)); 433 break; 434 435 case 'q': //v>=2.0 436 handleRequest(inPackage.substring(1)); 437 break; 438 default: 439 log.warn("Unhandled code: {}", inPackage.charAt(0)); 440 break; 441 } 442 } catch (NullPointerException e) { 443 log.warn("No throttle frame to receive: {}", inPackage); 444 return false; 445 } 446 try { // Some layout connections cannot handle rapid inputs 447 Thread.sleep(20); 448 } catch (java.lang.InterruptedException ex) { 449 } 450 } else { // Address not set 451 switch (inPackage.charAt(0)) { 452 case 'L': // Set a Long address. 453 int addr = Integer.parseInt(inPackage.substring(1)); 454 setAddress(addr, true); 455 break; 456 457 case 'S': // Set a Short address. 458 addr = Integer.parseInt(inPackage.substring(1)); 459 setAddress(addr, false); 460 break; 461 462 case 'E': //v>=1.7 Address from RosterEntry 463 requestEntryFromID(inPackage.substring(1)); 464 break; 465 466 case 'C': 467 setLocoForConsistFunctions(inPackage.substring(1)); 468 469 break; 470 471 case 'c': //v>=1.7 Consist Lead from RosterEntry 472 setRosterLocoForConsistFunctions(inPackage.substring(1)); 473 break; 474 475 default: 476 break; 477 } 478 } 479 return true; 480 481 } 482 483 private void clearLeadLoco() { 484 if (useLeadLocoF) { 485 leadLocoF.dispose(); 486 functionThrottle.removePropertyChangeListener(this); 487 if (throttle != null) { 488 setFunctionThrottle(throttle); 489 } 490 491 leadLocoF = null; 492 useLeadLocoF = false; 493 } 494 } 495 496 public void setFunctionThrottle(DccThrottle t) { 497 functionThrottle = t; 498 functionThrottle.addPropertyChangeListener(this); 499 } 500 501 public void setLocoForConsistFunctions(String inPackage) { 502 /* 503 * This is used to control speed and direction on the 504 * consist address, but have functions mapped to lead. 505 * Consist address must be set first! 506 */ 507 508 leadAddress = new DccLocoAddress(Integer.parseInt(inPackage.substring(1)), (inPackage.charAt(0) != 'S')); 509 log.debug("Setting lead loco address: {}, for consist: {}", leadAddress, getCurrentAddressString()); 510 clearLeadLoco(); 511 leadLocoF = new ConsistFunctionController(this); 512 useLeadLocoF = leadLocoF.requestThrottle(leadAddress); 513 514 if (!useLeadLocoF) { 515 log.warn("Lead loco address not available."); 516 leadLocoF = null; 517 } 518 } 519 520 public void setRosterLocoForConsistFunctions(String id) { 521 RosterEntry re; 522 List<RosterEntry> l = Roster.getDefault().matchingList(null, null, null, null, null, null, id); 523 if (l.size() > 0) { 524 log.debug("Consist Lead Roster Loco found: {} for ID: {}", l.get(0).getDccAddress(), id); 525 re = l.get(0); 526 clearLeadLoco(); 527 leadLocoF = new ConsistFunctionController(this, re); 528 useLeadLocoF = leadLocoF.requestThrottle(re.getDccLocoAddress()); 529 530 if (!useLeadLocoF) { 531 log.warn("Lead loco address not available."); 532 leadLocoF = null; 533 } 534 } else { 535 log.debug("No Roster Loco found for: {}", id); 536 } 537 } 538 539// Device is quitting or has lost connection 540 public void shutdownThrottle() { 541 542 try { 543 if (isAddressSet) { 544 throttle.setSpeedSetting(0); 545 addressRelease(); 546 } 547 } catch (NullPointerException e) { 548 log.warn("No throttle to shutdown"); 549 } 550 clearLeadLoco(); 551 } 552 553 /** 554 * handle the conversion from rawSpeed to the float value needed in the 555 * DccThrottle 556 * 557 * @param rawSpeed Value sent from mobile device, range 0 - 126 558 */ 559 synchronized protected void setSpeed(int rawSpeed) { 560 561 float newSpeed = (rawSpeed * speedMultiplier); 562 563 log.debug("raw: {}, NewSpd: {}", rawSpeed, newSpeed); 564 while(lastSentSpeed.offer(Float.valueOf(newSpeed))==false){ 565 log.debug("failed attempting to add speed to queue"); 566 } 567 throttle.setSpeedSetting(newSpeed); 568 } 569 570 protected void setDirection(boolean isForward) { 571 log.debug("set direction to: {}", (isForward ? "Fwd" : "Rev")); 572 throttle.setIsForward(isForward); 573 } 574 575 protected void eStop() { 576 throttle.setSpeedSetting(-1); 577 } 578 579 protected void idle() { 580 throttle.setSpeedSetting(0); 581 } 582 583 protected void setAddress(int number, boolean isLong) { 584 log.debug("setAddress: {}, isLong: {}", number, isLong); 585 if (rosterLoco != null) { 586 jmri.InstanceManager.throttleManagerInstance().requestThrottle(rosterLoco, this, true); 587 } else { 588 jmri.InstanceManager.throttleManagerInstance().requestThrottle(new DccLocoAddress(number, isLong), this, true); 589 590 } 591 } 592 593 public void requestEntryFromID(String id) { 594 RosterEntry re; 595 List<RosterEntry> l = Roster.getDefault().matchingList(null, null, null, null, null, null, id); 596 if (l.size() > 0) { 597 log.debug("Roster Loco found: {} for ID: {}", l.get(0).getDccAddress(), id); 598 re = l.get(0); 599 rosterLoco = re; 600 setAddress(Integer.parseInt(re.getDccAddress()), re.isLongAddress()); 601 } else { 602 log.debug("No Roster Loco found for: {}", id); 603 } 604 } 605 606 public DccThrottle getThrottle() { 607 return throttle; 608 } 609 610 public DccThrottle getFunctionThrottle() { 611 return functionThrottle; 612 } 613 614 public DccLocoAddress getCurrentAddress() { 615 return (DccLocoAddress) throttle.getLocoAddress(); 616 } 617 618 /** 619 * Get the string representation of this throttles address. Returns 'Not 620 * Set' if no address in use. 621 * 622 * @return string value of throttle address 623 */ 624 public String getCurrentAddressString() { 625 if (isAddressSet) { 626 return ((DccLocoAddress) throttle.getLocoAddress()).toString(); 627 } else { 628 return "Not Set"; 629 } 630 } 631 632 /** 633 * Get the string representation of this Roster ID. Returns empty string 634 * if no address in use. 635 * since 4.15.4 636 * 637 * @return string value of throttle Roster ID 638 */ 639 public String getCurrentRosterIdString() { 640 if (rosterLoco != null) { 641 return rosterLoco.getId() ; 642 } else { 643 return " "; 644 } 645 } 646 647 public void sendAddress() { 648 for (ControllerInterface listener : controllerListeners) { 649 listener.sendPacketToDevice(whichThrottle + getCurrentAddressString()); 650 } 651 } 652 653// Function methods 654 protected void handleFunction(String inPackage) { 655 // get the function # sent from device 656 int receivedFunction = Integer.parseInt(inPackage.substring(2)); 657 if (inPackage.charAt(1) == '1') { // Function Button down 658 log.debug("Trying to set function {}", receivedFunction); 659 // Toggle button state: 660 boolean state = functionThrottle.getFunction(receivedFunction); 661 functionThrottle.setFunction(receivedFunction, !state); 662 log.debug("Throttle: {}, Function: {}, set state: {}", functionThrottle.getLocoAddress(), receivedFunction, !state); 663 } else { // Function Button up 664 665 // F2 is momentary for horn, unless prefs are set to follow roster entry 666 if ((isMomF2) && (receivedFunction==2)) { 667 functionThrottle.setFunction(2, false); 668 return; 669 } 670 671 // Do nothing if lockable, turn off if momentary 672 if (functionThrottle.getFunctionMomentary(receivedFunction)) { 673 functionThrottle.setFunction(receivedFunction, false); 674 log.debug("Throttle: {}, Momentary Function: {}, set false", functionThrottle.getLocoAddress(), receivedFunction); 675 } 676 } 677 } 678 679 protected void forceFunction(String inPackage) { 680 int receivedFunction = Integer.parseInt(inPackage.substring(1)); 681 boolean newVal = inPackage.charAt(0) == '1'; 682 log.debug("Trying to set function {} to {}", receivedFunction,newVal); 683 throttle.setFunction(receivedFunction, newVal); 684 } 685 686 protected void handleSpeedStepMode(SpeedStepMode newMode) { 687 throttle.setSpeedStepMode(newMode); 688 } 689 690 protected void handleMomentary(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 ? "Momentary":"Locking"); 694 throttle.setFunctionMomentary(receivedFunction, newVal); 695 } 696 697 protected void handleRequest(String inPackage) { 698 switch (inPackage.charAt(0)) { 699 case 'V': { 700 sendCurrentSpeed(throttle); 701 break; 702 } 703 case 'R': { 704 sendCurrentDirection(throttle); 705 break; 706 } 707 case 's': { 708 sendSpeedStepMode(throttle); 709 break; 710 } 711 case 'm': { 712 sendAllMomentaryStates(throttle); 713 break; 714 } 715 default: 716 log.warn("Unhandled code: {}", inPackage.charAt(0)); 717 break; 718 } 719 720 } 721 722 723 private static SpeedStepMode decodeSpeedStepMode(String mode) { 724 // NOTE: old speed step modes use the original numeric values 725 // from when speed step modes were in DccThrottle. If the input does not match 726 // any of the old modes, decode based on the new speed step names. 727 if(mode.equals("1")) { 728 return SpeedStepMode.NMRA_DCC_128; 729 } else if(mode.equals("2")) { 730 return SpeedStepMode.NMRA_DCC_28; 731 } else if(mode.equals("4")) { 732 return SpeedStepMode.NMRA_DCC_27; 733 } else if(mode.equals("8")) { 734 return SpeedStepMode.NMRA_DCC_14; 735 } else if(mode.equals("16")) { 736 return SpeedStepMode.MOTOROLA_28; 737 } 738 return SpeedStepMode.getByName(mode); 739 } 740 741 private final static Logger log = LoggerFactory.getLogger(ThrottleController.class); 742 743}