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