001package jmri.jmrit.dispatcher; 002 003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 004import java.beans.PropertyChangeEvent; 005import java.beans.PropertyChangeListener; 006import java.util.LinkedList; 007 008import javax.annotation.CheckForNull; 009 010import jmri.*; 011import jmri.implementation.SignalSpeedMap; 012import jmri.jmrit.dispatcher.ActiveTrain.TrainDetection; 013import jmri.jmrit.roster.RosterEntry; 014import jmri.util.swing.JmriJOptionPane; 015 016/** 017 * This class holds information and options for an ActiveTrain when it is 018 * running in AUTOMATIC mode. It is an extension to Active Train for automatic 019 * running. 020 * <p> 021 * This class implements logic that follows a train around a layout. Train 022 * follows signals, provided the next Section is allocated to it, and its 023 * ActiveTrain's status is RUNNING. 024 * <p> 025 * This class is linked via its parent ActiveTrain object. 026 * <p> 027 * This file is part of JMRI. 028 * <p> 029 * JMRI is open source software; you can redistribute it and/or modify it under 030 * the terms of version 2 of the GNU General Public License as published by the 031 * Free Software Foundation. See the "COPYING" file for a copy of this license. 032 * <p> 033 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY 034 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 035 * A PARTICULAR PURPOSE. See the GNU General Public License for more details. 036 * <p> 037 * The AutoEngineer sub class is based in part on code by Pete Cressman 038 * contained in Warrants.java 039 * 040 * @author Dave Duchamp Copyright (C) 2010-2011 041 */ 042public class AutoActiveTrain implements ThrottleListener { 043 044 /** 045 * Create an AutoActiveTrain. 046 * 047 * @param at the train to automate 048 */ 049 public AutoActiveTrain(ActiveTrain at) { 050 _activeTrain = at; 051 at.setAutoActiveTrain(this); 052 _autoTrainAction = new AutoTrainAction(this); 053 _lbManager = InstanceManager.getDefault(jmri.jmrit.display.layoutEditor.LayoutBlockManager.class); 054 // listen for additions in our allocated section table 055 at.addPropertyChangeListener("sectionallocated",this::handleAnotherSectionAllocatedChange); 056 } 057 058 /* Speed aspects as defined by Douglas A. Kerr - "Rail Signal Aspects and Indications" 059 * http://dougkerr.net/Pumpkin/articles/Rail_signal_aspects.pdf (from Pete Cressman) 060 */ 061 // public static final int SPEED_MASK = 0x07; // least significant 3 bits 062 public static final int STOP_SPEED = 0x01; // No Speed 063 public static final int RESTRICTED_SPEED = 0x02; // Train able to stop within 1/2 visual range (10mph) 064 public static final int SLOW_SPEED = 0x03; // Typically 15 mph (25% of NORMAL) 065 public static final int MEDIUM_SPEED = 0x04; // Typically 30 mph (40% of NORMAL) 066 public static final int LIMITED_SPEED = 0x05; // Typically 40-45 mph (65% of NORMAL) 067 public static final int NORMAL_SPEED = 0x06; // Varies with road and location 068 public static final int MAXIMUM_SPEED = 0x07; // "full" throttle 069 070 private final Float[] _speedRatio = {-1.0F, 0.0F, 0.25F, 0.35F, 0.50F, 0.65F, 0.8F, 1.15F}; 071 072 /* The ramp rates below are in addition to what the decoder itself does 073 */ 074 public static final int RAMP_NONE = 0x00; // No ramping - set speed immediately 075 public static final int RAMP_FAST = 0x01; // Fast ramping 076 public static final int RAMP_MEDIUM = 0x02; // Medium ramping 077 public static final int RAMP_MED_SLOW = 0x03; // Medium/slow ramping 078 public static final int RAMP_SLOW = 0x04; // Slow ramping 079 public static final int RAMP_SPEEDPROFILE = 0x05; // use speed profile and section distance 080 public static final int RAMP_PHYSICS = 0x06; // physics-based acceleration 081 082 /* Stop tasks codes 083 */ 084 public static final int NO_TASK = 0x00; // No task at stop 085 public static final int END_REVERSAL = 0x01; // Handle reversing direction at end for back and forth running 086 public static final int BEGINNING_RESET = 0x02; // Handle reseting beginning for back and forth running 087 public static final int END_TRAIN = 0x04; // Ending Transit. 088 089 // operational instance variables 090 private static final NamedBean.DisplayOptions USERSYS = NamedBean.DisplayOptions.USERNAME_SYSTEMNAME; 091 private ActiveTrain _activeTrain = null; 092 private AutoTrainAction _autoTrainAction = null; 093 private DccThrottle _throttle = null; 094 private AutoEngineer _autoEngineer = null; 095 private int _address = -1; 096 private DccLocoAddress _dccAddress; 097 private int _savedStatus = ActiveTrain.RUNNING; 098 private int _currentRampRate = RAMP_NONE; // current Ramp Rate 099 private boolean _pausingActive = false; // true if train pausing thread is active 100 private DispatcherFrame _dispatcher; 101 102 // persistent instance variables (saved with train info) 103 private int _rampRate = RAMP_NONE; // default Ramp Rate 104 private float _speedFactor = 1.0f; // default speed factor 105 private float _maxSpeed = 1.0f; // default maximum train speed 106 // Maximum speed in scale km/h (0.0f = disabled; use throttle % cap) 107 private float _maxSpeedScaleKmh = 0.0f; 108 private float _minReliableOperatingSpeed = 0.0f; 109 private boolean _runInReverse = false; // true if the locomotive should run through Transit in reverse 110 private boolean _soundDecoder = false; // true if locomotive has a sound decoder 111 private long _MaxTrainLength = 600; // default train length mm. 112 private float _stopBySpeedProfileAdjust = 1.0f; 113 private boolean _stopBySpeedProfile = false; 114 // Distance-based stopping (HEAD/TAIL reference) — runtime memory 115 private float _stopByDistanceMm = 0.0f; // 0.0f => feature disabled 116 private boolean _stopByDistanceRefTail = false; // false => HEAD; true => TAIL 117 118 /** Returns the configured distance to stop into the block (mm); 0.0f means disabled. 119 * @return _stopByDistanceRefTail */ 120 public boolean isStopByDistanceRefTail() { 121 return _stopByDistanceRefTail; 122 } 123 public float getStopByDistanceMm() { 124 return _stopByDistanceMm; 125 } 126 127 /** Sets whether the stop reference is TAIL (true) or HEAD (false). */ 128 public void setStopByDistanceRefTail(boolean tail) { _stopByDistanceRefTail = tail; } 129 130 /** Sets the configured distance to stop into the block (mm). */ 131 public void setStopByDistanceMm(float mm) { _stopByDistanceMm = (mm > 0.0f) ? mm : 0.0f; } 132 133 /** Returns true if the stop reference is TAIL (add train length); false for HEAD. */ 134 private boolean _useSpeedProfileRequested = true; 135 private int _functionLight = 0; 136 private int _functionBell = 1; 137 private int _functionHorn = 2; 138 139 // accessor functions 140 public ActiveTrain getActiveTrain() { 141 return _activeTrain; 142 } 143 144 public DccLocoAddress getDccAddress() { 145 return _dccAddress; 146 } 147 148 public AutoEngineer getAutoEngineer() { 149 return _autoEngineer; 150 } 151 152 public AutoTrainAction getAutoTrainAction() { 153 return _autoTrainAction; 154 } 155 156 public RosterEntry getRosterEntry() { 157 return re; 158 } 159 160 public boolean getForward() { 161 return _autoEngineer.getIsForward(); 162 } 163 164 public void setForward(boolean set) { 165 _autoEngineer.setIsForward(set); 166 } 167 168 /** 169 * Manually set the train throttle Function value. 170 * Value passed through to the Throttle. 171 * @param functionNum the function number. 172 * @param isSet true is on, false is off. 173 */ 174 public void setFunction(int functionNum, boolean isSet) { 175 _autoEngineer.setFunction(functionNum, isSet); 176 } 177 178 public synchronized float getTargetSpeed() { 179 return _autoEngineer.getTargetSpeed(); 180 } 181 182 public synchronized void setTargetSpeedByPass(float speed) { 183 _autoEngineer.setTargetSpeed(-1.0f, speed); 184 } 185 186 public synchronized void setTargetSpeedByPass(float distance, float speed) { 187 if (distance < 0.0f) { 188 _autoEngineer.setTargetSpeed(speed); 189 } else { 190 _autoEngineer.setTargetSpeed(distance, speed); 191 } 192 } 193 194 public synchronized void setTargetSpeed(float speed) { 195 log.debug("{}: setTargetSpeed(speed={}): stopped={}, targetWas={}, delayedMode=?", 196 _activeTrain.getTrainName(), speed, _autoEngineer.isStopped(), getTargetSpeed()); 197 if (_autoEngineer.isStopped() && getTargetSpeed() == 0.0f && speed > 0.0f) { 198 boolean hold = _autoTrainAction.isDelayedStart(-1.0f, speed); 199 log.debug("{}: delayedStart check (no-distance): hold={}", _activeTrain.getTrainName(), hold); 200 if (_autoTrainAction.isDelayedStart(-1.0f, speed)) 201 return; 202 } 203 _autoEngineer.setTargetSpeed(speed); 204 } 205 206 public synchronized void setTargetSpeed(float distance, float speed) { 207 log.debug("{}: setTargetSpeed(distance={}, speed={}): stopped={}, targetWas={}", 208 _activeTrain.getTrainName(), distance, speed, _autoEngineer.isStopped(), getTargetSpeed()); 209 if (_autoEngineer.isStopped() && getTargetSpeed() == 0.0f && speed > 0.0f) { 210 if (_autoTrainAction.isDelayedStart(distance, speed)) 211 return; 212 } 213 _autoEngineer.setTargetSpeed(distance, speed); 214 } 215 216 public int getSavedStatus() { 217 return _savedStatus; 218 } 219 220 public void setSavedStatus(int status) { 221 _savedStatus = status; 222 } 223 224 public synchronized void setCurrentRampRate(int rate) { 225 _currentRampRate = rate; 226 } 227 228 public int getRampRate() { 229 return _rampRate; 230 } 231 232 public void setRampRate(int rate) { 233 _rampRate = rate; 234 _currentRampRate = rate; 235 } 236 237 public float getSpeedFactor() { 238 return _speedFactor; 239 } 240 241 public void setSpeedFactor(float factor) { 242 _speedFactor = factor; 243 } 244 245 public float getMaxSpeed() { 246 return _maxSpeed; 247 } 248 249 public void setMaxSpeed(float speed) { 250 _maxSpeed = speed; 251 if (_autoEngineer != null ) { 252 _autoEngineer.setSpeedLimits(_minReliableOperatingSpeed, _maxSpeed, _speedFactor); 253 } 254 } 255 256 public float getMaxSpeedScaleKmh() { return _maxSpeedScaleKmh; } 257 public void setMaxSpeedScaleKmh(float kmh) { _maxSpeedScaleKmh = kmh; } 258 259 /** 260 * gets the lowest speed as a percentage of throttle that the loco reliably operates. 261 * @return percentage throttle 262 */ 263 public float getMinReliableOperatingSpeed() { 264 return _minReliableOperatingSpeed; 265 } 266 267 /** 268 * Sets the lowest speed as a percentage of throttle that the loco reliably operates. 269 * @param speed percentage of throttle. 270 */ 271 public void setMinReliableOperatingSpeed(float speed) { 272 _minReliableOperatingSpeed = speed; 273 if (_autoEngineer != null ) { 274 _autoEngineer.setSpeedLimits(_minReliableOperatingSpeed, _maxSpeed, _speedFactor); 275 } 276 } 277 278 /** 279 * @deprecated Use {@code ActiveTrain.setTrainDetection(TrainDetection value } insteadUse 280 * @param set True if entire train is detectable 281 */ 282 @Deprecated (since="5.7.6",forRemoval=true) 283 public void setResistanceWheels(boolean set) { 284 if (set) { 285 _activeTrain.setTrainDetection(TrainDetection.TRAINDETECTION_WHOLETRAIN); 286 } else { 287 _activeTrain.setTrainDetection(TrainDetection.TRAINDETECTION_HEADONLY); 288 } 289 } 290 291 public boolean getRunInReverse() { 292 return _runInReverse; 293 } 294 295 public void setRunInReverse(boolean set) { 296 _runInReverse = set; 297 } 298 299 public boolean getSoundDecoder() { 300 return _soundDecoder; 301 } 302 303 public void setSoundDecoder(boolean set) { 304 _soundDecoder = set; 305 } 306 307 /** 308 * 309 * @return train length in MM. 310 */ 311 public long getMaxTrainLengthMM() { 312 return _MaxTrainLength; 313 } 314 315 /** 316 * Set Train length in Scale Meters 317 * @param length length of train in meterd 318 * @param scaleFactor as supplied by scale object 319 */ 320 public void setMaxTrainLength(double length, double scaleFactor) { 321 _MaxTrainLength = (long) (length * 1000.0 * scaleFactor); 322 log.trace("setMaxTrainLength[{}]",_MaxTrainLength); 323 } 324 325 public void setUseSpeedProfile(boolean tf) { 326 _useSpeedProfileRequested = tf; 327 } 328 329 public boolean getUseSpeedProfile() { 330 return _useSpeedProfileRequested; 331 } 332 333 public void setStopBySpeedProfile(boolean tf) { 334 _stopBySpeedProfile = tf; 335 } 336 337 public void setStopBySpeedProfileAdjust(float adjust) { 338 _stopBySpeedProfileAdjust = adjust; 339 } 340 341 public boolean getStopBySpeedProfile() { 342 return _stopBySpeedProfile; 343 } 344 345 public float getStopBySpeedProfileAdjust() { 346 return _stopBySpeedProfileAdjust; 347 } 348 /** 349 * Set the F-Number for the light 350 * @param value F-Number 351 */ 352 public void setFunctionLight(int value) { 353 _functionLight = value; 354 } 355 /** 356 * Returns the F-Number for the light. 357 * @return F-Number 358 */ 359 public int getFunctionLight() { 360 return _functionLight; 361 } 362 /** 363 * Set the F-Number for the Bell 364 * @param value F-Number 365 */ 366 public void setFunctionBell(int value) { 367 _functionBell = value; 368 } 369 /** 370 * Returns the F-Number for the Bell. 371 * @return F-Number 372 */ 373 public int getFunctionBell() { 374 return _functionBell; 375 } 376 /** 377 * Set the F-Number for the Horn 378 * @param value F-Number 379 */ 380 public void setFunctionHorn(int value) { 381 _functionHorn = value; 382 } 383 /** 384 * Returns the F-Number for the Horn. 385 * @return F-Number 386 */ 387 public int getFunctionHorn() { 388 return _functionHorn; 389 } 390 391 /** 392 * Get current Signal DisplayName. 393 * @return empty String if no signal, otherwise Display Name. 394 */ 395 public String getCurrentSignal() { 396 if (_activeTrain.getSignalType() == DispatcherFrame.SIGNALHEAD) 397 return (_controllingSignal == null ) ? "" : _controllingSignal.getDisplayName() ; 398 else 399 return (_controllingSignalMast == null ) ? "" : _controllingSignalMast.getDisplayName(); 400 } 401 402 /** 403 * Get current Signal UserName. 404 * @return empty String if no signal, otherwise UserName. 405 */ 406 public String getCurrentSignalUserName() { 407 if (_activeTrain.getSignalType() == DispatcherFrame.SIGNALHEAD) 408 return ( _controllingSignal == null || _controllingSignal.getUserName() == null) ? "" : _controllingSignal.getUserName(); 409 else 410 return ( _controllingSignalMast == null || _controllingSignalMast.getUserName() == null) ? "" : _controllingSignalMast.getUserName(); 411 } 412 413 private RosterEntry re = null; 414 boolean useSpeedProfile = false; 415 416 /** 417 * Initialize new Auto Active Train or get a new throttle after WORKING Sets 418 * up the DCC address and initiates creation of a throttle to run the train. 419 * 420 * @return true if initialized; false otherwise 421 */ 422 public boolean initialize() { 423 //clear all flags 424 _pausingActive = false; 425 _stoppingBySensor = false; 426 _stoppingByBlockOccupancy = false; 427 _stoppingUsingSpeedProfile = false; 428 // get the dispatcher 429 _dispatcher = InstanceManager.getDefault(DispatcherFrame.class); 430 431 // Sync "Use stop sensor" from ActiveTrain/TrainInfo (default true if absent) 432 // When Override stop sensor is checked in the UI, ActiveTrain/TrainInfo will have useStopSensor == false. 433 this._useStopSensor = _activeTrain.getUseStopSensor(); 434 435 // DEBUG 436 log.debug("{}: useStopSensor at init - ActiveTrain={}, AutoActiveTrain={}", 437 _activeTrain.getTrainName(), _activeTrain.getUseStopSensor(), this._useStopSensor); 438 439 // get decoder address 440 try { 441 _address = Integer.parseInt(_activeTrain.getDccAddress()); 442 } catch (NumberFormatException ex) { 443 log.warn("invalid dcc address '{}' for {}", _activeTrain.getDccAddress(), _activeTrain.getTrainName()); 444 return false; 445 } 446 if ((_address < 1) || (_address > 9999)) { 447 log.warn("invalid dcc address '{}' for {}", _activeTrain.getDccAddress(), _activeTrain.getTrainName()); 448 return false; 449 } 450 // request a throttle for automatic operation, throttle returned via callback below 451 useSpeedProfile = false; 452 boolean ok; 453 _dccAddress = new DccLocoAddress( 454 _address,!InstanceManager.throttleManagerInstance().canBeShortAddress(_address)); 455 if (_activeTrain.getTrainSource() == ActiveTrain.ROSTER) { 456 if (_activeTrain.getRosterEntry() != null) { 457 re = _activeTrain.getRosterEntry(); 458 ok = InstanceManager.throttleManagerInstance().requestThrottle(re, this, false); 459 if (_useSpeedProfileRequested) { 460 if (re.getSpeedProfile() != null && re.getSpeedProfile().getProfileSize() > 0) { 461 useSpeedProfile = true; 462 } 463 } 464 log.debug("{}: requested roster entry '{}', address={}, use speed profile requested={} usespeedprofile set={}", 465 _activeTrain.getTrainName(), re.getId(), _address, _useSpeedProfileRequested, useSpeedProfile); 466 } else { 467 ok = InstanceManager.throttleManagerInstance().requestThrottle(_dccAddress, this, false); 468 log.debug("{}: requested throttle address={}, roster entry not found", _activeTrain.getTrainName(), _address); 469 } 470 } else { 471 ok = InstanceManager.throttleManagerInstance().requestThrottle(_dccAddress, this, false); 472 log.debug("{}: requested throttle address={}", _activeTrain.getTrainName(), _address); 473 } 474 if (!ok) { 475 log.warn("Throttle for locomotive address {} could not be setup.", _address); 476 _activeTrain.setMode(ActiveTrain.DISPATCHED); 477 return false; 478 } 479 return true; 480 } 481 482 // Throttle feedback method - Initiates running AutoEngineer with the new throttle 483 @Override 484 public void notifyThrottleFound(DccThrottle t) { 485 _throttle = t; 486 if (_throttle == null) { 487 JmriJOptionPane.showMessageDialog(null, java.text.MessageFormat.format(Bundle.getMessage( 488 "Error28"), new Object[]{_activeTrain.getTrainName()}), Bundle.getMessage("MessageTitle"), 489 JmriJOptionPane.INFORMATION_MESSAGE); 490 log.warn("null throttle returned for train '{}' during automatic initialization.", _activeTrain.getTrainName()); 491 _activeTrain.setMode(ActiveTrain.DISPATCHED); 492 return; 493 } 494 log.debug("{}: New AutoEngineer, address={}, length (mm)={}, factor={}, useSpeedProfile={}", 495 _activeTrain.getTrainName(), 496 _throttle.getLocoAddress(), 497 getMaxTrainLengthMM(), _speedFactor, useSpeedProfile); 498 // get off this thread ASAP, some throttles does not completely initialize 499 // until this thread finishes 500 jmri.util.ThreadingUtil.runOnLayoutDelayed(() -> { 501 if (_autoEngineer != null) { 502 log.error("Second Trottle for same loco[{}] - ignoring", _address); 503 // at least make sure its going the right way... 504 setEngineDirection(); 505 } else { 506 _autoEngineer = new AutoEngineer(t, re); 507 _activeTrain.setMode(ActiveTrain.AUTOMATIC); 508 // set initial direction 509 setEngineDirection(); 510 _autoEngineer.setRamping(_currentRampRate, _dispatcher.getFullRampTime(), 511 _dispatcher.getMinThrottleInterval(), _currentRampRate); 512 _autoEngineer.setSpeedLimits(_minReliableOperatingSpeed, _maxSpeed, _speedFactor); 513 } 514 if (_resumingAutomatic) { 515 _resumingAutomatic = false; 516 _activeTrain.setStatus(ActiveTrain.RUNNING); 517 setupNewCurrentSignal(null, true); 518 // if no current signal use saved. 519 if (!isCurrentSignal()) { 520 restoreSavedSpeedAndDirection(); 521 } else { 522 setSpeedBySignal(); 523 } 524 } else if (_dispatcher.getAutoAllocate()) { 525 // starting for the first time with automatic allocation of 526 // Sections 527 // the last of 2 threads must call setSpeedBySignal 528 // if the other thread is incomplete _currentAllocated Section 529 // will be null 530 if (_currentAllocatedSection != null) { 531 setSpeedBySignal(); 532 } 533 } 534 }, 500); 535 } 536 537 protected DccThrottle getThrottle() { 538 return _throttle; 539 } 540 541 @Override 542 public void notifyFailedThrottleRequest(jmri.LocoAddress address, String reason) { 543 log.error("Throttle request failed for {} because {}", address, reason); 544 } 545 546 /** 547 * No steal or share decisions made locally 548 * <p> 549 * {@inheritDoc} 550 */ 551 @Override 552 public void notifyDecisionRequired(jmri.LocoAddress address, DecisionType question) { 553 } 554 555 // more operational variables 556 // private final ArrayList<AllocatedSection> _allocatedSectionList = new ArrayList<>(); 557 private jmri.jmrit.display.layoutEditor.LayoutBlockManager _lbManager = null; 558 private AllocatedSection _lastAllocatedSection = null; 559 560 protected Section getLastAllocatedSection() { 561 Section as = _activeTrain.getLastAllocatedSection(); 562 return as; 563 } 564 565 private boolean _initialized = false; 566 private Section _nextSection = null; // train has not reached this Section yet 567 private volatile AllocatedSection _currentAllocatedSection = null; // head of the train is in this Section 568 private volatile AllocatedSection _previousAllocatedSection = null; // previous Section - part of train could still be in this section 569 private SignalHead _controllingSignal = null; 570 private SignalMast _controllingSignalMast = null; 571 private SignalHead _controllingSignalPrev = null; 572 private SignalMast _controllingSignalMastPrev = null; 573 private PropertyChangeListener _conSignalListener = null; 574 private PropertyChangeListener _conSignalMastListener = null; 575 private Block _conSignalProtectedBlock = null; 576 private volatile Block _currentBlock = null; 577 private Block _nextBlock = null; 578 private volatile Block _previousBlock = null; 579 private boolean _stoppingBySensor = false; 580 private Sensor _stopSensor = null; 581 private PropertyChangeListener _stopSensorListener = null; 582 private Turnout _turnoutStateNeeded = null; 583 private PropertyChangeListener _turnoutStateListener = null; 584 private boolean _stoppingByBlockOccupancy = false; // if true, stop when _stoppingBlock goes UNOCCUPIED 585 private boolean _stoppingUsingSpeedProfile = false; // if true, using the speed profile against the roster entry to bring the loco to a stop in a specific distance 586 // Distance stop is armed (waiting to start at the section's first occupied block) 587 private boolean _distanceStopPending = false; 588 // If true, the pending distance stop is an approach-to-min (hold until sensor), not a stop-to-zero 589 private boolean _distanceStopPendingToMin = false; 590 private float _distanceStopPendingMm = 0.0f; 591 private int _distanceStopPendingTask = NO_TASK; 592 private volatile Block _stoppingBlock = null; 593 private boolean _resumingAutomatic = false; // if true, resuming automatic mode after WORKING session 594 private boolean _needSetSpeed = false; // if true, train will set speed according to signal instead of stopping 595 private boolean waitingOnAllocation = false; //if true the train was stopped due to next section not allocated 596 // keeps track of and restores previous speed 597 private float _savedSpeed = 0.0f; 598 private boolean _savedForward = true; 599 600 public void set_useStopSensor(boolean _useStopSensor) { 601 this._useStopSensor = _useStopSensor; 602 } 603 604 private boolean _useStopSensor = true; //used by DispatcherSystem to override use of stop sensor 605 606 // --- Physics runtime state --- 607 private float _additionalWeightTonnes = 0.0f; // extra consist mass in metric tonnes (t) 608 private float _rollingResistanceCoeff = 0.002f; // dimensionless c_rr; default ~0.002 609 610 public void setAdditionalTrainWeightMetricTonnes(float tonnes) { 611 _additionalWeightTonnes = Math.max(0.0f, tonnes); 612 } 613 public float getAdditionalTrainWeightMetricTonnes() { return _additionalWeightTonnes; } 614 615 public void setRollingResistanceCoeff(float value) { 616 _rollingResistanceCoeff = Math.max(0.0f, value); 617 } 618 public float getRollingResistanceCoeff() { return _rollingResistanceCoeff; } 619 620 // Driver’s applied power/regulator during acceleration (0.0..1.0); default 1.0 621 private float _driverPowerPercent = 1.0f; 622 public void setDriverPowerPercent(float value) { 623 if (value < 0.0f) { 624 value = 0.0f; 625 } 626 if (value > 1.0f) { 627 value = 1.0f; 628 } 629 _driverPowerPercent = value; 630 } 631 public float getDriverPowerPercent() { return _driverPowerPercent; } 632 633 protected void saveSpeedAndDirection() { 634 _savedSpeed = _autoEngineer.getTargetSpeed(); 635 _savedForward = _autoEngineer.getIsForward(); 636 } 637 638 protected void restoreSavedSpeedAndDirection() { 639 _autoEngineer.setTargetSpeed(_savedSpeed); 640 _autoEngineer.setIsForward(_savedForward); 641 } 642 643 // keeps track of number of horn execution threads that are active 644 private int _activeHornThreads = 0; 645 646 protected void decrementHornExecution() { 647 _activeHornThreads--; 648 } 649 650 protected void incrementHornExecution() { 651 _activeHornThreads++; 652 } 653 654 // 655 // Notification methods 656 // 657 /** 658 * Handle notification of changes in section state. 659 * 660 * @param as the allocated that changed 661 */ 662 protected void handleSectionStateChange(AllocatedSection as) { 663 if (!_activeTrain.isInAllocatedList(as)) { 664 addAllocatedSection(as); 665 } 666 } 667 668 /** 669 * Handle notification of allocation added to the ActiveTrain allocatedsections table. 670 * Subtly different from change in a sections status. 671 * 672 * @param evt the allocation that changed 673 */ 674 private void handleAnotherSectionAllocatedChange( PropertyChangeEvent evt) { 675 if (waitingOnAllocation || _activeTrain.getSignalType() == DispatcherFrame.SECTIONSALLOCATED) { 676 waitingOnAllocation = false; 677 setSpeedBySignal(); 678 } 679 } 680 681 /** 682 * Handle notification of changes in section occupancy. 683 * 684 * @param as the section that changed 685 */ 686 protected void handleSectionOccupancyChange(AllocatedSection as) { 687 if (!_activeTrain.isInAllocatedList(as)) { 688 log.debug("Unexpected occupancy change notification - Section {}", as.getSection().getDisplayName(USERSYS)); 689 return; 690 } 691 if (as.getSection().getOccupancy() == Section.OCCUPIED) { 692 // Section changed to OCCUPIED - process if expected next Section 693 if (as.getSection() == _nextSection) { 694 setNewCurrentSection(as); 695 } 696 } else if (as.getSection().getOccupancy() == Section.UNOCCUPIED) { 697 jmri.TransitSection ts = as.getTransitSection(); 698 if (ts != null) { 699 _autoTrainAction.removeTransitSection(ts); 700 } 701 } 702 } 703 704 @SuppressFBWarnings(value = "IS2_INCONSISTENT_SYNC", 705 justification = "OK to not sync here, no conflict expected") 706 protected void handleBlockStateChange(AllocatedSection as, Block b) { 707 //Block oldPreviousBlock = _previousBlock; 708 if (b.getState() == Block.OCCUPIED) { 709 // Block changed to OCCUPIED - train has entered this block 710 log.debug("{}: handleBlockStateChange to OCCUPIED section {}, block {}, length {}", _activeTrain.getTrainName(), 711 as.getSection().getDisplayName(USERSYS), 712 b.getDisplayName(USERSYS), getBlockLength(b)); 713 // If a distance stop is pending, start exactly at the first block INSIDE the current section 714 if (_distanceStopPending && _currentAllocatedSection != null) { 715 Block enter = _currentAllocatedSection.getEnterBlock(_previousAllocatedSection); 716 if (enter == b) { 717 float mm = _distanceStopPendingMm; 718 int taskPending = _distanceStopPendingTask; 719 boolean toMin = _distanceStopPendingToMin; 720 _distanceStopPending = false; 721 _distanceStopPendingToMin = false; 722 723 _stoppingUsingSpeedProfile = true; // commit to distance-based braking 724 if (toMin) { 725 // COMBINED (approach-to-min + sensor): 726 re.getSpeedProfile().setMinCommandIntervalMs(_dispatcher.getMinThrottleInterval()); 727 re.getSpeedProfile().setMinCommandIntervalMs(_dispatcher.getMinThrottleInterval()); 728 re.getSpeedProfile().setMinCommandIntervalMs(_dispatcher.getMinThrottleInterval()); 729 re.getSpeedProfile().setMinMaxLimitsKmh(_minReliableOperatingSpeed, _maxSpeed, 730 _maxSpeedScaleKmh, (float) _dispatcher.getScale().getScaleRatio(), 731 _autoEngineer.getIsForward()); 732 re.getSpeedProfile().planApproachToMinOverDistanceThenStopBySensor( 733 getThrottle(), mm, _stopSensor, _speedFactor); 734 return; // bypass legacy inner controller 735 } 736 // PURE distance stop-to-zero: cancel and plan via RosterSpeedProfile (same semantics) 737 cancelStopInCurrentSection(); 738 re.getSpeedProfile().setMinMaxLimitsKmh(_minReliableOperatingSpeed, _maxSpeed, _maxSpeedScaleKmh, 739 (float) _dispatcher.getScale().getScaleRatio(), _autoEngineer.getIsForward()); 740 re.getSpeedProfile().planStopToZeroOverDistance(getThrottle(), mm, _speedFactor); 741 Thread tWait = jmri.util.ThreadingUtil.newThread(new WaitForTrainToStop(taskPending), 742 "Wait for stop " + getActiveTrain().getActiveTrainName()); 743 tWait.start(); 744 } 745 } 746 if (b == _nextBlock || _nextBlock == null) { 747 _currentBlock = b; 748 // defer setting the next/previous blocks until we know if its required and in what fashion 749 // for stopping blocks that action happens after the train has stopped. 750 // first check for entering the end point 751 if (!_activeTrain.isTransitReversed() && as.getSequence() == _activeTrain.getEndBlockSectionSequenceNumber()) { 752 // are we going to reverse at end 753 if ( _activeTrain.getReverseAtEnd() ) { 754 removeCurrentSignal(); 755 stopInCurrentSection(END_REVERSAL, StopContext.DESTINATION); 756 } 757 // are we going continuously without delay 758 else if ( _activeTrain.getResetWhenDone() && _activeTrain.getDelayedRestart() == ActiveTrain.NODELAY) { 759 _activeTrain.setRestart(_activeTrain.getDelayedRestart(),_activeTrain.getRestartDelay(), 760 _activeTrain.getRestartSensor(),_activeTrain.getResetRestartSensor()); 761 _activeTrain.setTransitReversed(false); 762 _activeTrain.resetAllAllocatedSections(); 763 _previousBlock = null; 764 _nextBlock = getNextBlock(_currentBlock, _currentAllocatedSection); 765 setEngineDirection(); 766 if ((_nextSection != null) && !_activeTrain.isInAllocatedList(_nextSection)) { 767 // we need to get a next section 768 _dispatcher.queueScanOfAllocationRequests(); 769 // and then set the signal 770 } 771 // can be mid block 772 setupNewCurrentSignal(null, true); 773 setSpeedBySignal(); 774 } 775 // are we restarting later 776 else if ( _activeTrain.getResetWhenDone()) { 777 // We enter this code for each block in the section. 778 // If we stop in the farthest block eg Block 3 in a 3 Block Section 779 // nothing special is required when starting. 780 // If we stop in Block 1 of a 3 block section, and enter this code 781 // when starting off again, so its just an advance of the _nextBlock. 782 // we can tell which situation it is by looking 783 // whether the _nextSection is not null and allocated to us. 784 if ( _nextSection == null || !_activeTrain.isInAllocatedList(_nextSection)) { 785 removeCurrentSignal(); 786 _nextBlock = getNextBlock(_currentBlock, _currentAllocatedSection); 787 stopInCurrentSection(BEGINNING_RESET, StopContext.DESTINATION); 788 } else { 789 _nextBlock = getNextBlock(_currentBlock, _currentAllocatedSection); 790 } 791 } 792 // else we are ending here 793 else { 794 log.debug("{}: Trip end, stop in Current Section, Block= {}", _activeTrain.getTrainName(), b.getDisplayName(USERSYS)); 795 removeCurrentSignal(); 796 stopInCurrentSection(END_TRAIN, StopContext.DESTINATION); 797 } 798 } 799 // are we entering the start point 800 else if (_activeTrain.isTransitReversed() && as.getSequence() == _activeTrain.getStartBlockSectionSequenceNumber()) { 801 // are we coming back from a reverse and running continiuosly 802 if ( _activeTrain.getResetWhenDone() && _activeTrain.isTransitReversed() ) { 803 removeCurrentSignal(); 804 stopInCurrentSection(BEGINNING_RESET, StopContext.DESTINATION); 805 } 806 // else we are ending here 807 else { 808 log.debug("{}: Trip end, stop in Current Section, Block= {}", _activeTrain.getTrainName(), b.getDisplayName(USERSYS)); 809 removeCurrentSignal(); 810 stopInCurrentSection(END_TRAIN, StopContext.DESTINATION); 811 } 812 } else { 813 // if we are not in first and not in last get the next block 814 //_previousBlock = oldPreviousBlock; 815 _nextBlock = getNextBlock(b, as); 816 if (_nextBlock != null) { 817 // this is a normal block/block change 818 // set the blocks as normal 819 _previousBlock = _currentBlock; 820 _nextBlock = getNextBlock(b, as); 821 //if (_nextBlock.getState() == Block.OCCUPIED) { 822 // handleBlockStateChange(as, _nextBlock); 823 //} 824 setupNewCurrentSignal(as, false); 825 } else { 826 // assume we have reached last block in this transit, for safety sake. 827 log.warn("{}: No next Block from Block= {} Section= {}", _activeTrain.getTrainName(), 828 b.getDisplayName(USERSYS), as.getSection().getDisplayName(USERSYS)); 829 removeCurrentSignal(); 830 stopInCurrentSection(NO_TASK); 831 } 832 } 833 } else if (b != _currentBlock) { 834 log.trace("{}: block going occupied {} is not _nextBlock or _currentBlock - ignored.", 835 _activeTrain.getTrainName(), b.getDisplayName(USERSYS)); 836 return; 837 } 838 } else if (b.getState() == Block.UNOCCUPIED) { 839 log.debug("{}: handleBlockStateChange to UNOCCUPIED - Section {}, Block {}, speed {}", _activeTrain.getTrainName(), 840 as.getSection().getDisplayName(USERSYS), b.getDisplayName(USERSYS), 841 _autoEngineer == null ? "" : getTargetSpeed()); 842 if (_stoppingByBlockOccupancy && (b == _stoppingBlock)) { 843 log.trace("{}: setStopNow by block occupancy from Block unoccupied, Block= {}", _activeTrain.getTrainName(), b.getDisplayName(USERSYS)); 844 _stoppingByBlockOccupancy = false; 845 _stoppingBlock = null; 846 if (_needSetSpeed) { 847 _needSetSpeed = false; 848 setSpeedBySignal(); 849 } else { 850 setStopNow(); 851 } 852 } else { 853 if (!isStopping() && _dispatcher.getUseOccupiedTrackSpeed()) { 854 setSpeedBySignal(); 855 } 856 } 857 } 858 _autoTrainAction.handleBlockStateChange(as, b); 859 } 860 861 /** 862 * support methods 863 */ 864 protected void setEngineDirection() { 865 boolean oldFwd = getForward(); 866 if (_runInReverse) { 867 setForward(_activeTrain.isTransitReversed()); 868 } else { 869 setForward(!_activeTrain.isTransitReversed()); 870 } 871 log.debug("[{}]flipping direction was [{}] now [{}]",_activeTrain.getActiveTrainName() ,oldFwd, getForward()); 872 } 873 874 protected AllocatedSection getCurrentAllocatedSection() { 875 return _currentAllocatedSection; 876 } 877 878 /* 879 * Reverse lookup for allocated section. 880 */ 881 protected AllocatedSection getAllocatedSectionForSection(Section s) { 882 for (AllocatedSection allocatedSection : _activeTrain.getAllocatedSectionList()) { 883 if (allocatedSection.getSection() == s) 884 return allocatedSection; 885 } 886 return null; 887 } 888 889 protected void allocateAFresh() { 890 //Reset initialized flag 891 _initialized = false; 892 // set direction 893 _currentAllocatedSection=null; 894 _currentBlock=null; 895 setForward(!getRunInReverse()); 896 } 897 898 private void addAllocatedSection(AllocatedSection as) { 899 if (!_initialized) { 900 // this is first allocated section, get things started 901 _initialized = true; 902 _nextSection = as.getSection(); 903 _currentBlock = _activeTrain.getStartBlock(); 904 if (as.getSection().containsBlock(_currentBlock)) { 905 // starting Block is in this allocated section - find next Block 906 setNewCurrentSection(as); 907 _nextBlock = getNextBlock(_currentBlock, as); 908 } else if (as.getSection().connectsToBlock(_currentBlock)) { 909 // starting Block is connected to a Block in this allocated section 910 EntryPoint ep = as.getSection().getEntryPointFromBlock(_currentBlock, as.getDirection()); 911 if (ep != null) { 912 _nextBlock = ep.getBlock(); 913 } else { 914 log.error("failure to get entry point to Transit from Block {}", _currentBlock.getDisplayName(USERSYS)); 915 } 916 } 917 if (_nextBlock != null) { 918 // set up new current signal, as this a beginning we allow a signal not at end of block 919 // to control the speed. 920 setupNewCurrentSignal(as,true); 921 } 922 } 923 // if train is stopping for lack of an allocation, set flag to restart it 924 if (!_pausingActive && (_lastAllocatedSection == _currentAllocatedSection) 925 && isStopping() && (_activeTrain.getStatus() == ActiveTrain.RUNNING)) { 926 _needSetSpeed = true; 927 } 928 929 // request next allocation if appropriate--Dispatcher must decide whether to allocate it and when 930 if ((!_dispatcher.getAutoAllocate()) && ((_lastAllocatedSection == null) 931 || (_lastAllocatedSection.getNextSection() == as.getSection()))) { 932 // if AutoAllocate, this is now done in DispatcherFrame.java for all trains 933 _lastAllocatedSection = as; 934 if (as.getNextSection() != null) { 935 Section nSection = as.getNextSection(); 936 int nextSeq = as.getNextSectionSequence(); 937 int nextDir = _activeTrain.getAllocationDirectionFromSectionAndSeq(nSection, nextSeq); 938 _dispatcher.requestAllocation(_activeTrain, nSection, nextDir, nextSeq, true, null); 939 } 940 } 941 } 942 943 private boolean isStopping() { 944 // here add indicator for new stopping methods, if any are added 945 return (_stoppingBySensor || _stoppingByBlockOccupancy || _stoppingUsingSpeedProfile); 946 } 947 948 private void removeCurrentSignal() { 949 if (_conSignalListener != null) { 950 _controllingSignal.removePropertyChangeListener(_conSignalListener); 951 _conSignalListener = null; 952 } 953 _controllingSignalPrev = _controllingSignal; 954 _controllingSignal = null; 955 if (_conSignalMastListener != null) { 956 _controllingSignalMast.removePropertyChangeListener(_conSignalMastListener); 957 _conSignalMastListener = null; 958 } 959 _controllingSignalMastPrev = _controllingSignalMast; 960 _controllingSignalMast = null; 961 _needSetSpeed = false; 962 } 963 964 /** 965 * checks for a controlling signal 966 * @return true if there is one 967 */ 968 protected boolean isCurrentSignal() { 969 if (_activeTrain.getSignalType() == DispatcherFrame.SIGNALHEAD) 970 return _controllingSignal != null; 971 else 972 // SignalMast 973 return _controllingSignalMast != null; 974 } 975 976 /** 977 * 978 * @param as current section the train is in, can be null 979 * @param forceSpeedChange if true, the speed will be set using the signal mast 980 * even if it is not on the immediate block boundary 981 */ 982 protected synchronized void setupNewCurrentSignal(AllocatedSection as, boolean forceSpeedChange) { 983 log.trace("setupNewCurrentSignal Called Section[{}] forceSpeedChange[{}]", as != null ? as.getSectionName() : "null",forceSpeedChange); 984 removeCurrentSignal(); 985 if (_activeTrain.getSignalType() == DispatcherFrame.SIGNALHEAD) { 986 SignalHead sh = _lbManager.getFacingSignalHead(_currentBlock, _nextBlock); 987 if (sh != null) { 988 _controllingSignal = sh; 989 _conSignalProtectedBlock = _nextBlock; 990 sh.addPropertyChangeListener(_conSignalListener = (PropertyChangeEvent e) -> { 991 if (e.getPropertyName().equals("Appearance")) { 992 // controlling signal has changed appearance 993 setSpeedBySignal(); 994 } 995 }); 996 _activeTrain.setControlingSignal(_controllingSignal, _controllingSignalPrev); 997 log.debug("new current signal = {}", sh.getDisplayName(USERSYS)); 998 } else { 999 // Note: null signal head will result when exiting throat-to-throat blocks. 1000 log.warn("new current signal is null - sometimes OK"); 1001 } 1002 setSpeedBySignal(); 1003 } else if (_activeTrain.getSignalType() == DispatcherFrame.SIGNALMAST) { 1004 //SignalMast 1005 SignalMast sm = null; 1006 Block cB = _currentBlock; 1007 Block nB = _nextBlock; 1008 if (as == null) { 1009 as = _currentAllocatedSection; 1010 } 1011 // get signal mast at current block change, if there is no signal mast we will proceed with no change in speed 1012 // unless forceSpeedChange is true, such as beginning, resets of transit. 1013 // previous signal mast speed unless the mast is held. 1014 boolean weAreAtSpeedChangingMast=forceSpeedChange; 1015 if ( !forceSpeedChange && nB != null ) { 1016 sm = _lbManager.getFacingSignalMast(cB, nB); 1017 if (sm != null) {weAreAtSpeedChangingMast=true;} 1018 } 1019 1020 while (sm == null && nB != null) { 1021 sm = _lbManager.getFacingSignalMast(cB, nB); 1022 if (sm == null) { 1023 cB = nB; 1024 nB = getNextBlock(nB, as); 1025 } 1026 } 1027 if (sm != null) { 1028 _controllingSignalMast = sm; 1029 _conSignalProtectedBlock = nB; 1030 sm.addPropertyChangeListener(_conSignalMastListener = (PropertyChangeEvent e) -> { 1031 if (e.getPropertyName().equals("Aspect") || e.getPropertyName().equals("Held")) { 1032 // controlling signal has changed appearance or a hold has been released 1033 // even if its a hold we still have to use target speed etc else we override pauses and other stop events. 1034 setSpeedBySignal(); 1035 } 1036 }); 1037 _activeTrain.setControlingSignal(_controllingSignalMast, _controllingSignalMastPrev); 1038 log.debug("{}: new current signalmast {}({}) for section {}", _activeTrain.getTrainName(), sm.getDisplayName(USERSYS), 1039 sm.getAspect(), as.getSection().getDisplayName(USERSYS)); 1040 if ( weAreAtSpeedChangingMast ) { 1041 setSpeedBySignal(); 1042 } else { 1043 checkForGhost(); 1044 } 1045 } else { 1046 // There is a missing signal mast at a block boundary. 1047 // If the next block is allocated to this train we can continue. 1048 // If the train was stopped here we can try and restart it. Either way we use 1049 // setting setSpeedBySectionsAllocated as a way out of the dilemma. 1050 log.debug("{}: new current signalmast is null for section {} - sometimes OK", _activeTrain.getTrainName(), 1051 as == null ? "Null" : as.getSection().getDisplayName(USERSYS)); 1052 if (_nextBlock == null || ! _activeTrain.getBlockList().contains(_nextBlock) || _autoEngineer.isStopped()) { 1053 log.warn("{}: new current signalmast is null for section {} and next block is not this trains. Temporarily continuing by allocations", _activeTrain.getTrainName(), 1054 as == null ? "Null" : as.getSection().getDisplayName(USERSYS)); 1055 setSpeedBySectionsAllocated(); 1056 } 1057 checkForGhost(); 1058 } 1059 } else { 1060 setSpeedBySignal(); 1061 } 1062 } 1063 1064 @CheckForNull 1065 private Block getNextBlock(Block b, AllocatedSection as) { 1066 //if (((_currentBlock == _activeTrain.getEndBlock()) && _activeTrain.getReverseAtEnd() 1067 // && (as.getSequence() == _activeTrain.getEndBlockSectionSequenceNumber()))) { 1068 // return _previousBlock; 1069 //} 1070 if ((_currentBlock == _activeTrain.getStartBlock()) 1071 && _activeTrain.getResetWhenDone() && _activeTrain.isTransitReversed() 1072 && (as.getSequence() == _activeTrain.getStartBlockSectionSequenceNumber())) 1073 return _previousBlock; 1074 if (as.getNextSection() != null) { 1075 EntryPoint ep = as.getSection().getExitPointToSection(_nextSection, as.getDirection()); 1076 if ((ep != null) && (ep.getBlock() == b)) 1077 // this block is connected to a block in the next section 1078 return ep.getFromBlock(); 1079 } 1080 // this allocated section has multiple blocks _or_ there is no next Section 1081 Block blk = as.getSection().getEntryBlock(); 1082 while (blk != null) { 1083 if (b == blk) 1084 return as.getSection().getNextBlock(); 1085 blk = as.getSection().getNextBlock(); 1086 } 1087 return null; 1088 } 1089 1090 private void setNewCurrentSection(AllocatedSection as) { 1091 if (as.getSection() == _nextSection) { 1092 _previousAllocatedSection = _currentAllocatedSection; 1093 _currentAllocatedSection = as; 1094 _nextSection = as.getNextSection(); 1095 TransitSection ts = as.getTransitSection(); 1096 if (ts != null) { 1097 _autoTrainAction.addTransitSection(ts); 1098 } 1099 // written the long way for readability 1100 boolean nextSectionExpected = true; 1101 if (ts != null && 1102 ts.isSafe() && 1103 _activeTrain.getAllocateMethod() == ActiveTrain.ALLOCATE_BY_SAFE_SECTIONS) { 1104 nextSectionExpected = false; 1105 } else if (!_activeTrain.isAllocationReversed() && 1106 _activeTrain.getEndBlockSection() == _currentAllocatedSection.getSection()) { 1107 nextSectionExpected = false; 1108 } else if (_activeTrain.isAllocationReversed() && 1109 _activeTrain.getStartBlockSectionSequenceNumber() == _currentAllocatedSection.getSequence()) { 1110 nextSectionExpected = false; 1111 } 1112 log.debug("{}:Next Section Expected[{}]",_activeTrain.getActiveTrainName(), nextSectionExpected); 1113 // NOw handled in SetSpeedBySignal() 1114 // check if new next Section exists but is not allocated to this train excepting above circumstances 1115 //if ( nextSectionExpected &&_nextSection != null && !_activeTrain.isInAllocatedList(_nextSection)) { 1116 // // next section is not allocated to this train, must not enter it, even if signal is OK. 1117 // log.warn("Stopping train [{}] in section [{}], as next section [{}] is not allocated", 1118 // _activeTrain.getActiveTrainName(),_currentAllocatedSection.getSection().getDisplayName(USERSYS),_nextSection.getDisplayName(USERSYS)); 1119 // stopInCurrentSection(NO_TASK); 1120 // _needSetSpeed = false; 1121 //} 1122 // see if we need to rescan as entering safe section. 1123 if (ts != null && 1124 ts.isSafe() && 1125 _activeTrain.getAllocateMethod() == ActiveTrain.ALLOCATE_BY_SAFE_SECTIONS) { 1126 _dispatcher.queueScanOfAllocationRequests(); 1127 } 1128 1129 } 1130 } 1131 1132 // Criteria for being able to set or get a speed. 1133 protected boolean canSpeedBeSetOrChecked() { 1134 if (_pausingActive || getAutoEngineer() == null || 1135 ((_activeTrain.getStatus() != ActiveTrain.RUNNING) && 1136 (_activeTrain.getStatus() != ActiveTrain.WAITING)) || 1137 !_activeTrain.getStarted() || 1138 (_activeTrain.getMode() != ActiveTrain.AUTOMATIC)) { 1139 log.debug("{}:Train is not currently eligible for settingspeed or checking ghosts",_activeTrain.getActiveTrainName()); 1140 return false; 1141 } 1142 return true; 1143 } 1144 1145 // called by above or when resuming after stopped action 1146 protected synchronized void setSpeedBySignal() { 1147 log.trace("Set Speed by Signal"); 1148 if (!canSpeedBeSetOrChecked()) { 1149 log.trace("[{}]:cannot set speed.",getActiveTrain().getActiveTrainName()); 1150 return; 1151 } 1152 1153 // only bother to check signal if the next allocation is ours. 1154 // and the turnouts have been set 1155 if (checkAllocationsAhead() && checkTurn(getAllocatedSectionForSection(_nextSection))) { 1156 if (_activeTrain.getSignalType() == DispatcherFrame.SIGNALHEAD 1157 && _controllingSignal != null) { 1158 setSpeedBySignalHead(); 1159 } else if (_activeTrain.getSignalType() == DispatcherFrame.SIGNALMAST 1160 && _controllingSignalMast != null) { 1161 setSpeedBySignalMast(); 1162 } else { 1163 log.trace("{}:Set Speed by BlocksAllocated",_activeTrain.getActiveTrainName()); 1164 setSpeedBySectionsAllocated(); 1165 } 1166 checkForGhost(); 1167 } else { 1168 // This might be the last section.... 1169 if (_currentAllocatedSection != null && _currentAllocatedSection.getNextSection() == null) { 1170 stopInCurrentSection(END_TRAIN, StopContext.DESTINATION); 1171 } else { 1172 // This will stop it. 1173 stopInCurrentSection(NO_TASK); 1174 log.debug("{}:Set Stop",_activeTrain.getActiveTrainName()); 1175 waitingOnAllocation = true; // flag setSpeedBySignal required when another allocation made. 1176 } 1177 } 1178 } 1179 1180 private void checkForGhost() { 1181 if (!canSpeedBeSetOrChecked()) { 1182 log.trace("[{}]:cannot check for ghost.",getActiveTrain().getActiveTrainName()); 1183 return; 1184 } 1185 if ( !(getTargetSpeed() == 0.0f || isStopping()) 1186 && _nextBlock != null 1187 && _currentBlock != null 1188 && _nextBlock.getSensor() != null 1189 && _nextBlock.getIsGhost()) { 1190 if ( _currentBlock.getIsGhost()) { 1191 log.error("Stopping due to two consecutive no sensor blocks [{}], [{}]", 1192 _currentBlock.getDisplayName(), _nextBlock.getDisplayName()); 1193 } else { 1194 try { 1195 _currentBlock.addPropertyChangeListener(new DarkTerritoryListener(_nextBlock.getSensor())); 1196 _nextBlock.getSensor().setKnownState(Sensor.ACTIVE); 1197 } catch (jmri.JmriException ex) { 1198 log.error("Error entering darkterratory"); 1199 } 1200 } 1201 } 1202 } 1203 1204 /* 1205 * Check at least the next section is allocated 1206 */ 1207 private boolean checkAllocationsAhead() { 1208 if (_nextSection != null) { 1209 // Check that next section is allocated... 1210 for (AllocatedSection allocatedSection : _activeTrain.getAllocatedSectionList()) { 1211 if (allocatedSection.getSection() == _nextSection) 1212 return true; 1213 } 1214 } 1215 return false; 1216 } 1217 1218 private void setSpeedBySectionsAllocated() { 1219 if (!canSpeedBeSetOrChecked()) { 1220 log.trace("[{}]:cannot set speed.",getActiveTrain().getActiveTrainName()); 1221 return; 1222 } 1223 1224 if (_stoppingByBlockOccupancy && (_stoppingBlock != null && _stoppingBlock.getState() == Block.UNOCCUPIED)) 1225 // we are awaiting a delayed stop 1226 return; 1227 int sectionsAhead = 0; 1228 for (AllocatedSection allocatedSection : _activeTrain.getAllocatedSectionList()) { 1229 if (!allocatedSection.getEntered()) { 1230 sectionsAhead++; 1231 } 1232 } 1233 float newSpeed = 0.0f; 1234 log.debug("[{}:SectionsAhead[{}]",_activeTrain.getActiveTrainName() ,sectionsAhead); 1235 switch (sectionsAhead) { 1236 case 0: 1237 newSpeed = 0.0f; 1238 break; 1239 case 1: 1240 newSpeed = InstanceManager.getDefault(SignalSpeedMap.class) 1241 .getSpeed("Medium"); 1242 // .getSpeed(_dispatcher.getStoppingSpeedName()); 1243 _activeTrain.setStatus(ActiveTrain.RUNNING); 1244 break; 1245 default: 1246 newSpeed = InstanceManager.getDefault(SignalSpeedMap.class) 1247 .getSpeed("Normal"); 1248 // .getSpeed(_dispatcher.getStoppingSpeedName()); 1249 _activeTrain.setStatus(ActiveTrain.RUNNING); 1250 } 1251 if (_dispatcher.getUseOccupiedTrackSpeed()) { 1252 newSpeed = getMinSpeedOfOccupiedBlocks(newSpeed); 1253 } 1254 // see if needs to slow for next block. 1255 if (newSpeed > 0 && _nextBlock != null) { 1256 float speed = getSpeedFromBlock(_nextBlock); 1257 if (speed < newSpeed) { 1258 // slow for next block 1259 newSpeed = speed; 1260 } 1261 } 1262 if (newSpeed > 0) { 1263 log.trace("setSpeedBySectionsAllocated isStopping[{}]",isStopping()); 1264 cancelStopInCurrentSection(); 1265 setTargetSpeed(getThrottleSettingFromSpeed(newSpeed)); 1266 } else { 1267 waitingOnAllocation = true; 1268 stopInCurrentSection(NO_TASK); 1269 } 1270 } 1271 1272 // Check for speed of incoming blocks. 1273 // in and out speed in is throttle percent. 1274 private float getMinSpeedOfOccupiedBlocks(float speed) { 1275 if (!_dispatcher.getUseOccupiedTrackSpeed()) 1276 return speed; 1277 // get slowest speed of any entered and still occupied 1278 // or entered but not released (HEADONLY / HEADANDTAIL 1279 float newSpeed = speed; 1280 for (AllocatedSection asE : _activeTrain.getAllocatedSectionList()) { 1281 if (asE.getEntered()) { 1282 for (Block b : asE.getSection().getBlockList()) { 1283 if (b.getState() == Block.OCCUPIED 1284 || _activeTrain.getTrainDetection() != TrainDetection.TRAINDETECTION_WHOLETRAIN ) { 1285 if (getSpeedFromBlock(b) < newSpeed) { 1286 newSpeed = getSpeedFromBlock(b); 1287 } 1288 } 1289 } 1290 } 1291 } 1292 log.trace("{}: getMinSpeedOfOccupiedBlocks Org Speed [{}] New [{}]", 1293 _activeTrain.getActiveTrainName(), speed, newSpeed); 1294 return newSpeed; 1295 } 1296 1297 /** 1298 * Check that all turnouts in a section have finished setting 1299 * for passage. If not listens on first bad turnout 1300 * and rechecks when set. 1301 * @param as Allocated section whose turnouts need to be checked. 1302 * @return true if no errors else false 1303 */ 1304 private boolean checkTurn(AllocatedSection as) { 1305 if (as != null && as.getAutoTurnoutsResponse() != null) { 1306 if (_turnoutStateNeeded != null && _turnoutStateListener != null) { 1307 _turnoutStateNeeded.removePropertyChangeListener("KnownState",_turnoutStateListener); 1308 _turnoutStateNeeded = null; 1309 _turnoutStateListener =null; 1310 } 1311 _turnoutStateNeeded = _dispatcher.getAutoTurnoutsHelper().checkStateAgainstList(as.getAutoTurnoutsResponse()); 1312 if (_turnoutStateNeeded != null) { 1313 _turnoutStateNeeded.addPropertyChangeListener("KnownState",_turnoutStateListener = (PropertyChangeEvent e) -> { 1314 _turnoutStateNeeded.removePropertyChangeListener("KnownState",_turnoutStateListener); 1315 _turnoutStateListener=null; 1316 _turnoutStateNeeded=null; 1317 setSpeedBySignal(); 1318 }); 1319 return false; 1320 } 1321 } 1322 return true; 1323 } 1324 1325 private void setSpeedBySignalMast() { 1326 //Set speed using SignalMasts; 1327 if (_controllingSignalMast == null) { 1328 // temporarily revert to by sections allocated 1329 setSpeedBySectionsAllocated(); 1330 return; 1331 } 1332 String displayedAspect = _controllingSignalMast.getAspect(); 1333 if (log.isTraceEnabled()) { 1334 log.trace("{}: Controlling mast {} ({})", _activeTrain.getTrainName(), _controllingSignalMast.getDisplayName(USERSYS), displayedAspect); 1335 if (_conSignalProtectedBlock == null) { 1336 log.trace("{}: Protected block is null", _activeTrain.getTrainName()); 1337 } else { 1338 log.trace("{}: Protected block: {} state: {} speed: {}", _activeTrain.getTrainName(), 1339 _conSignalProtectedBlock.getSensor().getDisplayName(USERSYS), 1340 (_conSignalProtectedBlock.getSensor().getState() == Block.OCCUPIED ? "OCCUPIED" : "NOT OCCUPIED"), 1341 _conSignalProtectedBlock.getBlockSpeed()); 1342 } 1343 } 1344 1345 if ((_controllingSignalMast.getAppearanceMap().getSpecificAppearance(SignalAppearanceMap.DANGER).equals(displayedAspect)) 1346 || !_controllingSignalMast.getLit() || _controllingSignalMast.getHeld()) { 1347 checkForSignalPassedOrStop(_controllingSignalMast.getDisplayName(USERSYS)); 1348 } else if (_controllingSignalMast.getAppearanceMap().getSpecificAppearance(SignalAppearanceMap.PERMISSIVE) != null 1349 && _controllingSignalMast.getAppearanceMap().getSpecificAppearance(SignalAppearanceMap.PERMISSIVE).equals(displayedAspect)) { 1350 setTargetSpeedState(RESTRICTED_SPEED); 1351 _activeTrain.setStatus(ActiveTrain.RUNNING); 1352 } else { 1353 1354 //if using signalmasts, set speed to lesser of aspect speed and signalmastlogic speed 1355 // (minimum speed on the path to next signal, using turnout and block speeds) 1356 String aspectSpeedStr = (String) _controllingSignalMast.getSignalSystem().getProperty(displayedAspect, "speed"); 1357 log.trace("{}: Signal {} speed {} for aspect {}", _activeTrain.getTrainName(), _controllingSignalMast.getDisplayName(USERSYS), aspectSpeedStr, displayedAspect); 1358 float speed = -1.0f; 1359 if (aspectSpeedStr != null) { 1360 try { 1361 speed = Float.parseFloat(aspectSpeedStr); 1362 } catch (NumberFormatException nx) { 1363 try { 1364 speed = InstanceManager.getDefault(SignalSpeedMap.class).getSpeed(aspectSpeedStr); 1365 log.trace("{}: Signal {} speed from map for {} is {}", _activeTrain.getTrainName(), _controllingSignalMast.getDisplayName(USERSYS), aspectSpeedStr, speed); 1366 } catch (IllegalArgumentException ex) { 1367 //Considered Normal if the speed does not appear in the map 1368 log.trace("{}: Speed not found {}", _activeTrain.getTrainName(), aspectSpeedStr); 1369 } 1370 } 1371 } 1372 int aspectSpeed = (int) speed; //save for debug message 1373 1374 //get maximum speed for the route between current and next signalmasts 1375 float smLogicSpeed = -1.0f; 1376 String smDestinationName = "unknown"; 1377 SignalMastLogic smLogic = InstanceManager.getDefault(SignalMastLogicManager.class).getSignalMastLogic(_controllingSignalMast); 1378 if (smLogic != null) { 1379 SignalMast smDestination = smLogic.getActiveDestination(); 1380 if (smDestination != null) { 1381 smDestinationName = smDestination.getDisplayName(USERSYS); 1382 smLogicSpeed = (int) smLogic.getMaximumSpeed(smDestination); 1383 } 1384 } 1385 1386 //use the smaller of aspect speed or route speed 1387 if (smLogicSpeed > -1.0f && smLogicSpeed < speed) { 1388 speed = smLogicSpeed; 1389 } 1390 1391 log.debug("{}: {}({}) {}({}), Dest: {}, path max: {}", 1392 _activeTrain.getTrainName(), 1393 _controllingSignalMast.getDisplayName(USERSYS), displayedAspect, aspectSpeedStr, aspectSpeed, 1394 smDestinationName, (int) smLogicSpeed); 1395 // Adjust for occupied blocks. 1396 if (_dispatcher.getUseOccupiedTrackSpeed()) { 1397 speed = getMinSpeedOfOccupiedBlocks(speed); 1398 } 1399 if (speed > -1.0f) { 1400 /* We should work on the basis that the speed required in the current block/section is governed by the signalmast 1401 that we have passed and not the one we are approaching when we are accelerating. 1402 However when we are decelerating we should be aiming to meet the speed required by the approaching signalmast 1403 whether that is to slow down or come to a complete stand still. 1404 */ 1405 if (prevSpeed == -1 || speed < prevSpeed) { 1406 log.debug("{}: Signal {} setting speed to {} for next", _activeTrain.getTrainName(), 1407 _controllingSignalMast.getDisplayName(USERSYS), speed); 1408 setTargetSpeedValue(speed); 1409 } else { 1410 log.debug("{}: Signal {} setting speed to {} for previous", _activeTrain.getTrainName(), 1411 _controllingSignalMast.getDisplayName(USERSYS), speed); 1412 setTargetSpeedValue(prevSpeed); 1413 } 1414 prevSpeed = speed; 1415 _activeTrain.setStatus(ActiveTrain.RUNNING); 1416 1417 } else { 1418 log.warn("{}: No specific speeds found so will use the default", _activeTrain.getTrainName()); 1419 setTargetSpeedState(NORMAL_SPEED); 1420 _activeTrain.setStatus(ActiveTrain.RUNNING); 1421 } 1422 } 1423 } 1424 1425 private void setSpeedBySignalHead() { 1426 // a held signal always stop 1427 if ( _controllingSignal != null && _controllingSignal.getAppearance() == SignalHead.HELD ) { 1428 // Held - Stop 1429 stopInCurrentSection(NO_TASK, StopContext.SIGNAL); 1430 return; 1431 } 1432 1433 if (useSpeedProfile) { 1434 // find speed from signal. 1435 // find speed from block 1436 // use least 1437 float blockSpeed = getSpeedFromBlock(_conSignalProtectedBlock); 1438 1439 float signalSpeed; 1440 String signalSpeedName; 1441 String displayedAspect = _controllingSignal.getAppearanceName(); 1442 try { 1443 signalSpeedName = 1444 InstanceManager.getDefault(SignalSpeedMap.class).getAppearanceSpeed(displayedAspect); 1445 signalSpeed = InstanceManager.getDefault(SignalSpeedMap.class).getSpeed(signalSpeedName); 1446 } catch (Throwable ex) { // if _anything_ goes wrong, contain it 1447 signalSpeed = -1.0f; 1448 log.warn("{}: Block {} AppearanceSpeed {} not found in SignalSpeedMap", 1449 _activeTrain.getTrainName(), _conSignalProtectedBlock.getDisplayName(USERSYS), displayedAspect); 1450 } 1451 float useSpeed; 1452 if (blockSpeed < signalSpeed) { 1453 useSpeed = blockSpeed; 1454 } else { 1455 useSpeed = signalSpeed; 1456 } 1457 1458 log.trace("BlockSpeed[{}] SignalSpeed[{}]", blockSpeed, signalSpeed); 1459 if (useSpeed < 0.01f) { 1460 checkForSignalPassedOrStop(_controllingSignal.getDisplayName(USERSYS)); 1461 } else { 1462 setTargetSpeedByProfile(useSpeed,_stopBySpeedProfileAdjust,true); 1463 } 1464 } else { 1465 switch (_controllingSignal.getAppearance()) { 1466 case SignalHead.DARK: 1467 case SignalHead.RED: 1468 case SignalHead.FLASHRED: 1469 // May get here from signal changing before Block knows it is occupied, so must 1470 // check Block occupancy sensor, which must change before signal. 1471 // check to to see if its allocated to us!!! 1472 // check Block occupancy sensor if it is in an allocated block, which must change before signal 1473 // If the train has no _currentAllocatedSection it is in a first block outside transit. 1474 checkForSignalPassedOrStop(_controllingSignal.getDisplayName(USERSYS)); 1475 break; 1476 case SignalHead.YELLOW: 1477 case SignalHead.FLASHYELLOW: 1478 setTargetSpeedState(SLOW_SPEED); 1479 _activeTrain.setStatus(ActiveTrain.RUNNING); 1480 break; 1481 case SignalHead.GREEN: 1482 case SignalHead.FLASHGREEN: 1483 setTargetSpeedState(NORMAL_SPEED); 1484 _activeTrain.setStatus(ActiveTrain.RUNNING); 1485 break; 1486 case SignalHead.LUNAR: 1487 case SignalHead.FLASHLUNAR: 1488 setTargetSpeedState(RESTRICTED_SPEED); 1489 _activeTrain.setStatus(ActiveTrain.RUNNING); 1490 break; 1491 default: 1492 log.warn("Signal Head[{}] has invalid Appearence - using stop",_controllingSignal.getAppearance()); 1493 stopInCurrentSection(NO_TASK, StopContext.SIGNAL); 1494 } 1495 1496 } 1497 } 1498 1499 /** 1500 * Check to see if a stop is really required, or if this is the 1501 * signal head that was just passed, in which case ignore as the signal goes red before a 1502 * new signal exists. 1503 * 1504 * @param displayName name of signal for debug messages. 1505 */ 1506 private void checkForSignalPassedOrStop(String displayName) { 1507 // if current section is null we are in a pre transit block. 1508 if (_currentAllocatedSection != null) { 1509 if ((_currentAllocatedSection.isInActiveBlockList(_conSignalProtectedBlock) || 1510 (_nextSection != null && _activeTrain.isInAllocatedList(_nextSection) && _nextSection.containsBlock(_conSignalProtectedBlock))) 1511 && _conSignalProtectedBlock.getSensor().getState() == Block.OCCUPIED) { 1512 // Train has just passed this signal - ignore this signal 1513 log.debug("{}: _conSignalProtectedBlock [{}] for signal [{}] is the block just past so ignore.", _activeTrain.getTrainName(), 1514 _conSignalProtectedBlock.getDisplayName(USERSYS), displayName); 1515 } else { 1516 log.debug("{}: stopping for signal [{}] ", _activeTrain.getTrainName(), 1517 displayName); 1518 stopInCurrentSection(NO_TASK); 1519 } 1520 } 1521 } 1522 1523 protected float getSpeedFromBlock(Block block) { 1524 String blockSpeedName = block.getBlockSpeed(); 1525 if (blockSpeedName.contains("Global")) { 1526 blockSpeedName = InstanceManager.getDefault(BlockManager.class).getDefaultSpeed(); 1527 } 1528 float blockSpeed = -1.0f; 1529 if (!blockSpeedName.isEmpty()) { 1530 try { 1531 blockSpeed = Float.parseFloat(blockSpeedName); 1532 } catch (NumberFormatException nx) { 1533 try { 1534 blockSpeed = InstanceManager.getDefault(SignalSpeedMap.class).getSpeed(blockSpeedName); 1535 log.debug("{} {}: block speed from map for {} is {}", 1536 _activeTrain.getTrainName(), block.getDisplayName(USERSYS), blockSpeedName, 1537 blockSpeed); 1538 } catch (Throwable ex) { // if _anything_ goes wrong, contain it 1539 //Considered Normal if the speed does not appear in the map 1540 log.warn("{}: Block {} Speed {} not found in SignalSpeedMap", 1541 _activeTrain.getTrainName(), block.getDisplayName(USERSYS), blockSpeed); 1542 } 1543 } 1544 } 1545 return blockSpeed; 1546 } 1547 1548 float prevSpeed = -1.0f; 1549 1550 // called to cancel a stopping action that is in progress 1551 private synchronized void cancelStopInCurrentSection() { 1552 log.trace("[{}]:Cancel Stopping", _activeTrain.getTrainName()); 1553 // Cancel any pending or in-progress stop-by-distance / profile-based stopping schedules. 1554 // If conditions improve (signal clears or allocations become available), we must be able to cancel 1555 // the stopping sequence and resume running. 1556 _distanceStopPending = false; 1557 _distanceStopPendingToMin = false; 1558 _distanceStopPendingMm = 0.0f; 1559 _distanceStopPendingTask = NO_TASK; 1560 if (re != null && re.getSpeedProfile() != null) { 1561 try { 1562 re.getSpeedProfile().cancelSpeedChange(); 1563 } catch (RuntimeException ex) { 1564 log.warn("{}: cancelSpeedChange failed while cancelling stop", _activeTrain.getTrainName(), ex); 1565 } 1566 } 1567 1568 cancelStoppingBySensor(); 1569 _stoppingByBlockOccupancy = false; 1570 _stoppingBlock = null; 1571 _stoppingUsingSpeedProfile = false; 1572 if (_autoEngineer != null) { 1573 _autoEngineer.slowToStop(false); 1574 } 1575 } 1576 1577 /** Clamp throttle [% 0..1] */ 1578 private static float clampThrottle(float pct) { 1579 if (pct < 0.0f) return 0.0f; 1580 if (pct > 1.0f) return 1.0f; 1581 return pct; 1582 } 1583 1584 private enum StopContext { 1585 DESTINATION, 1586 SIGNAL, 1587 OTHER 1588 } 1589 1590 private synchronized void stopInCurrentSection(int task) { 1591 stopInCurrentSection(task, StopContext.OTHER); 1592 } 1593 1594 private synchronized void stopInCurrentSection(int task, StopContext context) { 1595 if (_currentAllocatedSection == null) { 1596 log.error("{}: Current allocated section null on entry to stopInCurrentSection", _activeTrain.getTrainName()); 1597 setStopNow(); 1598 return; 1599 } 1600 1601 log.debug("{}: StopInCurrentSection called for {} task[{}] targetspeed[{}]", _activeTrain.getTrainName(), 1602 _currentAllocatedSection.getSection().getDisplayName(USERSYS), task, getTargetSpeed()); 1603 1604 if (((_autoEngineer != null) && _autoEngineer.isStopped()) || isStopping()) { 1605 log.debug("{}: train is already stopped or stopping.", _activeTrain.getTrainName()); 1606 // ignore if train is already stopped or if stopping is in progress 1607 return; 1608 } 1609 1610 1611 /* ======================================================================= 1612 * Distance-based stopping (destination section only) — custom planner. 1613 * We compute a constant-deceleration braking curve to stop exactly at 'distanceMm' 1614 * and drive the throttle ourselves via AutoEngineer.setSpeedImmediate(...). 1615 * 1616 * No dependency on RosterSpeedProfile.changeLocoSpeed or AutoEngineer.setTargetSpeed(distance,...). 1617 * We only read profile speeds via re.getSpeedProfile().getDistanceTravelled(...) to invert throttle ↔ mm/s. 1618 * 1619 * TODO (future): extend to signal stop points inside sections using the same controller, 1620 * with an explicit per-section stop origin. 1621 * ======================================================================= */ 1622 // Distance-based stopping is currently applied only to destination/platform-style stops. 1623 // For signal-driven and other stops, preserve existing Dispatcher stop behavior. 1624 boolean allowDistanceStop = (context == StopContext.DESTINATION); 1625 1626 if (allowDistanceStop) { 1627 boolean distanceEnabled = (_stopByDistanceMm > 0.0f); 1628 // Direction-aware profile availability (we must have speeds for the current direction) 1629 boolean profileAvailable = false; 1630 if (re != null && re.getSpeedProfile() != null) { 1631 boolean forward = _autoEngineer.getIsForward(); 1632 profileAvailable = forward ? re.getSpeedProfile().hasForwardSpeeds() 1633 : re.getSpeedProfile().hasReverseSpeeds(); 1634 } 1635 1636 // Resolve the section's stopping sensor for the current travel direction (do not mutate _stopSensor yet) 1637 Sensor stopSensorCandidate = null; 1638 if (_currentAllocatedSection != null) { 1639 if (_currentAllocatedSection.getSection().getState() == Section.FORWARD) { 1640 stopSensorCandidate = _currentAllocatedSection.getSection().getForwardStoppingSensor(); 1641 } else { 1642 stopSensorCandidate = _currentAllocatedSection.getSection().getReverseStoppingSensor(); 1643 } 1644 } 1645 1646 // Refresh override flag in case it changed (e.g., user updated the Train Info while running) 1647 this._useStopSensor = _activeTrain.getUseStopSensor(); 1648 1649 // Combined mode = user opted into Stop-by-Distance, profile is available, and a stopping sensor is present & in use 1650 boolean combinedMode = distanceEnabled && profileAvailable && (_useStopSensor) && (stopSensorCandidate != null); 1651 1652 // DEBUG 1653 log.debug("{}: stopInSection - distEnabled={}, profileAvail={}, sensorPresent={}, useStopSensor={}, combined={}", 1654 _activeTrain.getTrainName(), distanceEnabled, profileAvailable, (stopSensorCandidate != null), _useStopSensor, combinedMode); 1655 1656 if ((distanceEnabled && profileAvailable) && !_stoppingUsingSpeedProfile && !_distanceStopPending) { 1657 1658 // Compute requested travel distance from section entry to stop reference 1659 1660 float distanceMmBase = _stopByDistanceMm + (_stopByDistanceRefTail ? getMaxTrainLengthMM() : 0.0f); 1661 // Safety: do not allow stop-by-distance to extend beyond the destination section. 1662 // This prevents overrunning into the next section / train ahead when a large distance is configured. 1663 float sectionLenMm = (_currentAllocatedSection != null) ? _currentAllocatedSection.getActualLength() : 0.0f; 1664 if (sectionLenMm > 0.0f && distanceMmBase > sectionLenMm) { 1665 log.warn("{}: stop-by-distance {}mm exceeds section length {}mm; clamping to section length.", 1666 _activeTrain.getTrainName(), Float.valueOf(distanceMmBase), Float.valueOf(sectionLenMm)); 1667 distanceMmBase = sectionLenMm; 1668 } 1669 1670 if (combinedMode) { 1671 // --- New combined behaviour --- 1672 // We will decelerate to MinimumReliableOperatingSpeed within distanceMmBase, then hold until the stop sensor fires. 1673 1674 // Decide whether to start NOW (already past the section entry) or ARM to start at the entry block 1675 Block enter = (_currentAllocatedSection != null) 1676 ? _currentAllocatedSection.getEnterBlock(_previousAllocatedSection) 1677 : null; 1678 1679 if (enter == null || enter.getState() == Block.OCCUPIED) { 1680 // Start immediately from current position (adjust remaining distance if we’re already partway in) 1681 float remainingMm = distanceMmBase; 1682 if (_currentAllocatedSection != null && _currentBlock != null) { 1683 float sectionLen = _currentAllocatedSection.getActualLength(); 1684 float lenRemaining = _currentAllocatedSection.getLengthRemaining(_currentBlock); 1685 float progressed = Math.max(0.0f, sectionLen - lenRemaining); 1686 remainingMm = distanceMmBase - progressed; 1687 } 1688 if (remainingMm <= 0.0f) { 1689 // Already at/inside the target – assert crawl and fall through to sensor wait 1690 float vMin = 1691 re.getSpeedProfile().getSpeed(_minReliableOperatingSpeed, _autoEngineer.getIsForward()); 1692 float thrMin = re.getSpeedProfile().getThrottleSetting(vMin, _autoEngineer.getIsForward()); 1693 1694 // Quantize to a real throttle step to avoid values between 0 and the first speed step. 1695 float q = clampThrottle(thrMin); 1696 float inc = getThrottle().getSpeedIncrement(); 1697 if (inc > 0.0f) { 1698 int steps = Math.round(q / inc); 1699 q = steps * inc; 1700 if (q > 0.0f && q < inc) 1701 q = inc; 1702 q = clampThrottle(q); 1703 } 1704 _autoEngineer.setSpeedImmediate(q); 1705 } else { 1706 // Cancel first; then mark that we are in a distance-based stop (suppresses setSpeedBySignal correctly) 1707 cancelStopInCurrentSection(); 1708 _stoppingUsingSpeedProfile = true; // suppress setSpeedBySignal until done 1709 re.getSpeedProfile().setMinMaxLimitsKmh(_minReliableOperatingSpeed, _maxSpeed, 1710 _maxSpeedScaleKmh, (float) _dispatcher.getScale().getScaleRatio(), 1711 _autoEngineer.getIsForward()); 1712 // Combined mode: approach to MIN over 'remainingMm', then stop by the section stop sensor. 1713 // (Use correct API and throttle accessor; do NOT return here.) 1714 re.getSpeedProfile().planApproachToMinOverDistanceThenStopBySensor( 1715 getThrottle(), remainingMm, stopSensorCandidate, _speedFactor); 1716 1717 // Do NOT start the legacy DistanceStopController in combined mode. 1718 } 1719 1720 // Now arm the stop sensor, but do NOT pre-lower to a generic stopping speed 1721 _stopSensor = stopSensorCandidate; 1722 if (_stopSensor.getKnownState() == Sensor.ACTIVE) { 1723 setStopNow(); // sensor is already made – stop immediately 1724 } else { 1725 _stopSensor.addPropertyChangeListener(_stopSensorListener = (java.beans.PropertyChangeEvent e) -> { 1726 handleStopSensorChange(e); 1727 }); 1728 _stoppingBySensor = true; 1729 } 1730 // Ensure stop tasks/termination run when the train actually stops. 1731 Runnable __waitForStop = new WaitForTrainToStop(task); 1732 Thread __tWait = jmri.util.ThreadingUtil.newThread(__waitForStop, 1733 "Wait for stop " + getActiveTrain().getActiveTrainName()); 1734 __tWait.start(); 1735 1736 return; // combined branch handled 1737 } 1738 1739 // Not yet at the section entry: arm a pending approach-to-min plan and the stop sensor listener now 1740 _distanceStopPending = true; 1741 _distanceStopPendingToMin = true; 1742 _distanceStopPendingMm = distanceMmBase; 1743 _distanceStopPendingTask = task; 1744 1745 _stopSensor = stopSensorCandidate; 1746 if (_stopSensor.getKnownState() == Sensor.ACTIVE) { 1747 setStopNow(); 1748 } else { 1749 _stopSensor.addPropertyChangeListener(_stopSensorListener = (java.beans.PropertyChangeEvent e) -> { 1750 handleStopSensorChange(e); 1751 }); 1752 _stoppingBySensor = true; 1753 } 1754 // Ensure stop tasks/termination run when the train actually stops. 1755 Runnable __waitForStop = new WaitForTrainToStop(_distanceStopPendingTask); 1756 Thread __tWait = jmri.util.ThreadingUtil.newThread(__waitForStop, 1757 "Wait for stop " + getActiveTrain().getActiveTrainName()); 1758 __tWait.start(); 1759 1760 return; // wait for entry OCCUPIED to start the approach-to-min plan 1761 } 1762 1763 // --- Legacy pure distance stop (ramp to ZERO at the distance) --- 1764 // Case A/B logic (start now or arm pending), just like before. 1765 Block enter = (_currentAllocatedSection != null) 1766 ? _currentAllocatedSection.getEnterBlock(_previousAllocatedSection) 1767 : null; 1768 1769 if (enter == null || enter.getState() == Block.OCCUPIED) { 1770 float remainingMm = distanceMmBase; 1771 if (_currentAllocatedSection != null && _currentBlock != null) { 1772 float sectionLen = _currentAllocatedSection.getActualLength(); 1773 float lenRemaining = _currentAllocatedSection.getLengthRemaining(_currentBlock); 1774 float progressed = Math.max(0.0f, sectionLen - lenRemaining); 1775 remainingMm = distanceMmBase - progressed; 1776 } 1777 if (remainingMm <= 0.0f) { 1778 setStopNow(); 1779 } else { 1780 _stoppingUsingSpeedProfile = true; 1781 cancelStopInCurrentSection(); 1782 1783 re.getSpeedProfile().setMinMaxLimitsKmh(_minReliableOperatingSpeed, _maxSpeed, _maxSpeedScaleKmh, 1784 (float) _dispatcher.getScale().getScaleRatio(), _autoEngineer.getIsForward()); 1785 // Delegate pure distance stop-to-zero to RosterSpeedProfile 1786 re.getSpeedProfile().planStopToZeroOverDistance(getThrottle(), remainingMm, _speedFactor); 1787 Thread tWait = jmri.util.ThreadingUtil.newThread(new WaitForTrainToStop(task), 1788 "Wait for stop " + getActiveTrain().getActiveTrainName()); 1789 tWait.start(); 1790 } 1791 return; 1792 1793 } 1794 1795 // Arm pending pure distance stop 1796 _distanceStopPending = true; 1797 _distanceStopPendingToMin = false; 1798 _distanceStopPendingMm = distanceMmBase; 1799 _distanceStopPendingTask = task; 1800 return; 1801 } 1802 1803 } 1804 1805 // ======================================================================= 1806 // Do not exit before destination stop logic; 1807 // only bail out if the train is already at zero AND no profile/distance stop is requested. 1808 if (getTargetSpeed() == 0.0f && !_stopBySpeedProfile && _stopByDistanceMm <= 0.0f) { 1809 log.debug("{}: already stopped and no planned stop requested — skipping stop planning.", _activeTrain.getTrainName()); 1810 return; 1811 } 1812 // if Section has stopping sensors, use them 1813 if (_currentAllocatedSection.getSection().getState() == Section.FORWARD) { 1814 _stopSensor = _currentAllocatedSection.getSection().getForwardStoppingSensor(); 1815 } else { 1816 _stopSensor = _currentAllocatedSection.getSection().getReverseStoppingSensor(); 1817 } 1818 // DEBUG 1819 if (_stopSensor != null && !_useStopSensor) { 1820 log.debug("{}: Override enabled - ignoring section stop sensor {}", 1821 _activeTrain.getTrainName(), _stopSensor.getDisplayName(USERSYS)); 1822 } 1823 if (_stopSensor != null && _useStopSensor) { 1824 if (_stopSensor.getKnownState() == Sensor.ACTIVE) { 1825 // stop sensor is already active, stop now 1826 setStopNow(); 1827 } else { 1828 setDecreasedSpeedBeforeStop(); 1829 _stopSensor.addPropertyChangeListener(_stopSensorListener = (java.beans.PropertyChangeEvent e) -> { 1830 handleStopSensorChange(e); 1831 }); 1832 _stoppingBySensor = true; 1833 } 1834 } else if (useSpeedProfile && _stopBySpeedProfile) { 1835 log.debug("{}: Section [{}] Section Length[{}] Max Train Length [{}] StopBySpeedProfile [{}]. setStopNow", _activeTrain.getTrainName(), 1836 _currentAllocatedSection.getSection().getDisplayName(USERSYS), _currentAllocatedSection.getActualLength(), getMaxTrainLengthMM(), _stopBySpeedProfile); 1837 // stopping by speed profile uses section length to stop 1838 1839 setTargetSpeedState(STOP_SPEED,useSpeedProfile); 1840 1841 } else if (_currentAllocatedSection.getActualLength() < getMaxTrainLengthMM()) { 1842 log.debug("{}: Section [{}] Section Length[{}] Max Train Length [{}]. setStopNow({})", 1843 _activeTrain.getTrainName(), 1844 _currentAllocatedSection.getSection().getDisplayName(USERSYS), 1845 _currentAllocatedSection.getActualLength(), 1846 getMaxTrainLengthMM(), _stopBySpeedProfile); 1847 // train will not fit comfortably in the Section, stop it immediately 1848 setStopNow(); 1849 } else if (_activeTrain.getTrainDetection() == TrainDetection.TRAINDETECTION_WHOLETRAIN) { 1850 log.debug("{}: train will fit in [{}] ({}>={}), stop when prev block clears.", _activeTrain.getTrainName(), 1851 _currentAllocatedSection.getSection().getDisplayName(USERSYS), _currentAllocatedSection.getActualLength(), getMaxTrainLengthMM()); 1852 // train will fit in current allocated Section and has resistance wheels 1853 // try to stop by watching Section Block occupancy 1854 if (_currentAllocatedSection.getSection().getNumBlocks() == 1) { 1855 if (_previousAllocatedSection != null) { 1856 Block tBlock; 1857 // just because current section has one block does not mean the previous one did. 1858 if (_previousAllocatedSection.getSection().getNumBlocks() == 1) { 1859 tBlock = _previousAllocatedSection.getSection().getLastBlock(); 1860 } else { 1861 tBlock = _previousAllocatedSection.getSection().getExitBlock(); 1862 } 1863 if ((tBlock != null) && (tBlock.getState() == Block.OCCUPIED)) { 1864 _stoppingBlock = tBlock; 1865 setStopByBlockOccupancy(false); 1866 } else { 1867 setStopNow(); 1868 } 1869 } else { 1870 setStopNow(); 1871 } 1872 } else { 1873 // Section has multiple blocks 1874 Block exitBlock = _currentAllocatedSection.getExitBlock(); 1875 Block enterBlock = _currentAllocatedSection.getEnterBlock(_previousAllocatedSection); 1876 if (enterBlock == null) { 1877 // this is the first Section of the Transit, with train starting in this Section 1878 setStopNow(); 1879 } else if (exitBlock == enterBlock) { 1880 // entry and exit are from the same Block 1881 if ((_previousBlock != null) && (_previousBlock.getState() == Block.OCCUPIED) 1882 && (getBlockLength(exitBlock) > getMaxTrainLengthMM())) { 1883 _stoppingBlock = _previousBlock; 1884 setStopByBlockOccupancy(false); 1885 } else { 1886 setStopNow(); 1887 } 1888 } else { 1889 // try to move train as far into the Section as it will comfortably fit 1890 Block tstBlock = exitBlock; 1891 if (tstBlock == null) { 1892 if (_currentAllocatedSection.getDirection() == Section.REVERSE) { 1893 tstBlock = _currentAllocatedSection.getSection().getBlockBySequenceNumber(0); 1894 } else { 1895 tstBlock = _currentAllocatedSection.getSection().getBlockBySequenceNumber( 1896 _currentAllocatedSection.getSection().getNumBlocks() - 1); 1897 } 1898 } 1899 int tstLength = getBlockLength(tstBlock); 1900 int tstBlockSeq = _currentAllocatedSection.getSection().getBlockSequenceNumber(tstBlock); 1901 while ((tstLength < getMaxTrainLengthMM()) && (tstBlock != enterBlock)) { 1902 int newSeqNumber; 1903 if (_currentAllocatedSection.getDirection() == Section.REVERSE) { 1904 newSeqNumber = tstBlockSeq + 1; 1905 } else { 1906 newSeqNumber = tstBlockSeq - 1; 1907 } 1908 tstBlock = _currentAllocatedSection.getSection().getBlockBySequenceNumber(newSeqNumber); 1909 tstBlockSeq = newSeqNumber; 1910 tstLength += getBlockLength(tstBlock); 1911 } 1912 if (getMaxTrainLengthMM() > tstLength) { 1913 setStopNow(); 1914 } else if (tstBlock == enterBlock) { 1915 // train fits, but needs all available Blocks 1916 Block previousSectionExitBlock = _previousAllocatedSection.getExitBlock(); 1917 if ((previousSectionExitBlock != null) && (previousSectionExitBlock.getState() == Block.OCCUPIED)) { 1918 _stoppingBlock = previousSectionExitBlock; 1919 setStopByBlockOccupancy(true); 1920 } else { 1921 setStopNow(); 1922 } 1923 } else { 1924 // train fits, and doesn't need all available Blocks 1925 int xSeqNumber = tstBlockSeq + 1; 1926 if (_currentAllocatedSection.getDirection() == Section.FORWARD ) { 1927 xSeqNumber = tstBlockSeq - 1; 1928 } 1929 _stoppingBlock = _currentAllocatedSection.getSection(). 1930 getBlockBySequenceNumber(xSeqNumber); 1931 setStopByBlockOccupancy(true); 1932 } 1933 } 1934 } 1935 } else { 1936 // train will fit, but no way to stop it reliably 1937 setStopNow(); 1938 } 1939 1940 // even if no task is required it must be run 1941 // as cleanup happens after train stops. 1942 Runnable waitForStop = new WaitForTrainToStop(task); 1943 Thread tWait = jmri.util.ThreadingUtil.newThread(waitForStop, "Wait for stop " + getActiveTrain().getActiveTrainName()); 1944 tWait.start(); 1945 } 1946 1947 protected synchronized void executeStopTasks(int task) { 1948 // clean up stopping 1949 cancelStopInCurrentSection(); 1950 _stoppingUsingSpeedProfile = false; // queued stop has completed; allow normal speed logic again 1951 _dispatcher.queueReleaseOfCompletedAllocations(); 1952 log.trace("exec[{}]",task); 1953 switch (task) { 1954 case END_TRAIN: 1955 _activeTrain.setStatus(ActiveTrain.DONE); 1956 break; 1957 case NO_TASK: 1958 // clean up stop 1959 break; 1960 case END_REVERSAL: 1961 /* Reset _previousBlock to be the _currentBlock if we do a continious reverse otherwise the stop in block method fails 1962 to stop the loco in the correct block 1963 if the first block we come to has a stopped or held signal */ 1964 _activeTrain.setRestart(_activeTrain.getDelayReverseRestart(),_activeTrain.getReverseRestartDelay(), 1965 _activeTrain.getReverseRestartSensor(),_activeTrain.getResetReverseRestartSensor()); 1966 _activeTrain.setTransitReversed(true); 1967 _activeTrain.reverseAllAllocatedSections(); 1968 setEngineDirection(); 1969 _previousBlock = null; 1970 _nextBlock = getNextBlock(_currentBlock,_currentAllocatedSection); 1971 if (_activeTrain.getDelayReverseRestart() == ActiveTrain.NODELAY) { 1972 _activeTrain.holdAllocation(false); 1973 // a reversal can happen in mid section 1974 setupNewCurrentSignal(_currentAllocatedSection, true); 1975 setSpeedBySignal(); 1976 if ((_nextSection != null) && !_activeTrain.isInAllocatedList(_nextSection)) { 1977 _dispatcher.queueScanOfAllocationRequests(); 1978 break; 1979 } 1980 } 1981 break; 1982 case BEGINNING_RESET: 1983 _activeTrain.setRestart(_activeTrain.getDelayedRestart(),_activeTrain.getRestartDelay(), 1984 _activeTrain.getRestartSensor(),_activeTrain.getResetRestartSensor()); 1985 if (_activeTrain.getResetWhenDone()) { 1986 if (_activeTrain.getDelayedRestart() == ActiveTrain.NODELAY && !_activeTrain.getReverseAtEnd()) { 1987 log.error("[{}]: train is continueing without pause, should have been handled in handleBlockStateChange.",_activeTrain.getTrainName()); 1988 } else { 1989 // then active train is delayed 1990 _activeTrain.setTransitReversed(false); 1991 _activeTrain.resetAllAllocatedSections(); 1992 _previousBlock = null; 1993 _nextBlock = getNextBlock(_currentBlock,_currentAllocatedSection); 1994 setEngineDirection(); 1995 _activeTrain.setRestart(_activeTrain.getDelayedRestart(),_activeTrain.getRestartDelay(), 1996 _activeTrain.getRestartSensor(), _activeTrain.getResetRestartSensor()); 1997 if ((_nextSection != null) && !_activeTrain.isInAllocatedList(_nextSection)) { 1998 _dispatcher.queueScanOfAllocationRequests(); 1999 } 2000 // can be mid block 2001 setupNewCurrentSignal(null, true); 2002 setSpeedBySignal(); 2003 2004 } 2005 } else { 2006 // dispatcher cancelled auto restart while train was stopping? 2007 log.warn("[{}]resetWhenDone flag reset, likely user cancelling while processing stop", 2008 _activeTrain.getActiveTrainName()); 2009 } 2010 break; 2011 default: 2012 log.debug("[{}]Invalid action [{}] in executeStopTasksRequest to execute BEGINNING_RESET cancelled", _activeTrain.getActiveTrainName(),task); 2013 break; 2014 } 2015 } 2016 2017 /** 2018 * Remove the stopping sensor 2019 */ 2020 private void cancelStoppingBySensor() { 2021 if (_stopSensor != null) { 2022 _stopSensor.removePropertyChangeListener(_stopSensorListener); 2023 _stoppingBySensor = false; 2024 _stopSensorListener = null; 2025 _stopSensor = null; 2026 } 2027 } 2028 2029 /** 2030 * When the stopping sensor we are waiting on goes active 2031 * stop the train or set a new speed and destroy itself 2032 * @param e - the property change event 2033 */ 2034 private synchronized void handleStopSensorChange(java.beans.PropertyChangeEvent e) { 2035 if (e.getPropertyName().equals("KnownState") && (int) e.getNewValue() == Sensor.ACTIVE) { 2036 _stopSensor.removePropertyChangeListener(_stopSensorListener); 2037 _stoppingBySensor = false; 2038 _stopSensorListener = null; 2039 _stopSensor = null; 2040 if (_needSetSpeed) { 2041 _needSetSpeed = false; 2042 setSpeedBySignal(); 2043 } else { 2044 setStopNow(); 2045 } 2046 } 2047 } 2048 2049 private synchronized void setStopNow() { 2050 setStopNow(false); 2051 } 2052 2053 private synchronized void setStopNow(boolean useSpeedProfile) { 2054 setTargetSpeedState(STOP_SPEED,useSpeedProfile); 2055 if (_currentAllocatedSection == null) { // this may occur if the train is not in the selected block when initially created and the signal is held. 2056 _activeTrain.setStatus(ActiveTrain.WAITING); 2057 } else if (_currentAllocatedSection.getNextSection() == null) { 2058 // wait for train to stop - this lets action items complete in a timely fashion 2059 waitUntilStopped(); 2060 _activeTrain.setStatus(ActiveTrain.DONE); 2061 } else { 2062 _activeTrain.setStatus(ActiveTrain.WAITING); 2063 } 2064 } 2065 2066 /* 2067 * When multi block stopping, the stopping block may not be occupied yet. 2068 */ 2069 private void setStopByBlockOccupancy(boolean ignoreNotOccupied) { 2070 // note: _stoppingBlock must be set before invoking this method 2071 // verify that _stoppingBlock is actually occupied, if not stop immediately 2072 if (_stoppingBlock.getState() == Block.OCCUPIED || ignoreNotOccupied) { 2073 setDecreasedSpeedBeforeStop(); 2074 _stoppingByBlockOccupancy = true; 2075 } else { 2076 setStopNow(); 2077 } 2078 } 2079 2080 /** 2081 * Before stopping by sensor alone, or by clearing previous block, 2082 * set the speed to the user defined preference. 2083 */ 2084 private void setDecreasedSpeedBeforeStop() { 2085 float signalSpeed = 25; 2086 try { 2087 signalSpeed = InstanceManager.getDefault(SignalSpeedMap.class) 2088 .getSpeed(_dispatcher.getStoppingSpeedName()); 2089 } catch (IllegalArgumentException ex) { 2090 log.error("Missing [{}] from Speed table - defaulting to 25", 2091 _dispatcher.getStoppingSpeedName()); 2092 } 2093 if (getThrottleSettingFromSpeed(signalSpeed) < getTargetSpeed()) { 2094 if (useSpeedProfile) { 2095 // use 75 percent or normal amount, dont clear isstopping for ramping. 2096 setTargetSpeedByProfile(signalSpeed,_stopBySpeedProfileAdjust*0.75f,false); 2097 } else { 2098 setTargetSpeed(signalSpeed/100.0f); 2099 } 2100 } 2101 } 2102 2103 ///** 2104 // * Sets the throttle percent unless it is already less than the new setting 2105 // * @param throttleSetting Max ThrottleSetting required. 2106 // */ 2107 //private synchronized void setToAMaximumThrottle(float throttleSetting) { 2108 // if (throttleSetting < getTargetSpeed()) { 2109 // setTargetSpeed(throttleSetting); 2110 // } 2111 //} 2112 2113 /** 2114 * Calculates the throttle setting for a given speed. 2115 * @param speed the unadjusted speed. 2116 * @return - throttle setting (a percentage) 2117 */ 2118 private synchronized float getThrottleSettingFromSpeed(float speed) { 2119 if (useSpeedProfile) { 2120 float throttleSetting = _activeTrain.getRosterEntry().getSpeedProfile() 2121 .getThrottleSettingFromSignalMapSpeed(speed, getForward()); 2122 return throttleSetting; 2123 } 2124 if (_activeTrain.getSignalType() == DispatcherFrame.SIGNALMAST) { 2125 float mls; 2126 if (_controllingSignalMast != null) { 2127 mls = _controllingSignalMast.getSignalSystem().getMaximumLineSpeed(); 2128 } else { 2129 //plan B 2130 mls = _dispatcher.getMaximumLineSpeed(); 2131 } 2132 float throttleSetting = (speed / mls); 2133 return throttleSetting; 2134 } else 2135 return speed/100.0f; 2136 } 2137 2138 2139 /** 2140 * sets the throttle based on an index number into _speedRatio array 2141 * @param speedState Index value 2142 */ 2143 private synchronized void setTargetSpeedState(int speedState) { 2144 setTargetSpeedState(speedState,false); 2145 } 2146 2147 /** 2148 * sets the throttle based on an index number into _speedRatio array 2149 * @param speedState Index value 2150 * @param stopBySpeedProfile if true use speed profile 2151 */ 2152 private synchronized void setTargetSpeedState(int speedState,boolean stopBySpeedProfile) { 2153 log.trace("{}: setTargetSpeedState:({})",_activeTrain.getTrainName(),speedState); 2154 if (_currentAllocatedSection == null) { 2155 log.debug("_currentAllocatedSection == null in setTargetSpeedState"); 2156 return; 2157 } 2158 _autoEngineer.slowToStop(false); 2159 2160 float stoppingDistanceAdjust = _stopBySpeedProfileAdjust * 2161 ( _activeTrain.isTransitReversed() ? 2162 _currentAllocatedSection.getTransitSection().getRevStopPerCent() : 2163 _currentAllocatedSection.getTransitSection().getFwdStopPerCent()); 2164 log.debug("stoppingDistanceAdjust[{}] isReversed[{}] stopBySpeedProfileAdjust[{}]",stoppingDistanceAdjust, 2165 _activeTrain.isTransitReversed(),_stopBySpeedProfileAdjust ); 2166 if (speedState > STOP_SPEED) { 2167 cancelStopInCurrentSection(); 2168 if (_currentRampRate == RAMP_SPEEDPROFILE && useSpeedProfile) { 2169 // we are going to ramp up / down using section length and speed profile 2170 _autoEngineer.setTargetSpeed(_currentAllocatedSection.getLengthRemaining(_currentBlock) 2171 * stoppingDistanceAdjust, speedState); 2172 } else { 2173 setTargetSpeed(_speedRatio[speedState]); 2174 } 2175 } else if (stopBySpeedProfile) { 2176 // we are going to stop by profile 2177 _stoppingUsingSpeedProfile = true; 2178 _autoEngineer.setTargetSpeed(_currentAllocatedSection.getLengthRemaining(_currentBlock) 2179 * stoppingDistanceAdjust, 0.0f); 2180 } else { 2181 _autoEngineer.setHalt(true); 2182 setTargetSpeed(0.0f); 2183 } 2184 } 2185 2186 private synchronized void setTargetSpeedByProfile(float speedState, float stopBySpeedProfileAdjust, boolean cancelStopping) { 2187 // the speed comes in as units of warrents (mph, kph, mm/s etc) 2188 try { 2189 float throttleSetting = _activeTrain.getRosterEntry().getSpeedProfile().getThrottleSettingFromSignalMapSpeed(speedState, getForward()); 2190 log.debug("{}: setTargetSpeedByProfile: {} SpeedState[{}]", 2191 _activeTrain.getTrainName(), 2192 throttleSetting, 2193 speedState); 2194 if (throttleSetting > 0.009 && _currentRampRate != RAMP_SPEEDPROFILE && useSpeedProfile) { 2195 if (cancelStopping) {cancelStopInCurrentSection();} 2196 setTargetSpeed(throttleSetting); // apply speed factor and max 2197 } else if (throttleSetting > 0.009) { 2198 if (cancelStopping) {cancelStopInCurrentSection();} 2199 setTargetSpeed(_currentAllocatedSection.getLengthRemaining(_currentBlock) * stopBySpeedProfileAdjust , throttleSetting); 2200 } else if (useSpeedProfile && _stopBySpeedProfile) { 2201 setTargetSpeed(0.0f); 2202 _stoppingUsingSpeedProfile = true; 2203 _autoEngineer.setTargetSpeed(_currentAllocatedSection.getLengthRemaining(_currentBlock) * stopBySpeedProfileAdjust, 0.0f); 2204 } else { 2205 _autoEngineer.slowToStop(false); 2206 setTargetSpeed(0.0f); 2207 _autoEngineer.setHalt(true); 2208 } 2209 } catch (Exception ex) { 2210 log.error("setTargetSpeedByProfile crashed - Emergency Stop: ", ex ); 2211 _autoEngineer.slowToStop(false); 2212 setTargetSpeed(-1.0f); 2213 _autoEngineer.setHalt(true); 2214 } 2215 } 2216 2217 /** 2218 * Pass in speed as shown on dialogs, and convert to decimal speed needed by 2219 * throttle. 2220 */ 2221 private synchronized void setTargetSpeedValue(float speed) { 2222 log.debug("{}: setTargetSpeedValue: Speed[{}]",_activeTrain.getTrainName(),speed); 2223 if (useSpeedProfile) { 2224 setTargetSpeedByProfile(speed,_stopBySpeedProfileAdjust,true); 2225 return; 2226 } 2227 _autoEngineer.slowToStop(false); 2228 float mls; 2229 if (_controllingSignalMast != null) { 2230 mls = _controllingSignalMast.getSignalSystem().getMaximumLineSpeed(); 2231 } else { 2232 mls = _dispatcher.getMaximumLineSpeed(); 2233 } 2234 float decSpeed = (speed / mls); 2235 if (decSpeed > 0.0f) { 2236 cancelStopInCurrentSection(); 2237 setTargetSpeed(decSpeed); 2238 } else { 2239 setTargetSpeed(0.0f); 2240 _autoEngineer.setHalt(true); 2241 } 2242 } 2243 2244 private int getBlockLength(Block b) { 2245 if (b == null) 2246 return (0); 2247 return (int) b.getLengthMm(); 2248 // float fLength = b.getLengthMm() / (float) _dispatcher.getScale().getScaleFactor(); 2249 // if (_dispatcher.getUseScaleMeters()) { 2250 // return (int) (fLength * 0.001f); 2251 // } 2252 // return (int) (fLength * 0.00328084f); 2253 } 2254 2255 /** 2256 * Initiates running in manual mode with external throttle. 2257 * <p> 2258 * This method is triggered by an action in the Transit. The throttle in use 2259 * for automatic operation is dispatched. 2260 */ 2261 protected void initiateWorking() { 2262 if (_activeTrain.getStatus() != ActiveTrain.WORKING) { 2263 _activeTrain.setMode(ActiveTrain.DISPATCHED); 2264 _activeTrain.setStatus(ActiveTrain.WORKING); 2265 saveSpeedAndDirection(); 2266 if (_autoEngineer != null) { 2267 _autoEngineer.setHalt(true); 2268 waitUntilStopped(); 2269 _autoEngineer.abort(); 2270 InstanceManager.throttleManagerInstance().releaseThrottle(_throttle, this); 2271 _autoEngineer = null; 2272 _throttle = null; 2273 } 2274 } 2275 } 2276 2277 /** 2278 * Returns when train is stopped. 2279 * <p> 2280 * Note: Provides for _autoEngineer becoming null during wait Ties up the 2281 * current autoActiveTrain thread. 2282 */ 2283 protected void waitUntilStopped() { 2284 boolean doneWaiting = false; 2285 while (!doneWaiting) { 2286 if (_autoEngineer != null) { 2287 doneWaiting = _autoEngineer.isStopped(); 2288 } else { 2289 doneWaiting = true; 2290 } 2291 if (!doneWaiting) { 2292 try { 2293 Thread.sleep(50); 2294 } catch (InterruptedException e) { 2295 // ignore this exception 2296 } 2297 } 2298 } 2299 } 2300 2301 /** 2302 * Resumes automatic running after a working session using an external 2303 * throttle This method is triggered by the dispatcher hitting the "Resume 2304 * Auto Running" button A new throttle is acquired to allow automatic 2305 * running to resume 2306 */ 2307 protected void resumeAutomaticRunning() { 2308 if ((_activeTrain.getStatus() == ActiveTrain.WORKING) 2309 || (_activeTrain.getStatus() == ActiveTrain.READY)) { 2310 _autoTrainAction.cancelDoneSensor(); 2311 if (initialize()) { 2312 _resumingAutomatic = true; 2313 } else { 2314 log.error("Failed to initialize throttle when resuming automatic mode."); 2315 } 2316 } 2317 } 2318 2319 /** 2320 * Pause the auto active train for a specified number of fast clock minutes. 2321 * 2322 * @param fastMinutes the number of minutes to pause the train 2323 * @return the thread waiting on the pause or null if already paused 2324 */ 2325 public Thread pauseTrain(int fastMinutes) { 2326 if (_pausingActive) 2327 // if a pause train thread is currently active, ignore this call 2328 return (null); 2329 Runnable pauseTrain = new PauseTrain(fastMinutes); 2330 Thread tPause = jmri.util.ThreadingUtil.newThread(pauseTrain, "pause train " + _activeTrain.getTrainName()); 2331 tPause.start(); 2332 return tPause; 2333 } 2334 2335 public void terminate() { 2336 // here add code to stop the train and release its throttle if it is in autoRun 2337 while (_activeHornThreads > 0) { 2338 try { 2339 Thread.sleep(50); 2340 } catch (InterruptedException e) { 2341 // ignore this exception 2342 } 2343 } 2344 _autoTrainAction.clearRemainingActions(); 2345 if (_autoEngineer != null) { 2346 _autoEngineer.setHalt(true); 2347 try { 2348 Thread.sleep(50); 2349 } catch (InterruptedException e) { 2350 // ignore this exception 2351 } 2352 waitUntilStopped(); 2353 _autoEngineer.abort(); 2354 InstanceManager.throttleManagerInstance().releaseThrottle(_throttle, this); 2355 } 2356 } 2357 2358 public void dispose() { 2359 if (_controllingSignalMast != null && _conSignalMastListener != null) { 2360 _controllingSignalMast.removePropertyChangeListener(_conSignalMastListener); 2361 } 2362 _controllingSignalMast = null; 2363 _conSignalMastListener = null; 2364 if (_turnoutStateNeeded != null && _turnoutStateListener != null) { 2365 _turnoutStateNeeded.removePropertyChangeListener(_turnoutStateListener); 2366 } 2367 _turnoutStateNeeded = null; 2368 _turnoutStateListener = null; 2369 } 2370 2371 // _________________________________________________________________________________________ 2372 // This class waits for train stop in a separate thread 2373 class WaitForTrainToStop implements Runnable { 2374 2375 public WaitForTrainToStop(int task) { 2376 _task = task; 2377 } 2378 2379 @Override 2380 public void run() { 2381 boolean waitingOnTrain = true; 2382 try { 2383 while (waitingOnTrain) { 2384 if ((getAutoEngineer() != null) && (getAutoEngineer().isStopped())) { 2385 waitingOnTrain = false; 2386 } else { 2387 Thread.sleep(_delay); 2388 } 2389 } 2390 log.trace("executing task[{}]",_task); 2391 executeStopTasks(_task); 2392 } catch (InterruptedException e) { 2393 log.warn("Waiting for train to stop interrupted - stop tasks not executing"); 2394 } catch (Exception e) { 2395 log.error("Waiting for train to stop crashed - stop tasks not executing.", e); 2396 } 2397 } 2398 2399 private final int _delay = 91; 2400 private int _task = 0; 2401 } 2402 2403 /** 2404 * Pause the train in a separate thread. Train is stopped, then restarted 2405 * after specified number of fast Minutes have elapsed. 2406 */ 2407 class PauseTrain implements Runnable { 2408 /** 2409 * Create a PauseTrain 2410 * 2411 * @param fastMinutes the number of fast clock minutes to pause the 2412 * train 2413 */ 2414 public PauseTrain(int fastMinutes) { 2415 _fastMinutes = fastMinutes; 2416 } 2417 2418 @Override 2419 public void run() { 2420 // set to pause at a fast ramp rate 2421 _pausingActive = true; 2422 // TODO: use stop in section or block? 2423 _savedRampRate = getRampRate(); 2424 setCurrentRampRate(RAMP_FAST); 2425 stopInCurrentSection(NO_TASK); 2426 // wait for train to stop 2427 boolean waitNow = true; 2428 boolean keepGoing = true; 2429 while (waitNow) { 2430 try { 2431 Thread.sleep(101); 2432 if (_autoEngineer != null) { 2433 if (_autoEngineer.isStopped()) { 2434 waitNow = false; 2435 } 2436 } else { 2437 waitNow = false; 2438 } 2439 } catch (InterruptedException e) { 2440 log.trace("InterruptedException while waiting to stop for pause-indicates action cancelled.", e); 2441 waitNow = false; 2442 keepGoing = false; 2443 } 2444 } 2445 _activeTrain.setStatus(ActiveTrain.PAUSED); 2446 if (keepGoing) { 2447 // wait for specified fast clock time 2448 Timebase _clock = InstanceManager.getDefault(jmri.Timebase.class); 2449 java.beans.PropertyChangeListener _clockListener = (java.beans.PropertyChangeEvent e) -> { 2450 _fastMinutes--; 2451 }; 2452 _clock.addMinuteChangeListener(_clockListener); 2453 // wait for fast minutes to tick away 2454 waitNow = true; 2455 while (waitNow) { 2456 try { 2457 Thread.sleep(501); 2458 if (_fastMinutes <= 0) { 2459 waitNow = false; 2460 } 2461 } catch (InterruptedException e) { 2462 log.trace("InterruptedException indicates action cancelled.", e); 2463 keepGoing = false; 2464 } 2465 } 2466 _clock.removeMinuteChangeListener(_clockListener); 2467 } 2468 _pausingActive = false; 2469 if (keepGoing) { 2470 // this thread was not interrupted 2471 // resume running - restore speed, status, and ramp rate 2472 setCurrentRampRate(_savedRampRate); 2473 // Set speed by signal also works if signal missing 2474 // so we dont need to restore a previous value. 2475 _activeTrain.setStatus(ActiveTrain.RUNNING); 2476 setSpeedBySignal(); 2477 } 2478 } 2479 private int _fastMinutes = 0; 2480 private int _savedRampRate = RAMP_NONE; 2481 } 2482 2483 // _________________________________________________________________________________________ 2484 // this class handles the interface with the throttle 2485 // (This class started from code by Pete Cressman contained in Warrant.java.) 2486 class AutoEngineer { 2487 2488 AutoEngineer(DccThrottle throttle, RosterEntry rosterEntry) { 2489 this.throttle = throttle; 2490 this.rosterEntry = rosterEntry; 2491 } 2492 2493 private DccThrottle throttle; 2494 private int ramping; 2495 private boolean speedProfileStoppingIsRunning = false; 2496 private float speedIncrement = 0.0f; //will be recalculated 2497 private float targetSpeed; 2498 private RosterEntry rosterEntry; 2499 private int throttleInterval; 2500 private float minReliableOperatingSpeed; 2501 private float maxSpeed; 2502 private float speedFactor; 2503 2504 public void setRamping(int ramping, int fullRampTime, int minThrottleInterval, int rampRate) { 2505 this.ramping = ramping; 2506 this.throttleInterval = minThrottleInterval; 2507 //calculate speed increment to use in each minInterval time 2508 speedIncrement = (100.0f / ((float) fullRampTime / minThrottleInterval) 2509 / rampRate) / 100.0f; 2510 log.debug("{}: _speedIncrement={}", throttle.getLocoAddress(), speedIncrement); 2511 } 2512 2513 // Once physics ramping is found to be unusable for this train, permanently disable it 2514 // for the remainder of this AutoEngineer instance to avoid repeated stalls or repeated warnings. 2515 private boolean physicsRampingDisabled = false; 2516 2517 private void disablePhysicsRamping(String reason, float weightKg, float powerKw, float tractiveEffortKn) { 2518 if (!physicsRampingDisabled) { 2519 String id = (rosterEntry != null) ? rosterEntry.getId() : "<unknown>"; 2520 log.warn( 2521 "{}: Physics ramp disabled ({}). Roster physics: weightKg={}, powerKw={}, tractiveEffortKn={}; forcing RAMP_MEDIUM.", 2522 id, reason, Float.valueOf(weightKg), Float.valueOf(powerKw), Float.valueOf(tractiveEffortKn)); 2523 } 2524 physicsRampingDisabled = true; 2525 2526 // Ensure the AutoActiveTrain state is no longer RAMP_PHYSICS 2527 AutoActiveTrain.this.setRampRate(RAMP_MEDIUM); 2528 2529 // Ensure this AutoEngineer instance is no longer in physics mode 2530 this.ramping = RAMP_MEDIUM; 2531 2532 // Recompute ramp parameters for medium ramp so speedIncrement matches the selected mode 2533 if (AutoActiveTrain.this._dispatcher != null) { 2534 setRamping(RAMP_MEDIUM, AutoActiveTrain.this._dispatcher.getFullRampTime(), 2535 AutoActiveTrain.this._dispatcher.getMinThrottleInterval(), RAMP_MEDIUM); 2536 } 2537 } 2538 2539 2540 public void setIsForward(boolean isForward) { 2541 throttle.setIsForward(isForward); 2542 } 2543 2544 public boolean getIsForward() { 2545 return(throttle.getIsForward()); 2546 } 2547 2548 public void setTargetSpeed(float speed) { 2549 stopAllTimers(); 2550 2551 // Physics ramp: only if enabled AND speed profile exists for current direction 2552 boolean physicsRamp = (ramping == RAMP_PHYSICS); 2553 boolean forward = getIsForward(); 2554 boolean profileAvailable = false; 2555 if (AutoActiveTrain.this.re != null && AutoActiveTrain.this.re.getSpeedProfile() != null) { 2556 profileAvailable = forward 2557 ? AutoActiveTrain.this.re.getSpeedProfile().hasForwardSpeeds() 2558 : AutoActiveTrain.this.re.getSpeedProfile().hasReverseSpeeds(); 2559 } 2560 2561 2562 log.debug("[{}] setTargetSpeed: ramping={}, physicsRamp={}, profileAvailable={}, forward={}, speedArg={}", 2563 AutoActiveTrain.this._activeTrain.getTrainName(), 2564 ramping, physicsRamp, profileAvailable, forward, speed); 2565 2566 2567 2568 // If physics ramping is selected, ensure a usable speed profile and defined physics parameters exist. 2569 // If not, permanently fall back to RAMP_MEDIUM for this Auto Active Train. 2570 if (physicsRamp) { 2571 if (physicsRampingDisabled) { 2572 physicsRamp = false; 2573 } else if (!profileAvailable) { 2574 disablePhysicsRamping("no speed profile for current direction", 0.0f, 0.0f, 0.0f); 2575 physicsRamp = false; 2576 } else { 2577 float wKg = 0.0f; 2578 float pKw = 0.0f; 2579 float teKn = 0.0f; 2580 try { 2581 if (rosterEntry != null) { 2582 wKg = rosterEntry.getPhysicsWeightKg(); 2583 pKw = rosterEntry.getPhysicsPowerKw(); 2584 teKn = rosterEntry.getPhysicsTractiveEffortKn(); 2585 } 2586 } catch (Throwable ex) { 2587 // Older roster entries may not have physics fields 2588 wKg = 0.0f; 2589 pKw = 0.0f; 2590 teKn = 0.0f; 2591 } 2592 if ((wKg <= 0.0f) && (pKw <= 0.0f) && (teKn <= 0.0f)) { 2593 disablePhysicsRamping("no physics parameters defined", wKg, pKw, teKn); 2594 physicsRamp = false; 2595 } 2596 } 2597 } 2598 if (physicsRamp && profileAvailable) { 2599 // Physics ramp drives throttle asynchronously via RosterSpeedProfile; keep targetSpeed in sync 2600 // so higher-level stop logic does not treat a moving train as already stopped. 2601 targetSpeed = applyMaxThrottleAndFactor(speed); 2602 // Mark that a RosterSpeedProfile timer/queue may be active so stopAllTimers() can cancel it on terminate. 2603 speedProfileStoppingIsRunning = true; 2604 2605 // Run physics planner off the EDT 2606 Thread phys = jmri.util.ThreadingUtil.newThread(() -> { 2607 // Ensure min/max limits (including optional scale km/h cap) are in the profile 2608 re.getSpeedProfile().setMinMaxLimitsKmh( 2609 minReliableOperatingSpeed, 2610 maxSpeed, 2611 AutoActiveTrain.this._maxSpeedScaleKmh, 2612 (float) AutoActiveTrain.this._dispatcher.getScale().getScaleRatio(), 2613 forward 2614 ); 2615 // Delegate the acceleration plan & execution to RosterSpeedProfile 2616 re.getSpeedProfile().runPhysicsAccelerationToTargetThrottle( 2617 throttle, 2618 speed, 2619 AutoActiveTrain.this._driverPowerPercent, 2620 AutoActiveTrain.this._additionalWeightTonnes, 2621 AutoActiveTrain.this._rollingResistanceCoeff, 2622 (float) AutoActiveTrain.this._dispatcher.getScale().getScaleRatio(), 2623 speedFactor 2624 ); 2625 }, "PhysicsRamp " + AutoActiveTrain.this._activeTrain.getTrainName()); 2626 phys.start(); 2627 return; 2628 } 2629 // Fallback to existing behaviour 2630 targetSpeed = applyMaxThrottleAndFactor(speed); 2631 log.debug("setTargetSpeed: Set Speed[{}] adjusted to TargetSpeed[{}] ", speed, targetSpeed); 2632 if (ramping == RAMP_NONE || ramping == RAMP_SPEEDPROFILE) { 2633 throttle.setSpeedSetting(targetSpeed); 2634 } else { 2635 rampToTarget(); 2636 } 2637 } 2638 2639 public float getTargetSpeed(){ 2640 return(targetSpeed); 2641 } 2642 2643 /** 2644 * 2645 * @param throttleSetting the throttle setting that would normally be set 2646 * @return the adjusted throttle setting after applying Max Throttle and Percentage throttle settings 2647 */ 2648 private float applyMaxThrottleAndFactor(float throttleSetting) { 2649 // Apply speedFactor first (this is how the existing code behaves) 2650 float applied = (throttleSetting > 0.0f) ? (throttleSetting * speedFactor) : throttleSetting; 2651 2652 if (applied <= 0.0f) 2653 return applied; 2654 2655 // Compute the active upper cap: 2656 // - If a scale km/h cap is set AND a speed profile exists in the current direction, 2657 // derive an equivalent throttle cap using the roster profile + layout scale ratio. 2658 // - Otherwise, fall back to the throttle % cap (maxSpeed). 2659 float maxApplied; 2660 boolean forward = getIsForward(); 2661 boolean profileAvailable = false; 2662 if (AutoActiveTrain.this.re != null && AutoActiveTrain.this.re.getSpeedProfile() != null) { 2663 // Direction-aware availability 2664 profileAvailable = forward ? AutoActiveTrain.this.re.getSpeedProfile().hasForwardSpeeds() 2665 : AutoActiveTrain.this.re.getSpeedProfile().hasReverseSpeeds(); 2666 } 2667 2668 if (AutoActiveTrain.this._maxSpeedScaleKmh > 0.0f && profileAvailable && AutoActiveTrain.this._dispatcher != null) { 2669 // scale km/h -> actual mm/s 2670 float kmh = AutoActiveTrain.this._maxSpeedScaleKmh; 2671 float scaleRatio = (float) AutoActiveTrain.this._dispatcher.getScale().getScaleRatio(); 2672 float modelKmh = kmh / ((scaleRatio <= 0.0f) ? 1.0f : scaleRatio); 2673 float targetMms = modelKmh * 277.7778f; // 1 km/h = 277.7778 mm/s 2674 // Invert the roster profile to get the required throttle [% 0..1] via RosterSpeedProfile 2675 float thrCapPct = AutoActiveTrain.this.re.getSpeedProfile().getThrottleSetting(targetMms, forward); 2676 // This cap applies to the FINAL applied throttle (after speedFactor), 2677 // so clamp 'applied' directly to thrCapPct. 2678 maxApplied = thrCapPct; 2679 } else { 2680 // Fallback to the existing throttle % cap 2681 maxApplied = maxSpeed; 2682 } 2683 2684 // Enforce min and max caps 2685 if (applied > maxApplied) { applied = maxApplied; } 2686 if (applied < minReliableOperatingSpeed) { applied = minReliableOperatingSpeed; } 2687 2688 return applied; 2689 } 2690 2691 /** 2692 * Flag from user's control. 2693 * 2694 * @param halt true to immediately stop the train; false otherwise 2695 */ 2696 public void setHalt(boolean halt) { 2697 if (halt) { 2698 this.setSpeedImmediate(0.0f); 2699 } 2700 } 2701 2702 /** 2703 * Set the limits and adjustment factore for train speed. 2704 * Active train will calculate the required setting and it will be adjusted if not 0.0f 2705 * required setting * speed Factor then test for less than max and greater than min. 2706 * @param minReliableOperatingSpeed lowest throttle % train will reliably move. 2707 * @param maxSpeed max throttle % for train. 2708 * @param speedFactor multiplier 2709 */ 2710 public void setSpeedLimits(float minReliableOperatingSpeed, float maxSpeed, float speedFactor) { 2711 this.minReliableOperatingSpeed = minReliableOperatingSpeed; 2712 this.maxSpeed = maxSpeed; 2713 this.speedFactor = speedFactor; 2714 } 2715 2716 public void setTargetSpeed(float distance, float speed) { 2717 log.debug("Set Target Speed[{}] with distance{{}] from speed[{}]",speed,distance,throttle.getSpeedSetting()); 2718 stopAllTimers(); 2719 if (rosterEntry != null) { 2720 rosterEntry.getSpeedProfile().setExtraInitialDelay(1500f); 2721 rosterEntry.getSpeedProfile().setMinMaxLimitsKmh(minReliableOperatingSpeed, maxSpeed, 2722 AutoActiveTrain.this._maxSpeedScaleKmh, 2723 (float) AutoActiveTrain.this._dispatcher.getScale().getScaleRatio(), getIsForward()); 2724 rosterEntry.getSpeedProfile().changeLocoSpeed(_throttle, distance, speed); 2725 speedProfileStoppingIsRunning = true; 2726 targetSpeed = speed; 2727 } else { 2728 setTargetSpeed((0.0f)); 2729 } 2730 } 2731 2732 public void slowToStop(boolean on) { 2733 stopAllTimers(); 2734 if (on) { 2735 log.debug("SlowToStopOn"); 2736 setTargetSpeed((0.0f)); 2737 } 2738 } 2739 2740 public void stopAllTimers() { 2741 if (speedProfileStoppingIsRunning) { 2742 re.getSpeedProfile().cancelSpeedChange(); 2743 speedProfileStoppingIsRunning = false; 2744 } 2745 if (rampingTimer != null) { 2746 rampingTimer.stop(); 2747 rampingTimer = null; 2748 } 2749 } 2750 2751 LinkedList<SpeedSetting> stepQueue; 2752 private javax.swing.Timer rampingTimer; 2753 2754 private void rampToTarget() { 2755 // target already adjusted. 2756 log.debug("RampToTarget[{}]current[{}]", getTargetSpeed(), throttle.getSpeedSetting()); 2757 stepQueue = new LinkedList<>(); 2758 if (throttle.getSpeedSetting() == getTargetSpeed()) 2759 return; 2760 else if (throttle.getSpeedSetting() < getTargetSpeed()) { 2761 // Up (accelerate) 2762 float newSpeed = throttle.getSpeedSetting(); 2763 if (newSpeed < minReliableOperatingSpeed) { 2764 stepQueue.add(new SpeedSetting(minReliableOperatingSpeed, throttleInterval)); 2765 newSpeed = minReliableOperatingSpeed; 2766 } 2767 while (newSpeed < getTargetSpeed()) { 2768 newSpeed += speedIncrement; 2769 if (newSpeed > getTargetSpeed()) { 2770 newSpeed = getTargetSpeed(); 2771 } 2772 log.trace("NewSpeedUp[{}]", newSpeed); 2773 stepQueue.add(new SpeedSetting(newSpeed, throttleInterval)); 2774 } 2775 } else { 2776 // Down (decelerate) 2777 boolean andStop = false; 2778 if (getTargetSpeed() <= 0.0f) { 2779 andStop = true; 2780 } 2781 float newSpeed = throttle.getSpeedSetting(); 2782 while (newSpeed > getTargetSpeed()) { 2783 newSpeed -= speedIncrement; 2784 if (newSpeed < getTargetSpeed()) { 2785 newSpeed = getTargetSpeed(); 2786 } 2787 log.trace("NewSpeedDown[{}]", newSpeed); 2788 stepQueue.add(new SpeedSetting(newSpeed, throttleInterval)); 2789 } 2790 if (andStop) { 2791 stepQueue.add(new SpeedSetting(0.0f, throttleInterval)); 2792 } 2793 } 2794 if (rampingTimer == null) { //If this is the first time round then kick off the speed change 2795 setNextStep(); 2796 } 2797 } 2798 2799 private void finishChange() { 2800 if (rampingTimer != null) { 2801 rampingTimer.stop(); 2802 } 2803 rampingTimer = null; 2804 stepQueue.clear(); 2805 stepQueue = null; 2806 } 2807 2808 synchronized void setNextStep() { 2809 if (stepQueue.isEmpty()) { 2810 log.trace("Empty"); 2811 finishChange(); 2812 return; 2813 } 2814 SpeedSetting ss = stepQueue.getFirst(); 2815 if (ss.getDuration() == 0) { 2816 log.trace("Duratiom Zero"); 2817 finishChange(); 2818 return; 2819 } 2820 stepQueue.removeFirst(); 2821 log.trace("Set New Speed[{}]",ss.getSpeedStep()); 2822 throttle.setSpeedSetting(ss.getSpeedStep()); 2823 log.debug("{}: ramp step -> {}", _activeTrain.getTrainName(), ss.getSpeedStep()); 2824 rampingTimer = new javax.swing.Timer(ss.getDuration(), (java.awt.event.ActionEvent e) -> { 2825 setNextStep(); 2826 }); 2827 rampingTimer.setRepeats(false); 2828 rampingTimer.start(); 2829 } 2830 2831 private class SpeedSetting { 2832 2833 float step = 0.0f; 2834 int duration = 0; 2835 2836 SpeedSetting(float step, int duration) { 2837 this.step = step; 2838 this.duration = duration; 2839 } 2840 2841 float getSpeedStep() { 2842 return step; 2843 } 2844 2845 int getDuration() { 2846 return duration; 2847 } 2848 } 2849 2850 /** 2851 * Set the train speed directly, bypassing ramping. 2852 * 2853 * @param speed 0.0 (stop) to 1.0 (full) 2854 */ 2855 public synchronized void setSpeedImmediate(float speed) { 2856 log.trace("{}: setting speed directly to {}%", _activeTrain.getTrainName(), (int) (speed * 100)); 2857 stopAllTimers(); 2858 targetSpeed = applyMaxThrottleAndFactor(speed); 2859 log.debug("{}: setSpeedImmediate -> {}", _activeTrain.getTrainName(), speed); 2860 throttle.setSpeedSetting(targetSpeed); 2861 } 2862 2863 /** 2864 * Check if train is moving or stopped. 2865 * 2866 * @return true if stopped; false otherwise 2867 */ 2868 public synchronized boolean isStopped() { 2869 // when stopping by speed profile you must refresh the throttle speed. 2870 return throttle.getSpeedSetting() <= 0.0004f; 2871 } 2872 2873 /** 2874 * Check if train is moving at its current requested speed. 2875 * 2876 * @return true if at requested speed; false otherwise 2877 */ 2878 public synchronized boolean isAtSpeed() { 2879 return java.lang.Math.abs(throttle.getSpeedSetting() - targetSpeed) <= 0.01; 2880 } 2881 2882 /** 2883 * Flag from user to end run. 2884 */ 2885 public void abort() { 2886 stopAllTimers(); 2887 } 2888 2889 protected void setFunction(int cmdNum, boolean isSet) { 2890 throttle.setFunction(cmdNum, isSet); 2891 } 2892 } 2893 2894 /** 2895 * Convert ramp rate name, stored as a string into the constant value 2896 * assigned. 2897 * 2898 * @param rampRate name of ramp rate, such as "RAMP_FAST" 2899 * @return integer representing a ramp rate constant value 2900 */ 2901 public static int getRampRateFromName(String rampRate) { 2902 if (rampRate.equals(Bundle.getMessage("RAMP_FAST"))) 2903 return RAMP_FAST; 2904 else if (rampRate.equals(Bundle.getMessage("RAMP_MEDIUM"))) 2905 return RAMP_MEDIUM; 2906 else if (rampRate.equals(Bundle.getMessage("RAMP_MED_SLOW"))) 2907 return RAMP_MED_SLOW; 2908 else if (rampRate.equals(Bundle.getMessage("RAMP_SLOW"))) 2909 return RAMP_SLOW; 2910 else if (rampRate.equals(Bundle.getMessage("RAMP_SPEEDPROFILE"))) 2911 return RAMP_SPEEDPROFILE; 2912 else if (rampRate.equals(Bundle.getMessage("RAMP_PHYSICS"))) 2913 return RAMP_PHYSICS; 2914 return RAMP_NONE; 2915 } 2916 2917 /* 2918 * Listener for switching Ghost blocks to unoccupied 2919 */ 2920 static class DarkTerritoryListener implements PropertyChangeListener { 2921 private Sensor sensor; 2922 2923 public DarkTerritoryListener(Sensor sensor) { 2924 this.sensor = sensor; 2925 log.trace("Sensor[{}]", sensor.getDisplayName()); 2926 } 2927 2928 @Override 2929 public void propertyChange(PropertyChangeEvent e) { 2930 if (e.getPropertyName().equals("state")) { 2931 ((Block) e.getSource()).removePropertyChangeListener(this); 2932 if (e.getNewValue().equals(Block.UNOCCUPIED)) { 2933 try { 2934 log.trace("Sensor INACTIVE[{}]", sensor.getDisplayName()); 2935 sensor.setKnownState(Sensor.INACTIVE); 2936 } catch (jmri.JmriException ex) { 2937 log.error("Error leaving darkterratory"); 2938 } 2939 } 2940 } 2941 } 2942 } 2943 2944 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AutoActiveTrain.class); 2945}