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