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