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