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