001package jmri.jmrit.roster; 002 003import java.util.ArrayList; 004import java.util.LinkedList; 005import java.util.List; 006import java.util.Locale; 007import java.util.Map.Entry; 008import java.util.TreeMap; 009 010import javax.annotation.CheckForNull; 011 012import jmri.Block; 013import jmri.DccThrottle; 014import jmri.InstanceManager; 015import jmri.NamedBean; 016import jmri.Sensor; 017import java.beans.PropertyChangeListener; 018import jmri.Section; 019import jmri.implementation.SignalSpeedMap; 020 021import org.jdom2.Element; 022 023/** 024 * A class to store a speed profile for a given loco. 025 * The speed steps against the profile are on a scale of 0 to 1000, 026 * this equates to the float speed x 1000. 027 * This allows a single profile to cover different throttle speed step settings. 028 * A profile generated for a loco using 28 steps can be used for a throttle with 126 steps. 029 */ 030public class RosterSpeedProfile { 031 032 private RosterEntry _re = null; 033 034 private float overRunTimeReverse = 0.0f; 035 private float overRunTimeForward = 0.0f; 036 037 private boolean _hasForwardSpeeds = false; 038 private boolean _hasReverseSpeeds = false; 039 040 /** 041 * Create a new RosterSpeedProfile. 042 * @param re the Roster Entry associated with the profile. 043 */ 044 public RosterSpeedProfile(RosterEntry re) { 045 _re = re; 046 } 047 048 /** 049 * Get the RosterEntry associated with the profile. 050 * @return the RosterEntry. 051 */ 052 public RosterEntry getRosterEntry() { 053 return _re; 054 } 055 056 public float getOverRunTimeForward() { 057 return overRunTimeForward; 058 } 059 060 public void setOverRunTimeForward(float dt) { 061 overRunTimeForward = dt; 062 } 063 064 public float getOverRunTimeReverse() { 065 return overRunTimeReverse; 066 } 067 068 public void setOverRunTimeReverse(float dt) { 069 overRunTimeReverse = dt; 070 } 071 072 public void clearCurrentProfile() { 073 speeds = new TreeMap<>(); 074 } 075 076 public void deleteStep(Integer step) { 077 speeds.remove(step); 078 } 079 080 /** 081 * Check if the Speed Profile contains Forward Speeds. 082 * @return true if forward speeds are present, else false. 083 */ 084 public boolean hasForwardSpeeds() { 085 return _hasForwardSpeeds; 086 } 087 088 /** 089 * Check if the Speed Profile contains Reverse Speeds. 090 * @return true if reverse speeds are present, else false. 091 */ 092 public boolean hasReverseSpeeds() { 093 return _hasReverseSpeeds; 094 } 095 096 /** 097 * place / remove SpeedProfile from test mode. 098 * reinitializes speedstep trace array 099 * @param value true/false 100 */ 101 public void setTestMode(boolean value) { 102 synchronized (this){ 103 profileInTestMode = value; 104 } 105 testSteps = new ArrayList<>(); 106 } 107 108 /** 109 * Gets the speed step trace array. 110 * @return speedstep trace array 111 */ 112 public List<SpeedSetting> getSpeedStepTrace() { 113 return testSteps; 114 } 115 116 /** 117 * Speed conversion Millimetres per second to Miles per hour. 118 */ 119 public static final float MMS_TO_MPH = 0.00223694f; 120 121 /** 122 * Speed conversion Millimetres per second to Kilometres per hour. 123 */ 124 public static final float MMS_TO_KPH = 0.0036f; 125 126 /** 127 * Returns the scale speed. 128 * If Warrant preferences are not a speed, value returns unchanged. 129 * @param mms MilliMetres per second. 130 * @param factorFastClock true to factor in the Fast Clock ratio, else false. 131 * @return scale speed in units specified by Warrant Preferences, 132 * unchanged if Warrant preferences are not a speed. 133 */ 134 public float mmsToScaleSpeed(float mms, boolean factorFastClock) { 135 int interp = InstanceManager.getDefault(SignalSpeedMap.class).getInterpretation(); 136 float scale = InstanceManager.getDefault(SignalSpeedMap.class).getLayoutScale(); 137 float fastClockFactor = ( factorFastClock ? 138 (float)InstanceManager.getDefault(jmri.Timebase.class).userGetRate() : 1 ); 139 140 switch (interp) { 141 case SignalSpeedMap.SPEED_MPH: 142 return mms * scale * MMS_TO_MPH * fastClockFactor; 143 case SignalSpeedMap.SPEED_KMPH: 144 return mms * scale * MMS_TO_KPH * fastClockFactor; 145 case SignalSpeedMap.PERCENT_THROTTLE: 146 case SignalSpeedMap.PERCENT_NORMAL: 147 return mms; 148 default: 149 log.warn("MMSToScaleSpeed: Signal Speed Map is not in a scale speed, not modifing."); 150 return mms; 151 } 152 } 153 154 /** 155 * Returns the scale speed as a numeric. 156 * If Warrant preferences are not a speed, value returns unchanged. 157 * @param mms MilliMetres per second 158 * @return scale speed in units specified by Warrant Preferences, 159 * unchanged if Warrant preferences are not a speed. 160 * @deprecated use {@link #mmsToScaleSpeed(float mms)} 161 */ 162 @Deprecated (since="5.9.6",forRemoval=true) 163 public float MMSToScaleSpeed(float mms) { 164 jmri.util.LoggingUtil.deprecationWarning(log, "MMSToScaleSpeed"); 165 return mmsToScaleSpeed(mms); 166 } 167 168 /** 169 * Returns the scale speed as a numeric. 170 * If Warrant preferences are not a speed, value returns unchanged. 171 * Does not factor Fast Clock ratio. 172 * @param mms MilliMetres per second 173 * @return scale speed in units specified by Warrant Preferences, 174 * unchanged if Warrant preferences are not a speed. 175 */ 176 public float mmsToScaleSpeed(float mms) { 177 return mmsToScaleSpeed(mms, false); 178 } 179 180 /** 181 * Returns the scale speed format as I18N string with the units added given 182 * MilliMetres per Second. 183 * If the warrant preference is a percentage of 184 * normal or throttle will use metres per second. 185 * The Fast Clock Ratio is not used in the calculation. 186 * 187 * @param mms MilliMetres per second 188 * @return a string with scale speed and units 189 */ 190 public static String convertMMSToScaleSpeedWithUnits(float mms) { 191 return convertMMSToScaleSpeedWithUnits(mms, false); 192 } 193 194 /** 195 * Returns the scale speed format as I18N string with the units added given 196 * MilliMetres per Second. 197 * If the warrant preference is a percentage of 198 * normal or throttle will use meters per second. 199 * The Fast Clock Ratio is not used in the calculation. 200 * 201 * @param mms MilliMetres per second 202 * @param useShortUnits true for mph etc 203 * @return a string with scale speed and units 204 */ 205 public static String convertMMSToScaleSpeedWithUnits(float mms, boolean useShortUnits) { 206 int interp = InstanceManager.getDefault(SignalSpeedMap.class).getInterpretation(); 207 float scale = InstanceManager.getDefault(SignalSpeedMap.class).getLayoutScale(); 208 String formattedWithUnits; 209 switch (interp) { 210 case SignalSpeedMap.SPEED_MPH: 211 String unitsMph; 212 if (useShortUnits) { 213 unitsMph = Bundle.getMessage("shortmph"); 214 } else { 215 unitsMph = Bundle.getMessage("mph"); 216 } 217 formattedWithUnits = String.format(Locale.getDefault(), "%.2f %s", mms * scale * MMS_TO_MPH, unitsMph); 218 break; 219 case SignalSpeedMap.SPEED_KMPH: 220 String unitsKph; 221 if (useShortUnits) { 222 unitsKph = Bundle.getMessage("shortkph"); 223 } else { 224 unitsKph = Bundle.getMessage("kph"); 225 } 226 formattedWithUnits = String.format(Locale.getDefault(), "%.2f %s", mms * scale * MMS_TO_KPH, unitsKph); 227 break; 228 case SignalSpeedMap.PERCENT_THROTTLE: 229 case SignalSpeedMap.PERCENT_NORMAL: 230 String unitsMms; 231 if (useShortUnits) { 232 unitsMms = Bundle.getMessage("shortmmps"); 233 } else { 234 unitsMms = Bundle.getMessage("mmps"); 235 } 236 formattedWithUnits = String.format(Locale.getDefault(), "%.2f %s", mms, unitsMms); 237 break; 238 default: 239 log.warn("ScaleSpeedToMMS: Signal Speed Map has no interp, not modifing."); 240 formattedWithUnits = String.format( Locale.getDefault(), "%.2f", mms); 241 } 242 return formattedWithUnits; 243 } 244 245 /** 246 * Returns the scale speed format as a string with the units added given a 247 * throttle setting. and direction. 248 * The Fast Clock Ratio is not used in the calculation. 249 * 250 * @param throttleSetting as percentage of 1.0 251 * @param isForward true or false 252 * @return a string with scale speed and units 253 */ 254 public String convertThrottleSettingToScaleSpeedWithUnits(float throttleSetting, boolean isForward) { 255 return convertMMSToScaleSpeedWithUnits(getSpeed(throttleSetting, isForward)); 256 } 257 258 /** 259 * MilliMetres per Second given scale speed. 260 * The Fast Clock Ratio is not used in the calculation. 261 * @param scaleSpeed in MPH or KPH 262 * @return MilliMetres per second 263 */ 264 public float convertScaleSpeedToMMS(float scaleSpeed) { 265 int interp = InstanceManager.getDefault(SignalSpeedMap.class).getInterpretation(); 266 float scale = InstanceManager.getDefault(SignalSpeedMap.class).getLayoutScale(); 267 float mmsSpeed; 268 switch (interp) { 269 case SignalSpeedMap.SPEED_MPH: 270 mmsSpeed = scaleSpeed / scale / MMS_TO_MPH; 271 break; 272 case SignalSpeedMap.SPEED_KMPH: 273 mmsSpeed = scaleSpeed / scale / MMS_TO_KPH; 274 break; 275 default: 276 log.warn("ScaleSpeedToMMS: Signal Speed Map is not in a scale speed, not modifing."); 277 mmsSpeed = scaleSpeed; 278 } 279 return mmsSpeed; 280 } 281 282 /** 283 * Converts from signal map speed to a throttle setting. 284 * The Fast Clock Ratio is not used in the calculation. 285 * @param signalMapSpeed value from warrants preferences 286 * @param isForward direction of travel 287 * @return throttle setting 288 */ 289 public float getThrottleSettingFromSignalMapSpeed(float signalMapSpeed, boolean isForward) { 290 int interp = InstanceManager.getDefault(SignalSpeedMap.class).getInterpretation(); 291 float throttleSetting = 0.0f; 292 switch (interp) { 293 case SignalSpeedMap.PERCENT_NORMAL: 294 case SignalSpeedMap.PERCENT_THROTTLE: 295 throttleSetting = signalMapSpeed / 100.0f; 296 break; 297 case SignalSpeedMap.SPEED_KMPH: 298 case SignalSpeedMap.SPEED_MPH: 299 throttleSetting = getThrottleSetting(convertScaleSpeedToMMS(signalMapSpeed), isForward); 300 break; 301 default: 302 log.warn("getThrottleSettingFromSignalMapSpeed: Signal Speed Map interp not supported."); 303 } 304 return throttleSetting; 305 } 306 307 /** 308 * Set the speed for the given speed step. 309 * 310 * @param speedStep the speed step to set 311 * @param forward speed in meters per second for running forward at 312 * speedStep 313 * @param reverse speed in meters per second for running in reverse at 314 * speedStep 315 */ 316 public void setSpeed(int speedStep, float forward, float reverse) { 317 SpeedStep ss = speeds.computeIfAbsent(speedStep, k -> new SpeedStep()); 318 ss.setForwardSpeed(forward); 319 ss.setReverseSpeed(reverse); 320 if (forward > 0.0f) { 321 _hasForwardSpeeds = true; 322 } 323 if (reverse > 0.0f) { 324 _hasReverseSpeeds = true; 325 } 326 } 327 328 public SpeedStep getSpeedStep(float speed) { 329 int iSpeedStep = Math.round(speed * 1000); 330 return speeds.get(iSpeedStep); 331 } 332 333 public void setForwardSpeed(float speedStep, float forward) { 334 if (forward > 0.0f) { 335 _hasForwardSpeeds = true; 336 } else { 337 return; 338 } 339 int iSpeedStep = Math.round(speedStep * 1000); 340 speeds.computeIfAbsent(iSpeedStep, k -> new SpeedStep()).setForwardSpeed(forward); 341 } 342 343 /** 344 * Merge raw throttleSetting value with an existing profile SpeedStep if 345 * key for the throttleSetting is within the speedIncrement of the SpeedStep. 346 * @param throttleSetting raw throttle setting value 347 * @param speed track speed 348 * @param speedIncrement throttle's speed step increment. 349 */ 350 public void setForwardSpeed(float throttleSetting, float speed, float speedIncrement) { 351 if (throttleSetting> 0.0f) { 352 _hasForwardSpeeds = true; 353 } else { 354 return; 355 } 356 int key; 357 Entry<Integer, SpeedStep> entry = findEquivalentEntry (throttleSetting, speedIncrement); 358 if (entry != null) { // close keys. i.e. resolve to same throttle step 359 float value = entry.getValue().getForwardSpeed(); 360 speed = (speed + value) / 2; 361 key = entry.getKey(); 362 } else { // nothing close. make new entry 363 key = Math.round(throttleSetting * 1000); 364 } 365 speeds.computeIfAbsent(key, k -> new SpeedStep()).setForwardSpeed(speed); 366 } 367 368 @CheckForNull 369 private Entry<Integer, SpeedStep> findEquivalentEntry (float throttleSetting, float speedIncrement) { 370 // search through table until end for an entry is found whose key / 1000 371 // is within the speedIncrement of the throttleSetting 372 // Note there may be zero values interspersed in the tree 373 Entry<Integer, SpeedStep> entry = speeds.firstEntry(); 374 if (entry == null) { 375 return null; 376 } 377 int key = entry.getKey(); 378 while (entry != null) { 379 entry = speeds.higherEntry(key); 380 if (entry != null) { 381 float speed = entry.getKey(); 382 if (Math.abs(speed/1000.0f - throttleSetting) <= speedIncrement) { 383 return entry; 384 } 385 key = entry.getKey(); 386 } 387 } 388 return null; 389 } 390 391 /** 392 * Merge raw throttleSetting value with an existing profile SpeedStep if 393 * key for the throttleSetting is within the speedIncrement of the SpeedStep. 394 * @param throttleSetting raw throttle setting value 395 * @param speed track speed 396 * @param speedIncrement throttle's speed step increment. 397 */ 398 public void setReverseSpeed(float throttleSetting, float speed, float speedIncrement) { 399 if (throttleSetting> 0.0f) { 400 _hasReverseSpeeds = true; 401 } else { 402 return; 403 } 404 int key; 405 Entry<Integer, SpeedStep> entry = findEquivalentEntry (throttleSetting, speedIncrement); 406 if (entry != null) { // close keys. i.e. resolve to same throttle step 407 float value = entry.getValue().getReverseSpeed(); 408 speed = (speed + value) / 2; 409 key = entry.getKey(); 410 } else { // nothing close. make new entry 411 key = Math.round(throttleSetting * 1000); 412 } 413 speeds.computeIfAbsent(key, k -> new SpeedStep()).setReverseSpeed(speed); 414 } 415 416 public void setReverseSpeed(float speedStep, float reverse) { 417 if (reverse > 0.0f) { 418 _hasReverseSpeeds = true; 419 } else { 420 return; 421 } 422 int iSpeedStep = Math.round(speedStep * 1000); 423 speeds.computeIfAbsent(iSpeedStep, k -> new SpeedStep()).setReverseSpeed(reverse); 424 } 425 426 /** 427 * return the forward speed in milli-meters per second for a given 428 * percentage throttle 429 * 430 * @param speedStep which is actual percentage throttle 431 * @return MilliMetres per second using straight line interpolation for 432 * missing points 433 */ 434 public float getForwardSpeed(float speedStep) { 435 int iSpeedStep = Math.round(speedStep * 1000); 436 if (iSpeedStep <= 0 || !_hasForwardSpeeds) { 437 return 0.0f; 438 } 439 // Note there may be zero values interspersed in the tree 440 if (speeds.containsKey(iSpeedStep)) { 441 float speed = speeds.get(iSpeedStep).getForwardSpeed(); 442 if (speed > 0.0f) { 443 return speed; 444 } 445 } 446 log.trace("no exact match forward for {}", iSpeedStep); 447 float lower = 0.0f; 448 float higher = 0.0f; 449 int highStep = iSpeedStep; 450 int lowStep = iSpeedStep; 451 452 Entry<Integer, SpeedStep> entry = speeds.higherEntry(highStep); 453 while (entry != null && higher <= 0.0f) { 454 highStep = entry.getKey(); 455 float value = entry.getValue().getForwardSpeed(); 456 if (value > 0.0f) { 457 higher = value; 458 } 459 entry = speeds.higherEntry(highStep); 460 } 461 boolean nothingHigher = (higher <= 0.0f); 462 463 entry = speeds.lowerEntry(lowStep); 464 while (entry != null && lower <= 0.0f) { 465 lowStep = entry.getKey(); 466 float value = entry.getValue().getForwardSpeed(); 467 if (value > 0.0f) { 468 lower = value; 469 } 470 entry = speeds.lowerEntry(lowStep); 471 } 472 log.trace("lowStep={}, lower={} highStep={} higher={} for iSpeedStep={}", 473 lowStep, lower, highStep, higher, iSpeedStep); 474 if (lower <= 0.0f) { // nothing lower 475 if (nothingHigher) { 476 log.error("Nothing in speed Profile"); 477 return 0.0f; // no forward speeds at all 478 } 479 return higher * iSpeedStep / highStep; 480 } 481 if (nothingHigher) { 482// return lower * (1.0f + (iSpeedStep - lowStep) / (1000.0f - lowStep)); 483 return lower + (iSpeedStep - lowStep) * lower / lowStep; 484 } 485 486 float valperstep = (higher - lower) / (highStep - lowStep); 487 488 return lower + (valperstep * (iSpeedStep - lowStep)); 489 } 490 491 /** 492 * return the reverse speed in millimetres per second for a given percentage 493 * throttle 494 * 495 * @param speedStep percentage of throttle 0.nnn 496 * @return millimetres per second 497 */ 498 public float getReverseSpeed(float speedStep) { 499 int iSpeedStep = Math.round(speedStep * 1000); 500 if (iSpeedStep <= 0 || !_hasReverseSpeeds) { 501 return 0.0f; 502 } 503 if (speeds.containsKey(iSpeedStep)) { 504 float speed = speeds.get(iSpeedStep).getReverseSpeed(); 505 if (speed > 0.0f) { 506 return speed; 507 } 508 } 509 log.trace("no exact match reverse for {}", iSpeedStep); 510 float lower = 0.0f; 511 float higher = 0.0f; 512 int highStep = iSpeedStep; 513 int lowStep = iSpeedStep; 514 // Note there may be zero values interspersed in the tree 515 516 Entry<Integer, SpeedStep> entry = speeds.higherEntry(highStep); 517 while (entry != null && higher <= 0.0f) { 518 highStep = entry.getKey(); 519 float value = entry.getValue().getReverseSpeed(); 520 if (value > 0.0f) { 521 higher = value; 522 } 523 entry = speeds.higherEntry(highStep); 524 } 525 boolean nothingHigher = (higher <= 0.0f); 526 entry = speeds.lowerEntry(lowStep); 527 while (entry != null && lower <= 0.0f) { 528 lowStep = entry.getKey(); 529 float value = entry.getValue().getReverseSpeed(); 530 if (value > 0.0f) { 531 lower = value; 532 } 533 entry = speeds.lowerEntry(lowStep); 534 } 535 log.trace("lowStep={}, lower={} highStep={} higher={} for iSpeedStep={}", 536 lowStep, lower, highStep, higher, iSpeedStep); 537 if (lower <= 0.0f) { // nothing lower 538 if (nothingHigher) { 539 log.error("Nothing in speed Profile"); 540 return 0.0f; // no reverse speeds at all 541 } 542 return higher * iSpeedStep / highStep; 543 } 544 if (nothingHigher) { 545 return lower * (1.0f + (iSpeedStep - lowStep) / (1000.0f - lowStep)); 546 } 547 548 float valperstep = (higher - lower) / (highStep - lowStep); 549 550 return lower + (valperstep * (iSpeedStep - lowStep)); 551 } 552 553 /** 554 * Get the approximate time a loco may travel a given distance at a given 555 * speed step. 556 * 557 * @param isForward true if loco is running forward; false otherwise 558 * @param speedStep the desired speed step 559 * @param distance the desired distance in millimeters 560 * @return the approximate time in seconds 561 */ 562 public float getDurationOfTravelInSeconds(boolean isForward, float speedStep, int distance) { 563 float spd; 564 if (isForward) { 565 spd = getForwardSpeed(speedStep); 566 } else { 567 spd = getReverseSpeed(speedStep); 568 } 569 if (spd < 0.0f) { 570 log.error("Speed not available to compute duration of travel"); 571 return 0.0f; 572 } 573 return (distance / spd); 574 } 575 576 /** 577 * Get the approximate distance a loco may travel a given duration at a 578 * given speed step. 579 * 580 * @param isForward true if loco is running forward; false otherwise 581 * @param speedStep the desired speed step 582 * @param duration the desired time in seconds 583 * @return the approximate distance in millimeters 584 */ 585 public float getDistanceTravelled(boolean isForward, float speedStep, float duration) { 586 float spd; 587 if (isForward) { 588 spd = getForwardSpeed(speedStep); 589 } else { 590 spd = getReverseSpeed(speedStep); 591 } 592 if (spd < 0.0f) { 593 log.error("Speed not available to compute distance travelled"); 594 return 0.0f; 595 } 596 return Math.abs(spd * duration); 597 } 598 599 /* 600 * ============================================================ 601 * Distance-based stopping API (public) - Stop to zero over a given distance 602 * (mm) - Approach to min reliable speed over a distance, then stop at 603 * sensor Notes: - Executes via RosterSpeedProfile's own stepQueue/stopTimer 604 * (H2/4A). - Overrun compensation ONLY for stop-to-zero (H4). - Optional 605 * speedFactor pre-divide supported (H3 / 4B). 606 * ============================================================ 607 */ 608 609 /** 610 * Plan and execute a stop-to-zero over a given distance (actual 611 * millimetres). 612 * 613 * @param t The DccThrottle to drive 614 * @param distanceMm Distance in mm ({@code >=} 0) 615 */ 616 public void planStopToZeroOverDistance(DccThrottle t, float distanceMm) { 617 planStopToZeroOverDistance(t, distanceMm, /* speedFactor */ 1.0f); 618 } 619 620 /** 621 * Plan and execute a stop-to-zero over a given distance (actual 622 * millimetres), with an optional external speed factor pre-divide (see 623 * AutoActiveTrain behaviour). 624 * 625 * @param t The DccThrottle to drive 626 * @param distanceMm Distance in mm ({@code >=} 0) 627 * @param speedFactor If {@code >} 0, throttle commands are divided by this 628 * factor before enqueuing. 629 */ 630 public void planStopToZeroOverDistance(DccThrottle t, float distanceMm, float speedFactor) { 631 planDistanceSchedule(t, distanceMm, /* toMinOnly */ false, speedFactor); 632 } 633 634 /** 635 * Plan and execute an approach to the minimum reliable operating speed over 636 * the given distance, then stop when the supplied sensor transitions to 637 * ACTIVE. 638 * 639 * @param t The DccThrottle to drive 640 * @param distanceMm Distance in mm ({@code >=} 0) 641 * @param stopSensor The sensor on which to stop (must not be null) 642 */ 643 public void planApproachToMinOverDistanceThenStopBySensor( 644 DccThrottle t, float distanceMm, jmri.Sensor stopSensor) { 645 planApproachToMinOverDistanceThenStopBySensor(t, distanceMm, stopSensor, /* 646 * speedFactor 647 */ 1.0f); 648 } 649 650 /** 651 * Plan and execute an approach to the minimum reliable operating speed over 652 * the given distance, then stop when the supplied sensor transitions to 653 * ACTIVE. Supports optional speed factor pre-divide. 654 * 655 * @param t The DccThrottle to drive 656 * @param distanceMm Distance in mm ({@code >=} 0) 657 * @param stopSensor The sensor on which to stop (must not be null) 658 * @param speedFactor If {@code >} 0, throttle commands are divided by this 659 * factor before enqueuing. 660 */ 661 public void planApproachToMinOverDistanceThenStopBySensor( 662 DccThrottle t, float distanceMm, jmri.Sensor stopSensor, float speedFactor) { 663 if (stopSensor == null) { 664 log.warn("planApproachToMin... called with null stopSensor; forcing immediate stop."); 665 if (t != null) 666 t.setSpeedSetting(0.0f); 667 return; 668 } 669 // Stash the sensor + a one-shot listener; finishChange() removes any leftover listener. 670 approachStopSensor = stopSensor; 671 approachStopSensorListener = (java.beans.PropertyChangeEvent e) -> { 672 if ("KnownState".equals(e.getPropertyName())) { 673 try { 674 if (((Integer) e.getNewValue()).intValue() == jmri.Sensor.ACTIVE) { 675 if (_throttle != null) { 676 lastIssuedSpeedSetting = 0.0f; 677 _throttle.setSpeedSetting(0.0f); 678 } 679 finishChange(); // also detaches this listener 680 } 681 } catch (RuntimeException ex) { 682 log.warn("Stop-by-sensor handler failed; forcing stop.", ex); 683 try { 684 if (_throttle != null) 685 _throttle.setSpeedSetting(0.0f); 686 } catch (Exception ex2) { 687 log.debug("Stop-by-sensor handler could not force stop.", ex2); 688 } 689 finishChange(); 690 } 691 } 692 }; 693 approachStopSensor.addPropertyChangeListener(approachStopSensorListener); 694 695 planDistanceSchedule(t, distanceMm, /* toMinOnly */ true, speedFactor); 696 } 697 698 /* 699 * ============================================================ Helpers for 700 * distance planning (mirrors inner controller logic) 701 * ============================================================ 702 */ 703 704 /** Clamp helper for throttle percentage. */ 705 private static float clampPct(float pct) { 706 if (pct < 0.0f) 707 return 0.0f; 708 if (pct > 1.0f) 709 return 1.0f; 710 return pct; 711 } 712 713 /** 714 * Invert the roster profile: map target speed (mm/s) -> throttle % via 715 * bisection. 716 */ 717 private float throttleForSpeedMms(final float targetMms, final boolean forward, 718 final float minPct, final float maxPct) { 719 float lo = clampPct(minPct); 720 float hi = clampPct(maxPct); 721 // Guard: if target is below/above bracket, return bracket end 722 float loMms = getSpeed(lo, forward); 723 float hiMms = getSpeed(hi, forward); 724 if (targetMms <= loMms) 725 return lo; 726 if (targetMms >= hiMms) 727 return hi; 728 729 float x = 0.5f * (lo + hi); 730 for (int i = 0; i < 18; i++) { // ~0.004 resolution 731 x = 0.5f * (lo + hi); 732 float xmms = getSpeed(x, forward); 733 if (xmms < targetMms) 734 lo = x; 735 else 736 hi = x; 737 } 738 return clampPct(x); 739 } 740 741 /** 742 * Core planner: builds a constant-deceleration throttle schedule to reach 743 * either: - zero speed exactly at distance (toMinOnly=false), applying 744 * overrun compensation; or - minimum reliable operating speed at distance 745 * (toMinOnly=true), then waits for stopSensor. Executes via this profile's 746 * own stepQueue/stopTimer (H2/4A). 747 */ 748 private void planDistanceSchedule(DccThrottle t, float distanceMm, boolean toMinOnly, float speedFactor) { 749 if (t == null) { 750 log.warn("planDistanceSchedule called with null throttle; ignoring."); 751 return; 752 } 753 // Do not clobber caller-configured min/max limits; just read them 754 final float minPct = this.minReliableOperatingSpeed; // 0..1 755 final float maxPct = this.maxOperatingSpeed; // 0..1 756 final boolean forward = t.getIsForward(); 757 758 // Kill any running timer WITHOUT resetting limits (avoid finishChange() here). 759 if (stopTimer != null) { 760 stopTimer.stop(); 761 stopTimer = null; 762 } 763 synchronized (this) { 764 stepQueue = new LinkedList<>(); 765 } 766 767 _throttle = t; 768 // Seed the "effective current" with a quantized value to avoid relying on getSpeedSetting() semantics. 769 lastIssuedSpeedSetting = quantizeToSpeedStep(_throttle, clampPct(_throttle.getSpeedSetting())); 770 771 // Apply a safe speedFactor 772 float speedFactorSafe = (speedFactor > 0.0f) ? speedFactor : 1.0f; 773 774 if (distanceMm <= 0.0f) { 775 if (toMinOnly) { 776 // Assert crawl and return; sensor listener (if any) will stop us. 777 float vMin = getSpeed(Math.max(0.0f, minPct), forward); 778 float thrMin = throttleForSpeedMms(vMin, forward, minPct, maxPct); 779 thrMin = clampPct(thrMin / speedFactorSafe); 780 thrMin = quantizeToSpeedStep(_throttle, thrMin); 781 lastIssuedSpeedSetting = thrMin; 782 _throttle.setSpeedSetting(thrMin); 783 return; 784 } else { 785 lastIssuedSpeedSetting = 0.0f; 786 _throttle.setSpeedSetting(0.0f); 787 return; 788 } 789 } 790 791 // Current speed (mm/s) from quantized speed setting 792 float thrNow = lastIssuedSpeedSetting; 793 float v0 = getSpeed(thrNow, forward); 794 float vMin = getSpeed(Math.max(0.0f, minPct), forward); 795 float vMax = getSpeed(Math.min(1.0f, maxPct), forward); 796 797 // If caller asked for approach-to-min but the configured minimum is effectively zero, 798 // then "approach to min" equals "stop to zero". 799 if (toMinOnly && vMin <= 0.0f) { 800 log.warn("planDistanceSchedule: minReliableOperatingSpeed=0; falling back to stop-to-zero over distance"); 801 toMinOnly = false; 802 } 803 804 // Clamp v0 into [vMin, vMax] to reflect realistic low/high bounds 805 if (v0 < vMin) 806 v0 = vMin; 807 if (v0 > vMax) 808 v0 = vMax; 809 810 // Adjust target distance for overrun ONLY for stop-to-zero. 811 float s = distanceMm; 812 if (!toMinOnly) { 813 float overrunSec = forward ? getOverRunTimeForward() : getOverRunTimeReverse(); 814 if (overrunSec < 0.0f) 815 overrunSec = 0.0f; 816 s = s - (vMin * overrunSec); 817 if (s < Math.max(0.0f, 0.5f * vMin)) { 818 s = Math.max(0.0f, 0.5f * vMin); 819 } 820 } 821 822 // If no distance effectively remains, set terminal target right away 823 if (s <= 0.0f) { 824 if (toMinOnly) { 825 float thrMin = throttleForSpeedMms(vMin, forward, minPct, maxPct); 826 thrMin = clampPct(thrMin / speedFactorSafe); 827 thrMin = quantizeToSpeedStep(_throttle, thrMin); 828 lastIssuedSpeedSetting = thrMin; 829 _throttle.setSpeedSetting(thrMin); 830 } else { 831 lastIssuedSpeedSetting = 0.0f; 832 _throttle.setSpeedSetting(0.0f); 833 } 834 return; 835 } 836 837 // Constant deceleration to meet distance at v=0 (or to vMin then hold). 838 final int internalSliceMs = 50; // internal integration resolution 839 final float dt = internalSliceMs / 1000.0f; 840 final int minCmdMs = getEffectiveMinCommandIntervalMs(); 841 842 float a; // mm/s^2 843 if (!toMinOnly) { 844 a = (v0 > 0.0f) ? -(v0 * v0) / (2.0f * s) : 0.0f; 845 } else { 846 a = (v0 > vMin && s > 0.0f) ? -((v0 * v0) - (vMin * vMin)) / (2.0f * s) : 0.0f; 847 } 848 849 java.util.LinkedList<SpeedSetting> plan = new java.util.LinkedList<>(); 850 float travelled = 0.0f; 851 float v = v0; 852 853 // Bucket accumulator to enforce command rate limiting without materially changing the integrated distance. 854 int bucketMs = 0; 855 float bucketSpeedTime = 0.0f; // sum of (mms * seconds) 856 857 while (travelled < s) { 858 float remaining = s - travelled; 859 float stepDt = dt; 860 if (toMinOnly && v > vMin && a != 0.0f) { 861 float tToMin = (v - vMin) / Math.abs(a); 862 if (tToMin > 0.0f && tToMin < stepDt) 863 stepDt = tToMin; 864 } 865 866 // Predict next speed 867 float vNext; 868 if (!toMinOnly) { 869 vNext = Math.max(0.0f, v + a * stepDt); 870 } else { 871 float raw = v + a * stepDt; 872 vNext = (raw >= vMin) ? raw : vMin; 873 } 874 875 float vStart = v; 876 float vEnd = vNext; 877 if (toMinOnly) { 878 if (vStart < vMin) 879 vStart = vMin; 880 if (vEnd < vMin) 881 vEnd = vMin; 882 } 883 float vMid = 0.5f * (vStart + vEnd); 884 if (vMid < 0.0f) 885 vMid = 0.0f; 886 887 // Distance in this slice 888 float deltaS; 889 if (!toMinOnly) { 890 deltaS = v * stepDt + 0.5f * a * stepDt * stepDt; 891 } else if (a != 0.0f && v > vMin && vNext >= vMin) { 892 deltaS = v * stepDt + 0.5f * a * stepDt * stepDt; 893 } else { 894 deltaS = vMin * stepDt; 895 } 896 if (deltaS < 0.0f) 897 deltaS = 0.0f; 898 899 // If this slice would overshoot, shorten to land exactly. 900 if (deltaS > remaining && vMid > 0.0f) { 901 float dtFinal = remaining / vMid; 902 if (dtFinal < 0.001f) 903 dtFinal = 0.001f; 904 int msFinal = Math.max(1, Math.round(dtFinal * 1000.0f)); 905 bucketMs += msFinal; 906 bucketSpeedTime += vMid * (msFinal / 1000.0f); 907 908 // Flush the bucket at end of run. 909 float bucketSec = bucketMs / 1000.0f; 910 float avgMms = (bucketSec > 0.0f) ? (bucketSpeedTime / bucketSec) : 0.0f; 911 float thr = throttleForSpeedMms(avgMms, forward, minPct, maxPct); 912 thr = clampPct(thr / speedFactorSafe); 913 thr = quantizeToSpeedStep(_throttle, thr); 914 plan.add(new SpeedSetting(thr, bucketMs, false)); 915 bucketMs = 0; 916 bucketSpeedTime = 0.0f; 917 break; 918 } 919 920 int ms = Math.max(1, Math.round(stepDt * 1000.0f)); 921 bucketMs += ms; 922 bucketSpeedTime += vMid * (ms / 1000.0f); 923 924 travelled += deltaS; 925 v = vNext; 926 927 // Flush the bucket when we reach the minimum command interval or at the end. 928 if (bucketMs >= minCmdMs || travelled >= s) { 929 float bucketSec = bucketMs / 1000.0f; 930 float avgMms = (bucketSec > 0.0f) ? (bucketSpeedTime / bucketSec) : 0.0f; 931 float thr = throttleForSpeedMms(avgMms, forward, minPct, maxPct); 932 thr = clampPct(thr / speedFactorSafe); 933 thr = quantizeToSpeedStep(_throttle, thr); 934 plan.add(new SpeedSetting(thr, bucketMs, false)); 935 bucketMs = 0; 936 bucketSpeedTime = 0.0f; 937 } 938 939 if (!toMinOnly && v <= 0.0f && travelled < s) { 940 break; 941 } 942 } 943 944 // Tail: for stop-to-zero, ensure a final explicit zero command. 945 if (!toMinOnly) { 946 int tailMs = Math.max(minCmdMs, internalSliceMs); 947 plan.add(new SpeedSetting(0.0f, tailMs, false)); 948 } 949 950 // Enqueue and kick timer 951 synchronized (this) { 952 for (SpeedSetting ss : plan) { 953 stepQueue.addLast(ss); 954 if (profileInTestMode) 955 testSteps.add(ss); 956 } 957 } 958 if (stopTimer == null) { 959 setNextStep(); 960 } 961} 962 963 private float distanceRemaining = 0; 964 private float distanceTravelled = 0; 965 966 private TreeMap<Integer, SpeedStep> speeds = new TreeMap<>(); 967 968 private DccThrottle _throttle; 969 970 private float desiredSpeedStep = -1; 971 972 private float extraDelay = 0.0f; 973 974 private float minReliableOperatingSpeed = 0.0f; 975 976 private float maxOperatingSpeed = 1.0f; 977 978 private NamedBean referenced = null; 979 private javax.swing.Timer stopTimer = null; 980 981 // --- Throttle command pacing / quantization --- 982 // Default minimum time between speed-setting commands issued by the distance/physics planners. 983 // The maintainers have found that sending more frequently than ~2/sec can become inaccurate 984 // due to delays through the chain and other concurrent traffic. 985 private static final int DEFAULT_MIN_COMMAND_INTERVAL_MS = 500; 986 private int minCommandIntervalMs = DEFAULT_MIN_COMMAND_INTERVAL_MS; 987 988 // Track the last speed-setting value actually issued by this class (after quantization). 989 // Do not rely on throttle.getSpeedSetting() to reflect what was finally sent to track. 990 private float lastIssuedSpeedSetting = -1.0f; 991 992 /** 993 * Set the minimum command interval (ms) used by the distance/physics 994 * planners. Values {@code <=} 0 revert to the default. Values less than 995 * DEFAULT_MIN_COMMAND_INTERVAL_MS are clamped up to the default. 996 * 997 * @param ms Minimum interval in milliseconds. 998 */ 999 public void setMinCommandIntervalMs(int ms) { 1000 if (ms <= 0) { 1001 minCommandIntervalMs = DEFAULT_MIN_COMMAND_INTERVAL_MS; 1002 } else { 1003 minCommandIntervalMs = Math.max(ms, DEFAULT_MIN_COMMAND_INTERVAL_MS); 1004 } 1005 } 1006 1007 private int getEffectiveMinCommandIntervalMs() { 1008 return Math.max(minCommandIntervalMs, DEFAULT_MIN_COMMAND_INTERVAL_MS); 1009 } 1010 1011 private static float quantizeToSpeedStep(DccThrottle t, float pct) { 1012 float v = clampPct(pct); 1013 if (t == null) 1014 return v; 1015 float inc; 1016 try { 1017 inc = t.getSpeedIncrement(); 1018 } catch (Throwable ex) { 1019 inc = 0.0f; 1020 } 1021 if (inc <= 0.0f) 1022 return v; 1023 // Round to nearest speed step. 1024 int steps = Math.round(v / inc); 1025 float q = steps * inc; 1026 // Ensure any non-zero request is at least one step. 1027 if (v > 0.0f && q < inc) 1028 q = inc; 1029 return clampPct(q); 1030 } 1031 1032 private float getEffectiveCurrentSpeedSetting() { 1033 if (lastIssuedSpeedSetting >= 0.0f) { 1034 return lastIssuedSpeedSetting; 1035 } 1036 if (_throttle != null) { 1037 return clampPct(_throttle.getSpeedSetting()); 1038 } 1039 return 0.0f; 1040 } 1041 1042 // Distance-based approach-to-min: optional stop-sensor hook (cleared in finishChange()). 1043 private Sensor approachStopSensor = null; 1044 private PropertyChangeListener approachStopSensorListener = null; 1045 1046 private long lastTimeTimerStarted = 0L; 1047 1048 /** 1049 * reset everything back to default once the change has finished. 1050 */ 1051 void finishChange() { 1052 // Remove any approach-stop sensor listener if present. 1053 if (approachStopSensor != null && approachStopSensorListener != null) { 1054 try { 1055 approachStopSensor.removePropertyChangeListener(approachStopSensorListener); 1056 } catch (Exception ex) { 1057 log.debug("finishChange: failed to remove approach stop sensor listener", ex); 1058 } 1059 } 1060 approachStopSensor = null; 1061 approachStopSensorListener = null; 1062 if (stopTimer != null) { 1063 stopTimer.stop(); 1064 } 1065 stopTimer = null; 1066 _throttle = null; 1067 distanceRemaining = 0; 1068 desiredSpeedStep = -1; 1069 extraDelay = 0.0f; 1070 minReliableOperatingSpeed = 0.0f; 1071 maxOperatingSpeed = 1.0f; 1072 referenced = null; 1073 lastIssuedSpeedSetting = -1.0f; 1074 synchronized (this) { 1075 distanceTravelled = 0; 1076 stepQueue = new LinkedList<>(); 1077 } 1078 _throttle = null; 1079 } 1080 1081 public void setExtraInitialDelay(float eDelay) { 1082 extraDelay = eDelay; 1083 } 1084 1085 public void setMinMaxLimits(float minReliableOperatingSpeed, float maxOperatingSpeed) { 1086 this.minReliableOperatingSpeed = minReliableOperatingSpeed; 1087 this.maxOperatingSpeed = maxOperatingSpeed; 1088 if (minReliableOperatingSpeed > maxOperatingSpeed) { 1089 log.warn("MaxOperatingSpeed [{}] < minReliableOperatingSpeed [{}] setting Max = Min", 1090 minReliableOperatingSpeed, maxOperatingSpeed); 1091 this.maxOperatingSpeed = this.minReliableOperatingSpeed; 1092 } 1093 } 1094 1095 /** 1096 * Set min/max throttle limits, optionally enforcing a scale km/h cap. If 1097 * maxSpeedScaleKmh == 0.0f, the percent maxOperatingSpeed takes precedence 1098 * (no effect). If maxSpeedScaleKmh {@code >} 0.0f, we convert the km/h cap 1099 * to an equivalent throttle% using the roster profile and the layout scale 1100 * ratio, then take the minimum of that and the percent cap. 1101 * 1102 * @param minReliableOperatingSpeed lowest throttle % the loco reliably 1103 * moves (0..1) 1104 * @param maxOperatingSpeed percent cap (0..1) 1105 * @param maxSpeedScaleKmh scale km/h cap; 0.0f means "unused" 1106 * @param layoutScaleRatio layout scale ratio (full-scale / model), 1107 * e.g. 87.0 for HO 1108 * @param isForward direction of travel 1109 */ 1110 public void setMinMaxLimitsKmh(float minReliableOperatingSpeed, 1111 float maxOperatingSpeed, 1112 float maxSpeedScaleKmh, 1113 float layoutScaleRatio, 1114 boolean isForward) { 1115 // Default to the percent cap 1116 float maxPct = maxOperatingSpeed; 1117 1118 // If a km/h cap is specified and we have speeds for this direction, convert to throttle% 1119 boolean dirHasProfile = isForward ? hasForwardSpeeds() : hasReverseSpeeds(); 1120 if (maxSpeedScaleKmh > 0.0f && dirHasProfile) { 1121 float safeScale = (layoutScaleRatio <= 0.0f) ? 1.0f : layoutScaleRatio; 1122 // Convert full-scale km/h -> model km/h -> model mm/s 1123 float modelKmh = maxSpeedScaleKmh / safeScale; 1124 float targetMms = modelKmh * 277.7778f; // 1 km/h = 277.7778 mm/s 1125 1126 float thrCapPct = getThrottleSetting(targetMms, isForward); 1127 if (thrCapPct > 0.0f) { 1128 maxPct = Math.min(maxOperatingSpeed, thrCapPct); 1129 } 1130 } 1131 1132 // Apply computed limits 1133 this.minReliableOperatingSpeed = minReliableOperatingSpeed; 1134 this.maxOperatingSpeed = maxPct; 1135 1136 // Guard: if min > max, clamp max to min (preserves previous method semantics) 1137 if (this.minReliableOperatingSpeed > this.maxOperatingSpeed) { 1138 log.warn("MaxOperatingSpeed [{}] < minReliableOperatingSpeed [{}]; setting Max = Min", 1139 this.maxOperatingSpeed, this.minReliableOperatingSpeed); 1140 this.maxOperatingSpeed = this.minReliableOperatingSpeed; 1141 } 1142 } 1143 1144 /** 1145 * Set speed of a throttle. 1146 * 1147 * @param t the throttle to set 1148 * @param blk the block used for length details 1149 * @param speed the speed to set 1150 */ 1151 public void changeLocoSpeed(DccThrottle t, Block blk, float speed) { 1152 if (blk == referenced && Float.compare(speed, desiredSpeedStep) == 0) { 1153 //log.debug("Already setting to desired speed step for this block"); 1154 return; 1155 } 1156 float blockLength = blk.getLengthMm(); 1157 if (blk == referenced) { 1158 distanceRemaining = distanceRemaining - getDistanceTravelled(_throttle.getIsForward(), _throttle.getSpeedSetting(), ((float) (System.nanoTime() - lastTimeTimerStarted) / 1000000000)); 1159 blockLength = distanceRemaining; 1160 //Not entirely reliable at this stage as the loco could still be running and not completed the calculation of the distance, this could result in an over run 1161 log.debug("Block passed is the same as we are currently processing"); 1162 } else { 1163 referenced = blk; 1164 } 1165 changeLocoSpeed(t, blockLength, speed); 1166 } 1167 1168 /** 1169 * Set speed of a throttle. 1170 * 1171 * @param t the throttle to set 1172 * @param sec the section used for length details 1173 * @param speed the speed to set 1174 * @param usePercentage the percentage of the block to be used for stopping 1175 */ 1176 @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "FE_FLOATING_POINT_EQUALITY", 1177 justification = "OK to compare floats, as even tiny differences should trigger update") 1178 public void changeLocoSpeed(DccThrottle t, Section sec, float speed, float usePercentage) { 1179 if (sec == referenced && speed == desiredSpeedStep) { 1180 log.debug("Already setting to desired speed step for this Section"); 1181 return; 1182 } 1183 float sectionLength = sec.getActualLength() * usePercentage; 1184 if (sec == referenced) { 1185 distanceRemaining = distanceRemaining - getDistanceTravelled(_throttle.getIsForward(), _throttle.getSpeedSetting(), ((float) (System.nanoTime() - lastTimeTimerStarted) / 1000000000)); 1186 sectionLength = distanceRemaining; 1187 //Not entirely reliable at this stage as the loco could still be running and not completed the calculation of the distance, this could result in an over run 1188 log.debug("Block passed is the same as we are currently processing"); 1189 } else { 1190 referenced = sec; 1191 } 1192 changeLocoSpeed(t, sectionLength, speed); 1193 } 1194 1195 /** 1196 * Set speed of a throttle. 1197 * 1198 * @param t the throttle to set 1199 * @param blk the block used for length details 1200 * @param speed the speed to set 1201 * @param usePercentage the percentage of the block to be used for stopping 1202 */ 1203 @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "FE_FLOATING_POINT_EQUALITY", 1204 justification = "OK to compare floats, as even tiny differences should trigger update") 1205 public void changeLocoSpeed(DccThrottle t, Block blk, float speed, float usePercentage) { 1206 if (blk == referenced && speed == desiredSpeedStep) { 1207 //if(log.isDebugEnabled()) log.debug("Already setting to desired speed step for this block"); 1208 return; 1209 } 1210 float blockLength = blk.getLengthMm() * usePercentage; 1211 if (blk == referenced) { 1212 distanceRemaining = distanceRemaining - getDistanceTravelled(_throttle.getIsForward(), _throttle.getSpeedSetting(), ((float) (System.nanoTime() - lastTimeTimerStarted) / 1000000000)); 1213 blockLength = distanceRemaining; 1214 //Not entirely reliable at this stage as the loco could still be running and not completed the calculation of the distance, this could result in an over run 1215 log.debug("Block passed is the same as we are currently processing"); 1216 } else { 1217 referenced = blk; 1218 } 1219 changeLocoSpeed(t, blockLength, speed); 1220 1221 } 1222 1223 /** 1224 * Set speed of a throttle to a speeed set by a float, using the section for 1225 * the length details 1226 * Set speed of a throttle. 1227 * 1228 * @param t the throttle to set 1229 * @param sec the section used for length details 1230 * @param speed the speed to set 1231 */ 1232 //@TODO if a section contains multiple blocks then we could calibrate the change of speed based upon the block status change. 1233 public void changeLocoSpeed(DccThrottle t, Section sec, float speed) { 1234 if (sec == referenced && Float.compare(speed, desiredSpeedStep) == 0) { 1235 log.debug("Already setting to desired speed step for this section"); 1236 return; 1237 } 1238 float sectionLength = sec.getActualLength(); 1239 log.debug("call to change speed via section {}", sec.getDisplayName()); 1240 if (sec == referenced) { 1241 distanceRemaining = distanceRemaining - getDistanceTravelled(_throttle.getIsForward(), _throttle.getSpeedSetting(), ((float) (System.nanoTime() - lastTimeTimerStarted) / 1000000000)); 1242 sectionLength = distanceRemaining; 1243 } else { 1244 referenced = sec; 1245 } 1246 1247 changeLocoSpeed(t, sectionLength, speed); 1248 } 1249 1250 /** 1251 * Set speed of a throttle. 1252 * 1253 * @param t the throttle to set 1254 * @param distance the distance in meters 1255 * @param requestedSpeed the speed to set 1256 */ 1257 public void changeLocoSpeed(DccThrottle t, float distance, float requestedSpeed) { 1258 float speed = 0.0f; 1259 log.debug("Call to change speed over specific distance: speed {} distance {}", requestedSpeed, distance); 1260 if (requestedSpeed > maxOperatingSpeed) { 1261 speed = maxOperatingSpeed; 1262 } else { 1263 speed = requestedSpeed; 1264 } 1265 if (Float.compare(speed, desiredSpeedStep) == 0) { 1266 // This requires no checks for min/max. 1267 log.debug("Already setting to desired speed step"); 1268 return; 1269 } 1270 log.debug("public change speed step by float {}", speed); 1271 log.debug("Desired Speed Step {} asked for {}", desiredSpeedStep, speed); 1272 1273 if (stopTimer != null) { 1274 log.debug("stop timer valid so will cancel"); 1275 cancelSpeedChange(); 1276 } 1277 _throttle = t; 1278 desiredSpeedStep = speed; 1279 1280 log.debug("Speed current {} required {} ", 1281 _throttle.getSpeedSetting(), speed); 1282 if (_throttle.getSpeedSetting() < speed) { 1283 log.debug("Going for acceleration"); 1284 } else { 1285 log.debug("Going for deceleration"); 1286 } 1287 1288 float adjSpeed = speed; 1289 boolean andStop = false; 1290 if (speed <= 0.0) { 1291 andStop = true; 1292 } 1293 if (speed < minReliableOperatingSpeed) { 1294 adjSpeed = minReliableOperatingSpeed; 1295 } 1296 log.debug("Speed[{}] adjSpeed[{}] MinSpeed[{}]", 1297 speed,adjSpeed, minReliableOperatingSpeed); 1298 1299 if (!andStop 1300 && (Float.compare(adjSpeed, t.getSpeedSetting()) == 0 1301 || (Math.round(adjSpeed/t.getSpeedIncrement()) == 1302 Math.round(t.getSpeedSetting()/t.getSpeedIncrement())))) { 1303 log.debug("Throttle and request speed setting are the same {} {} so will quit", speed, t.getSpeedSetting()); 1304 //Already at correct speed setting 1305 finishChange(); 1306 return; 1307 } 1308 calculateStepDetails(adjSpeed, distance, andStop); 1309 } 1310 1311 private List<SpeedSetting> testSteps = new ArrayList<>(); 1312 private boolean profileInTestMode = false; 1313 1314 void calculateStepDetails(float speedStep, float distance, boolean andStop) { 1315 1316 float stepIncrement = _throttle.getSpeedIncrement(); 1317 log.debug("Desired Speed Step {} asked for {}", desiredSpeedStep, speedStep); 1318 desiredSpeedStep = speedStep; 1319 log.debug("calculated current step {} required {} current {} increment {}", _throttle.getSpeedSetting(), speedStep, _throttle.getSpeedSetting(), stepIncrement); 1320 boolean increaseSpeed = false; 1321 if (_throttle.getSpeedSetting() < speedStep) { 1322 increaseSpeed = true; 1323 log.debug("Going for acceleration"); 1324 } else { 1325 log.debug("Going for deceleration"); 1326 } 1327 1328 if (distance <= 0) { 1329 log.debug("Distance is less than 0 {}", distance); 1330 _throttle.setSpeedSetting(speedStep); 1331 finishChange(); 1332 return; 1333 } 1334 1335 float calculatedDistance = distance; 1336 1337 if (stopTimer != null) { 1338 stopTimer.stop(); 1339 distanceRemaining = distance; 1340 } else { 1341 calculatedDistance = calculateInitialOverRun(distance); 1342 distanceRemaining = calculatedDistance; 1343 } 1344 if (distanceRemaining < 0.0f) { 1345 if (andStop) { 1346 _throttle.setSpeedSetting(0.0f); 1347 } else { 1348 _throttle.setSpeedSetting(speedStep); 1349 } 1350 log.warn("There is insufficient distance [{}] after adjustments, setting speed immediately", distanceRemaining); 1351 return; 1352 } 1353 1354 float calculatingStep = _throttle.getSpeedSetting(); 1355 if (increaseSpeed) { 1356 if (calculatingStep < minReliableOperatingSpeed) { 1357 calculatingStep = minReliableOperatingSpeed; 1358 } 1359 } 1360 1361 float endspd = 0; 1362 if (calculatingStep != 0.0 && desiredSpeedStep > 0) { // current speed 1363 if (_throttle.getIsForward()) { 1364 endspd = getForwardSpeed(desiredSpeedStep); 1365 } else { 1366 endspd = getReverseSpeed(desiredSpeedStep); 1367 } 1368 } else if (desiredSpeedStep != 0.0) { 1369 if (_throttle.getIsForward()) { 1370 endspd = getForwardSpeed(desiredSpeedStep); 1371 } else { 1372 endspd = getReverseSpeed(desiredSpeedStep); 1373 } 1374 } 1375 1376 boolean calculated = false; 1377 while (!calculated) { 1378 float spd = 0; 1379 if (calculatingStep != 0.0) { // current speed 1380 if (_throttle.getIsForward()) { 1381 spd = getForwardSpeed(calculatingStep); 1382 } else { 1383 spd = getReverseSpeed(calculatingStep); 1384 } 1385 } 1386 1387 log.debug("end spd {} spd {}", endspd, spd); 1388 double avgSpeed = Math.abs((endspd + spd) * 0.5); 1389 log.debug("avg Speed {}", avgSpeed); 1390 1391 double time = (calculatedDistance / avgSpeed); //in seconds 1392 time = time * 1000; //covert it to milli seconds 1393 float speeddiff = calculatingStep - desiredSpeedStep; 1394 if (increaseSpeed) { 1395 speeddiff = desiredSpeedStep - calculatingStep; 1396 } 1397 float noSteps = speeddiff / stepIncrement; 1398 log.debug("Speed diff {} number of Steps {} step increment {}", speeddiff, noSteps, stepIncrement); 1399 1400 int timePerStep = (int) (time / noSteps); 1401 if (timePerStep < 0) { 1402 log.error("Time per speed went to zero or below, setting finale speed immediatly."); 1403 if (_throttle != null) { 1404 addSpeedStepItem(calculated,new SpeedSetting(desiredSpeedStep, 10, andStop)); 1405 setNextStep(); 1406 } 1407 break; 1408 } 1409 float calculatedStepInc = stepIncrement; 1410 boolean lastStep = false; 1411 if (Math.abs(speeddiff) > (stepIncrement * 2)) { 1412 //We do not get reliable time results if the duration per speed step is less than 500ms 1413 //therefore we calculate how many speed steps will fit in to 750ms. 1414 if (timePerStep <= 500 && timePerStep > 0) { 1415 float newTime = 750.0f; 1416 float tmp =(float) Math.floor(newTime / timePerStep); 1417 // To avoid the lack of a stub ensure resultant speed is less than final speed by at least a step. 1418 if (increaseSpeed) { 1419 while (desiredSpeedStep - ( calculatingStep + (stepIncrement * tmp)) <= stepIncrement) { 1420 tmp = tmp - 1; 1421 } 1422 1423 if (tmp > 0 && calculatedDistance - getDistanceTravelled(_throttle.getIsForward(), 1424 calculatingStep + (stepIncrement * tmp), 1425 ((float) (newTime / 1000.0))) > 0) { 1426 calculatedStepInc = stepIncrement * tmp; 1427 timePerStep = (int)newTime; 1428 } 1429 } else { 1430 while (calculatingStep - (stepIncrement * tmp) - desiredSpeedStep <= stepIncrement) { 1431 tmp = tmp - 1; 1432 } 1433 if ( tmp > 0 && (calculatedDistance 1434 - getDistanceTravelled(_throttle.getIsForward(), 1435 calculatingStep - (stepIncrement * tmp), 1436 ((float) (newTime / 1000.0)))) > 0) { 1437 calculatedStepInc = stepIncrement * tmp; 1438 timePerStep = (int)newTime; 1439 } 1440 } 1441 log.debug("time per step was {} no of increments in 750 ms is {} new step increment in {}", timePerStep, tmp, calculatedStepInc); 1442 } 1443 } else { 1444 // last bit calculate duration from distance remaining 1445 if (increaseSpeed && calculatingStep == 0) { 1446 calculatingStep+=calculatedStepInc; 1447 } 1448 timePerStep = Math.round(calculatedDistance/getSpeed(calculatingStep,_throttle.getIsForward())*1000); 1449 if (!increaseSpeed) { 1450 calculatedStepInc = calculatingStep - desiredSpeedStep; 1451 } else { 1452 calculatedStepInc = desiredSpeedStep - calculatingStep ; 1453 } 1454 lastStep=true; 1455 } 1456 calculatedStepInc=Math.abs(calculatedStepInc); 1457 log.debug("per interval {}, increase {} lastStep {}", timePerStep, increaseSpeed,lastStep); 1458 //Calculate the new speed setting 1459 if (increaseSpeed) { 1460 //if (calculatingStep + calculatedStepInc == desiredSpeedStep) { 1461 if (lastStep) { 1462 SpeedSetting ss = new SpeedSetting(calculatingStep, timePerStep, andStop); 1463 addSpeedStepItem(calculated,ss); 1464 calculated = true; 1465 if (!andStop) { calculatingStep = desiredSpeedStep;timePerStep=2;} 1466 else { 1467 calculatingStep = 0.0f;timePerStep=2; 1468 } 1469 ss = new SpeedSetting(calculatingStep, timePerStep, andStop); 1470 addSpeedStepItem(calculated,ss); 1471 if (stopTimer == null) { 1472 setNextStep(); 1473 } 1474 break; 1475 } 1476 calculatingStep = calculatingStep + calculatedStepInc; 1477 } else { 1478 if (lastStep) { 1479 SpeedSetting ss = new SpeedSetting(calculatingStep, timePerStep, andStop); 1480 addSpeedStepItem(calculated,ss); 1481 calculated = true; 1482 if (!andStop) { calculatingStep = desiredSpeedStep;timePerStep=2;} 1483 else { 1484 calculatingStep = 0.0f;timePerStep=2; 1485 } 1486 ss = new SpeedSetting(calculatingStep, timePerStep, andStop); 1487 addSpeedStepItem(calculated,ss); 1488 if (stopTimer == null) { //If this is the first time round then kick off the speed change 1489 setNextStep(); 1490 } 1491 break; 1492 } 1493 calculatingStep = calculatingStep - calculatedStepInc; 1494 } 1495 SpeedSetting ss = new SpeedSetting(calculatingStep, timePerStep, andStop); 1496 addSpeedStepItem(calculated,ss); 1497 if (stopTimer == null) { //If this is the first time round then kick off the speed change 1498 setNextStep(); 1499 } 1500 if (calculated) { 1501 if (andStop) { 1502 ss = new SpeedSetting(0.0f, 10, andStop); 1503 } else { 1504 ss = new SpeedSetting(desiredSpeedStep, 10, andStop); 1505 } 1506 addSpeedStepItem(calculated,ss); } 1507 // The throttle can disappear during a stop situation 1508 if (_throttle != null) { 1509 calculatedDistance = calculatedDistance - getDistanceTravelled(_throttle.getIsForward(), calculatingStep, ((float) (timePerStep / 1000.0))); 1510 } else { 1511 log.warn("Throttle destroyed before zero length[{}] remaining.",calculatedDistance); 1512 calculatedDistance = 0; 1513 } 1514 1515 if (calculatedDistance <= 0 && !calculated) { 1516 log.warn("distance remaining is now 0, but we have not reached desired speed setting {} v {}", desiredSpeedStep, calculatingStep); 1517 calculated = true; 1518 } 1519 } 1520 } 1521 1522 private void addSpeedStepItem(Boolean calculated, SpeedSetting ss) { 1523 synchronized (this) { 1524 stepQueue.addLast(ss); 1525 if (profileInTestMode) { 1526 testSteps.add(ss); 1527 } 1528 if (ss.andStop && calculated) { 1529 ss = new SpeedSetting( 0.0f, 0, ss.andStop); 1530 stepQueue.addLast(ss); 1531 if (profileInTestMode) { 1532 testSteps.add(ss); 1533 } 1534 } 1535 } 1536 } 1537 1538 //The bit with the distance is not used 1539 float calculateInitialOverRun(float distance) { 1540 log.debug("Stop timer not configured so will add overrun {}", distance); 1541 if (_throttle.getIsForward()) { 1542 float extraAsDouble = (getOverRunTimeForward() + extraDelay) / 1000; 1543 if (log.isDebugEnabled()) { 1544 log.debug("Over run time to remove (Forward) {} {}", getOverRunTimeForward(), extraAsDouble); 1545 } 1546 float olddistance = getDistanceTravelled(true, _throttle.getSpeedSetting(), extraAsDouble); 1547 distance = distance - olddistance; 1548 //time = time-getOverRunTimeForward(); 1549 //time = time-(extraAsDouble*1000); 1550 } else { 1551 float extraAsDouble = (getOverRunTimeReverse() + extraDelay) / 1000; 1552 if (log.isDebugEnabled()) { 1553 log.debug("Over run time to remove (Reverse) {} {}", getOverRunTimeReverse(), extraAsDouble); 1554 } 1555 float olddistance = getDistanceTravelled(false, _throttle.getSpeedSetting(), extraAsDouble); 1556 distance = distance - olddistance; 1557 //time = time-getOverRunTimeReverse(); 1558 //time = time-(extraAsDouble*1000); 1559 } 1560 log.debug("Distance remaining {}", distance); 1561 //log.debug("Time after overrun removed " + time); 1562 return distance; 1563 1564 } 1565 1566 /** 1567 * This method is called to cancel the existing change in speed. 1568 */ 1569 public void cancelSpeedChange() { 1570 if (stopTimer != null && stopTimer.isRunning()) { 1571 stopTimer.stop(); 1572 } 1573 finishChange(); 1574 } 1575 1576 synchronized void setNextStep() { 1577 //if (profileInTestMode) { 1578 // return; 1579 //} 1580 if (stepQueue.isEmpty()) { 1581 log.debug("No more results"); 1582 finishChange(); 1583 return; 1584 } 1585 SpeedSetting ss = stepQueue.getFirst(); 1586 if (ss.getDuration() == 0) { 1587 if (ss.getAndStop()) { 1588 _throttle.setSpeedSetting(0.0f); 1589 } else { 1590 _throttle.setSpeedSetting(desiredSpeedStep); 1591 } 1592 finishChange(); 1593 return; 1594 } 1595 if (stopTimer != null) { 1596 //Reduce the distanceRemaining and calculate the distance travelling 1597 float distanceTravelledThisStep = getDistanceTravelled(_throttle.getIsForward(), 1598 getEffectiveCurrentSpeedSetting(), ((float) (stopTimer.getDelay() / 1000.0))); 1599 distanceTravelled = distanceTravelled + distanceTravelledThisStep; 1600 distanceRemaining = distanceRemaining - distanceTravelledThisStep; 1601 } 1602 stepQueue.removeFirst(); 1603 lastIssuedSpeedSetting = ss.getSpeedStep(); 1604 _throttle.setSpeedSetting(lastIssuedSpeedSetting); 1605 stopTimer = new javax.swing.Timer(ss.getDuration(), (java.awt.event.ActionEvent e) -> { 1606 setNextStep(); 1607 }); 1608 stopTimer.setRepeats(false); 1609 lastTimeTimerStarted = System.nanoTime(); 1610 stopTimer.start(); 1611 1612 } 1613 1614 private LinkedList<SpeedSetting> stepQueue = new LinkedList<>(); 1615 1616 public static class SpeedSetting { 1617 1618 private float step = 0.0f; 1619 private int duration = 0; 1620 private boolean andStop; 1621 1622 public SpeedSetting(float step, int duration, boolean andStop) { 1623 log.debug("Adding step {} duration {} andStop{}", step, duration, andStop); 1624 this.step = step; 1625 this.duration = duration; 1626 this.andStop = andStop; 1627 } 1628 1629 public float getSpeedStep() { 1630 return step; 1631 } 1632 1633 public int getDuration() { 1634 return duration; 1635 } 1636 1637 public boolean getAndStop() { 1638 return andStop; 1639 } 1640 } 1641 1642 /* 1643 * The follow deals with the storage and loading of the speed profile for a roster entry. 1644 */ 1645 public void store(Element e) { 1646 Element d = new Element("speedprofile"); 1647 d.addContent(new Element("overRunTimeForward").addContent(Float.toString(getOverRunTimeForward()))); 1648 d.addContent(new Element("overRunTimeReverse").addContent(Float.toString(getOverRunTimeReverse()))); 1649 Element s = new Element("speeds"); 1650 speeds.keySet().stream().forEachOrdered( i -> { 1651 Element ss = new Element("speed"); 1652 ss.addContent(new Element("step").addContent(Integer.toString(i))); 1653 ss.addContent(new Element("forward").addContent(Float.toString(speeds.get(i).getForwardSpeed()))); 1654 ss.addContent(new Element("reverse").addContent(Float.toString(speeds.get(i).getReverseSpeed()))); 1655 s.addContent(ss); 1656 }); 1657 d.addContent(s); 1658 e.addContent(d); 1659 } 1660 1661 public void load(Element e) { 1662 try { 1663 setOverRunTimeForward(Float.parseFloat(e.getChild("overRunTimeForward").getText())); 1664 } catch (NumberFormatException ex) { 1665 log.error("Over run Error For {}", _re.getId()); 1666 } 1667 try { 1668 setOverRunTimeReverse(Float.parseFloat(e.getChild("overRunTimeReverse").getText())); 1669 } catch (NumberFormatException ex) { 1670 log.error("Over Run Error Rev {}", _re.getId()); 1671 } 1672 e.getChild("speeds").getChildren("speed").forEach( spd -> { 1673 try { 1674 String step = spd.getChild("step").getText(); 1675 String forward = spd.getChild("forward").getText(); 1676 String reverse = spd.getChild("reverse").getText(); 1677 float forwardSpeed = Float.parseFloat(forward); 1678 if (forwardSpeed > 0.0f) { 1679 _hasForwardSpeeds = true; 1680 } 1681 float reverseSpeed = Float.parseFloat(reverse); 1682 if (reverseSpeed > 0.0f) { 1683 _hasReverseSpeeds = true; 1684 } 1685 setSpeed(Integer.parseInt(step), forwardSpeed, reverseSpeed); 1686 } catch (NumberFormatException ex) { 1687 log.error("Not loaded {}", ex.getMessage()); 1688 } 1689 }); 1690 } 1691 1692 public static class SpeedStep { 1693 1694 private float forward = 0.0f; 1695 private float reverse = 0.0f; 1696 1697 /** 1698 * Create a new SpeedStep, Reverse and Forward speeds are 0. 1699 */ 1700 public SpeedStep() { 1701 } 1702 1703 /** 1704 * Set the Forward speed for the step. 1705 * @param speed the forward speed for the Step. 1706 */ 1707 public void setForwardSpeed(float speed) { 1708 forward = speed; 1709 } 1710 1711 /** 1712 * Set the Reverse speed for the step. 1713 * @param speed the reverse speed for the Step. 1714 */ 1715 public void setReverseSpeed(float speed) { 1716 reverse = speed; 1717 } 1718 1719 /** 1720 * Get the Forward Speed for the Step. 1721 * @return the forward speed. 1722 */ 1723 public float getForwardSpeed() { 1724 return forward; 1725 } 1726 1727 /** 1728 * Get the Reverse Speed for the Step. 1729 * @return the reverse speed. 1730 */ 1731 public float getReverseSpeed() { 1732 return reverse; 1733 } 1734 1735 @Override 1736 public boolean equals(Object obj) { 1737 if (this == obj) { 1738 return true; 1739 } 1740 if (obj == null || getClass() != obj.getClass()) { 1741 return false; 1742 } 1743 SpeedStep ss = (SpeedStep) obj; 1744 return Float.compare(ss.getForwardSpeed(), forward) == 0 1745 && Float.compare(ss.getReverseSpeed(), reverse) == 0; 1746 } 1747 1748 @Override 1749 public int hashCode() { 1750 int result = 17; 1751 result = 31 * result + Float.floatToIntBits(forward); 1752 result = 31 * result + Float.floatToIntBits(reverse); 1753 return result; 1754 } 1755 1756 } 1757 1758 /** 1759 * Get the number of SpeedSteps. 1760 * If there are too few SpeedSteps, it may be difficult to get reasonable 1761 * distances and speeds over a large range of throttle settings. 1762 * @return the number of Speed Steps in the profile. 1763 */ 1764 public int getProfileSize() { 1765 return speeds.size(); 1766 } 1767 1768 public TreeMap<Integer, SpeedStep> getProfileSpeeds() { 1769 return speeds; 1770 } 1771 1772 /** 1773 * Get the throttle setting to achieve a track speed 1774 * 1775 * @param speed desired track speed in mm/sec 1776 * @param isForward direction 1777 * @return throttle setting 1778 */ 1779 public float getThrottleSetting(float speed, boolean isForward) { 1780 if ((isForward && !_hasForwardSpeeds) || (!isForward && !_hasReverseSpeeds)) { 1781 return 0.0f; 1782 } 1783 int slowerKey = 0; 1784 float slowerValue = 0; 1785 float fasterKey; 1786 float fasterValue; 1787 Entry<Integer, SpeedStep> entry = speeds.firstEntry(); 1788 if (entry == null) { 1789 log.warn("There is no speedprofile entries for [{}]", this.getRosterEntry().getId()); 1790 return (0.0f); 1791 } 1792 // search through table until end or the entry is greater than 1793 // what we are looking for. This leaves the previous lower value in key. and slower 1794 // Note there may be zero values interspersed in the tree 1795 if (isForward) { 1796 fasterKey = entry.getKey(); 1797 fasterValue = entry.getValue().getForwardSpeed(); 1798 while (entry != null && entry.getValue().getForwardSpeed() < speed) { 1799 slowerKey = entry.getKey(); 1800 float value = entry.getValue().getForwardSpeed(); 1801 if (value > 0.0f) { 1802 slowerValue = value; 1803 } 1804 entry = speeds.higherEntry(slowerKey); 1805 if (entry != null) { 1806 fasterKey = entry.getKey(); 1807 value = entry.getValue().getForwardSpeed(); 1808 if (value > 0.0f) { 1809 fasterValue = value; 1810 } 1811 } 1812 } 1813 } else { 1814 fasterKey = entry.getKey(); 1815 fasterValue = entry.getValue().getReverseSpeed(); 1816 while (entry != null && entry.getValue().getReverseSpeed() < speed) { 1817 slowerKey = entry.getKey(); 1818 float value = entry.getValue().getReverseSpeed(); 1819 if (value > 0.0f) { 1820 slowerValue = value; 1821 } 1822 entry = speeds.higherEntry(slowerKey); 1823 if (entry != null) { 1824 fasterKey = entry.getKey(); 1825 value = entry.getValue().getReverseSpeed(); 1826 if (value > 0.0f) { 1827 fasterValue = value; 1828 } 1829 } 1830 } 1831 } 1832 log.trace("slowerKey={}, slowerValue={} fasterKey={} fasterValue={} for speed={}", 1833 slowerKey, slowerValue, fasterKey, fasterValue, speed); 1834 if (entry == null) { 1835 // faster does not exists use slower... 1836 if (slowerValue <= 0.0f) { // neither does slower 1837 return (0.0f); 1838 } 1839 1840 // extrapolate 1841 float key = slowerKey * speed / slowerValue; 1842 if (key < 1000.0f) { 1843 return key / 1000.0f; 1844 } else { 1845 return 1.0f; 1846 } 1847 } 1848 if (Float.compare(slowerValue, speed) == 0 || fasterValue <= slowerValue) { 1849 return slowerKey / 1000.0f; 1850 } 1851 if (slowerValue <= 0.0f) { // no entry had a slower speed, therefore key is invalid 1852 slowerKey = 0; 1853 if (fasterValue <= 0.0f) { // neither is there a faster speed 1854 return (0.0f); 1855 } 1856 } 1857 // we need to interpolate 1858 float ratio = (speed - slowerValue) / (fasterValue - slowerValue); 1859 return (slowerKey + ((fasterKey - slowerKey) * ratio)) / 1000.0f; 1860 } 1861 1862 /** 1863 * Get track speed in millimeters per second from throttle setting 1864 * 1865 * @param speedStep throttle setting 1866 * @param isForward direction 1867 * @return track speed 1868 */ 1869 public float getSpeed(float speedStep, boolean isForward) { 1870 if (speedStep < 0.00001f) { 1871 return 0.0f; 1872 } 1873 float speed; 1874 if (isForward) { 1875 speed = getForwardSpeed(speedStep); 1876 } else { 1877 speed = getReverseSpeed(speedStep); 1878 } 1879 return speed; 1880 } 1881 1882 /** 1883 * Physics-based acceleration to a target throttle percent (0..1). Builds 1884 * and runs a throttle/time schedule using this profile's 1885 * stepQueue/stopTimer. 1886 * 1887 * @param t The DccThrottle to drive (must not be null) 1888 * @param targetThrottlePct Desired throttle percent [0..1] 1889 * @param driverPowerPercent Driver power/regulator percent [0..1] 1890 * (limits applied power/TE during 1891 * acceleration) 1892 * @param additionalWeightTonnes Extra consist mass in metric tonnes 1893 * ({@code >=} 0) 1894 * @param rollingResistanceCoeff Rolling resistance coefficient c_rr 1895 * ({@code >=} 0), e.g., ~0.002 1896 * @param layoutScaleRatio Layout scale ratio (full-scale / model), 1897 * e.g., 87.0 for HO 1898 * @param speedFactor If {@code >} 0, throttle commands are 1899 * divided by this factor before enqueuing 1900 */ 1901 public void runPhysicsAccelerationToTargetThrottle( 1902 jmri.DccThrottle t, 1903 float targetThrottlePct, 1904 float driverPowerPercent, 1905 float additionalWeightTonnes, 1906 float rollingResistanceCoeff, 1907 float layoutScaleRatio, 1908 float speedFactor) { 1909 if (t == null) { 1910 log.warn("runPhysicsAccelerationToTargetThrottle called with null throttle; ignoring."); 1911 return; 1912 } 1913 float speedFactorSafe = (speedFactor > 0.0f) ? speedFactor : 1.0f; 1914 float driverPct = clampPct(driverPowerPercent); 1915 float crr = (rollingResistanceCoeff < 0.0f) ? 0.0f : rollingResistanceCoeff; 1916 float scaleRatio = (layoutScaleRatio <= 0.0f) ? 1.0f : layoutScaleRatio; 1917 1918 final boolean forward = t.getIsForward(); 1919 final float minPct = this.minReliableOperatingSpeed; 1920 final float maxPct = this.maxOperatingSpeed; 1921 1922 // Kill any running timer and clear queue (do NOT call finishChange() which resets limits) 1923 if (stopTimer != null) { 1924 stopTimer.stop(); 1925 stopTimer = null; 1926 } 1927 synchronized (this) { 1928 stepQueue = new LinkedList<>(); 1929 } 1930 _throttle = t; 1931 lastIssuedSpeedSetting = quantizeToSpeedStep(_throttle, clampPct(_throttle.getSpeedSetting())); 1932 1933 float thrNow = lastIssuedSpeedSetting; 1934 float v0_mms = getSpeed(thrNow, forward); 1935 float vTarget_mms = getSpeed(clampPct(targetThrottlePct), forward); 1936 1937 float v0_fs = (v0_mms / 1000.0f) * scaleRatio; 1938 float vTarget_fs = (vTarget_mms / 1000.0f) * scaleRatio; 1939 1940 float vMin_mms = getSpeed(Math.max(0.0f, minPct), forward); 1941 float vMin_fs = (vMin_mms / 1000.0f) * scaleRatio; 1942 if (vTarget_fs < vMin_fs) 1943 vTarget_fs = vMin_fs; 1944 if (v0_fs < vMin_fs) 1945 v0_fs = vMin_fs; 1946 1947 float vCap_fs_roster = Float.POSITIVE_INFINITY; 1948 try { 1949 float kmhRoster = (_re != null) ? _re.getPhysicsMaxSpeedKmh() : 0.0f; 1950 if (kmhRoster > 0.0f) 1951 vCap_fs_roster = kmhRoster / 3.6f; 1952 } catch (Throwable ex) { 1953 log.debug("runPhysicsAccelerationToTargetThrottle: could not read roster max speed cap", ex); 1954 } 1955 vTarget_fs = Math.min(vTarget_fs, vCap_fs_roster); 1956 1957 float massKg = 1000.0f; 1958 float powerW = 0.0f; 1959 float teN = 0.0f; 1960 boolean mechTransmission = false; 1961 boolean isSteam = false; 1962 try { 1963 float rosterKg = (_re != null) ? _re.getPhysicsWeightKg() : 0.0f; 1964 float extraKg = Math.max(0.0f, additionalWeightTonnes) * 1000.0f; 1965 massKg = Math.max(1.0f, rosterKg + extraKg); 1966 powerW = (_re != null) ? (_re.getPhysicsPowerKw() * 1000.0f) : 0.0f; 1967 teN = (_re != null) ? (_re.getPhysicsTractiveEffortKn() * 1000.0f) : 0.0f; 1968 mechTransmission = (_re != null) && _re.isPhysicsMechanicalTransmission(); 1969 jmri.jmrit.roster.RosterEntry.TractionType tt = 1970 (_re != null) ? _re.getPhysicsTractionType() 1971 : jmri.jmrit.roster.RosterEntry.TractionType.DIESEL_ELECTRIC; 1972 isSteam = (tt == jmri.jmrit.roster.RosterEntry.TractionType.STEAM); 1973 } catch (Throwable ex) { 1974 log.warn("RosterEntry missing physics fields; falling back to immediate set.", ex); 1975 } 1976 1977 final float powerExpSteam = 0.85f; 1978 float alphaPower = 1979 isSteam ? (driverPct <= 0.0f ? 0.0f : (float) Math.pow(driverPct, powerExpSteam)) : driverPct; 1980 float alphaTE = driverPct <= 0.0f ? 0.0f : driverPct; 1981 float P_avail = powerW * alphaPower; 1982 float TE_avail = teN * alphaTE; 1983 1984 final int internalSliceMs = 50; 1985 final float dt = internalSliceMs / 1000.0f; 1986 final int minCmdMs = getEffectiveMinCommandIntervalMs(); 1987 1988 java.util.LinkedList<SpeedSetting> plan = new java.util.LinkedList<>(); 1989 1990 float v_fs = v0_fs; 1991 1992 final float[] gearFsMps = new float[]{ 1993 15f * 0.44704f, 1994 27f * 0.44704f, 1995 41f * 0.44704f 1996 }; 1997 boolean[] gearPauseDone = new boolean[gearFsMps.length]; 1998 for (int gi = 0; gi < gearPauseDone.length; gi++) { 1999 gearPauseDone[gi] = (v_fs >= gearFsMps[gi]); 2000 } 2001 2002 // Bucket accumulator for rate limiting. 2003 int bucketMs = 0; 2004 float bucketSpeedTime = 0.0f; // sum(mms * seconds) 2005 2006 int safety = 0; 2007 while (v_fs < vTarget_fs && safety < 10000) { 2008 float v_guard = Math.max(0.01f, v_fs); 2009 float F_power = (P_avail > 0.0f) ? (P_avail / v_guard) : 0.0f; 2010 float F_drive = (TE_avail > 0.0f) ? Math.min(TE_avail, F_power) : F_power; 2011 2012 final float g = 9.80665f; 2013 float F_rr = crr * massKg * g; 2014 float a_fs = (F_drive - F_rr) / massKg; 2015 if (a_fs < 0.0f) 2016 a_fs = 0.0f; 2017 2018 float stepDt = dt; 2019 float v_next_fs = v_fs + a_fs * stepDt; 2020 boolean finalStep = false; 2021 if (a_fs > 0.0f && v_next_fs > vTarget_fs) { 2022 stepDt = Math.max(0.001f, (vTarget_fs - v_fs) / a_fs); 2023 finalStep = true; 2024 v_next_fs = v_fs + a_fs * stepDt; 2025 } 2026 2027 // Gear-change pause (coast) if mechanical transmission and crossing a threshold 2028 boolean pausedThisSlice = false; 2029 if (mechTransmission) { 2030 for (int gi = 0; gi < gearFsMps.length; gi++) { 2031 if (!gearPauseDone[gi]) { 2032 float sTrig = gearFsMps[gi]; 2033 if ((vTarget_fs >= sTrig) && (v_fs < sTrig) && (v_next_fs >= sTrig)) { 2034 final float pauseSec = 3.5f; 2035 float left = pauseSec; 2036 float aCoast_fs = -(crr * g); 2037 while (left > 0.0f) { 2038 float chunk = Math.min(dt, left); 2039 float v_next_coast = v_fs + aCoast_fs * chunk; 2040 if (v_next_coast < vMin_fs) 2041 v_next_coast = vMin_fs; 2042 float v_mid_coast_fs = 0.5f * (v_fs + v_next_coast); 2043 float v_mid_model_ms = v_mid_coast_fs / scaleRatio; 2044 float v_mid_mms = v_mid_model_ms * 1000.0f; 2045 2046 int ms = Math.max(1, Math.round(chunk * 1000.0f)); 2047 bucketMs += ms; 2048 bucketSpeedTime += v_mid_mms * (ms / 1000.0f); 2049 2050 if (bucketMs >= minCmdMs) { 2051 float bucketSec = bucketMs / 1000.0f; 2052 float avgMms = (bucketSec > 0.0f) ? (bucketSpeedTime / bucketSec) : 0.0f; 2053 float thr = throttleForSpeedMms(avgMms, forward, minPct, maxPct); 2054 thr = clampPct(thr / speedFactorSafe); 2055 thr = quantizeToSpeedStep(_throttle, thr); 2056 plan.add(new SpeedSetting(thr, bucketMs, false)); 2057 bucketMs = 0; 2058 bucketSpeedTime = 0.0f; 2059 } 2060 2061 v_fs = v_next_coast; 2062 left -= chunk; 2063 safety++; 2064 if (safety >= 10000) 2065 break; 2066 } 2067 gearPauseDone[gi] = true; 2068 pausedThisSlice = true; 2069 break; 2070 } 2071 } 2072 } 2073 } 2074 if (pausedThisSlice) { 2075 continue; 2076 } 2077 2078 float v_mid_fs = 0.5f * (v_fs + v_next_fs); 2079 float v_mid_model_ms = v_mid_fs / scaleRatio; 2080 float v_mid_mms = v_mid_model_ms * 1000.0f; 2081 2082 int ms = Math.max(1, Math.round(stepDt * 1000.0f)); 2083 bucketMs += ms; 2084 bucketSpeedTime += v_mid_mms * (ms / 1000.0f); 2085 2086 // flush bucket 2087 if (bucketMs >= minCmdMs || finalStep) { 2088 float bucketSec = bucketMs / 1000.0f; 2089 float avgMms = (bucketSec > 0.0f) ? (bucketSpeedTime / bucketSec) : 0.0f; 2090 float thr = throttleForSpeedMms(avgMms, forward, minPct, maxPct); 2091 thr = clampPct(thr / speedFactorSafe); 2092 thr = quantizeToSpeedStep(_throttle, thr); 2093 plan.add(new SpeedSetting(thr, bucketMs, false)); 2094 bucketMs = 0; 2095 bucketSpeedTime = 0.0f; 2096 } 2097 2098 v_fs = v_next_fs; 2099 safety++; 2100 if (finalStep) 2101 break; 2102 } 2103 2104 // Flush any remaining bucket content 2105 if (bucketMs > 0) { 2106 float bucketSec = bucketMs / 1000.0f; 2107 float avgMms = (bucketSec > 0.0f) ? (bucketSpeedTime / bucketSec) : 0.0f; 2108 float thr = throttleForSpeedMms(avgMms, forward, minPct, maxPct); 2109 thr = clampPct(thr / speedFactorSafe); 2110 thr = quantizeToSpeedStep(_throttle, thr); 2111 plan.add(new SpeedSetting(thr, bucketMs, false)); 2112 } 2113 2114 if (plan.isEmpty()) { 2115 float thrFinal = throttleForSpeedMms(vTarget_mms, forward, minPct, maxPct); 2116 thrFinal = clampPct(thrFinal / speedFactorSafe); 2117 thrFinal = quantizeToSpeedStep(_throttle, thrFinal); 2118 lastIssuedSpeedSetting = thrFinal; 2119 _throttle.setSpeedSetting(thrFinal); 2120 return; 2121 } 2122 2123 synchronized (this) { 2124 for (SpeedSetting ss : plan) { 2125 stepQueue.addLast(ss); 2126 if (profileInTestMode) 2127 testSteps.add(ss); 2128 } 2129 } 2130 if (stopTimer == null) { 2131 setNextStep(); 2132 } 2133} 2134 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(RosterSpeedProfile.class); 2135 2136}