001package jmri.jmrit.logixng.actions; 002 003import jmri.jmrit.logixng.util.TimerUnit; 004 005import java.util.*; 006 007import jmri.InstanceManager; 008import jmri.JmriException; 009import jmri.jmrit.logixng.*; 010import jmri.jmrit.logixng.util.ProtectedTimerTask; 011import jmri.util.TimerUtil; 012 013/** 014 * Executes an action after some time. 015 * 016 * @author Daniel Bergqvist Copyright 2019 017 */ 018public class ActionTimer extends AbstractDigitalAction 019 implements FemaleSocketListener { 020 021 private static class State{ 022 private ProtectedTimerTask _timerTask; 023 private int _currentTimer = -1; 024 private TimerState _timerState = TimerState.Off; 025 private long _currentTimerDelay = 0; 026 private long _currentTimerStart = 0; 027 private boolean _startIsActive = false; 028 } 029 030 public static final int EXPRESSION_START = 0; 031 public static final int EXPRESSION_STOP = 1; 032 public static final int NUM_STATIC_EXPRESSIONS = 2; 033 034 private String _startExpressionSocketSystemName; 035 private String _stopExpressionSocketSystemName; 036 private final FemaleDigitalExpressionSocket _startExpressionSocket; 037 private final FemaleDigitalExpressionSocket _stopExpressionSocket; 038 private final List<ActionEntry> _actionEntries = new ArrayList<>(); 039 private boolean _startImmediately = false; 040 private boolean _runContinuously = false; 041 private boolean _startAndStopByStartExpression = false; 042 private TimerUnit _unit = TimerUnit.MilliSeconds; 043 private boolean _delayByLocalVariables = false; 044 private String _delayLocalVariablePrefix = ""; // An index is appended, for example Delay1, Delay2, ... Delay15. 045 private final Map<ConditionalNG, State> _stateMap = new HashMap<>(); 046 047 048 public ActionTimer(String sys, String user) { 049 super(sys, user); 050 _startExpressionSocket = InstanceManager.getDefault(DigitalExpressionManager.class) 051 .createFemaleSocket(this, this, Bundle.getMessage("ActionTimerSocketStart")); 052 _stopExpressionSocket = InstanceManager.getDefault(DigitalExpressionManager.class) 053 .createFemaleSocket(this, this, Bundle.getMessage("ActionTimerSocketStop")); 054 _actionEntries 055 .add(new ActionEntry(InstanceManager.getDefault(DigitalActionManager.class) 056 .createFemaleSocket(this, this, getNewSocketName()))); 057 } 058 059 public ActionTimer(String sys, String user, 060 List<Map.Entry<String, String>> expressionSystemNames, 061 List<ActionData> actionDataList) 062 throws BadUserNameException, BadSystemNameException { 063 super(sys, user); 064 _startExpressionSocket = InstanceManager.getDefault(DigitalExpressionManager.class) 065 .createFemaleSocket(this, this, Bundle.getMessage("ActionTimerSocketStart")); 066 _stopExpressionSocket = InstanceManager.getDefault(DigitalExpressionManager.class) 067 .createFemaleSocket(this, this, Bundle.getMessage("ActionTimerSocketStop")); 068 setActionData(actionDataList); 069 } 070 071 @Override 072 public Base getDeepCopy(Map<String, String> systemNames, Map<String, String> userNames) throws JmriException { 073 DigitalActionManager manager = InstanceManager.getDefault(DigitalActionManager.class); 074 String sysName = systemNames.get(getSystemName()); 075 String userName = userNames.get(getSystemName()); 076 if (sysName == null) sysName = manager.getAutoSystemName(); 077 ActionTimer copy = new ActionTimer(sysName, userName); 078 copy.setComment(getComment()); 079 copy.setNumActions(getNumActions()); 080 for (int i=0; i < getNumActions(); i++) { 081 copy.setDelay(i, getDelay(i)); 082 } 083 copy.setStartImmediately(_startImmediately); 084 copy.setRunContinuously(_runContinuously); 085 copy.setStartAndStopByStartExpression(_startAndStopByStartExpression); 086 copy.setUnit(_unit); 087 copy.setDelayByLocalVariables(_delayByLocalVariables); 088 copy.setDelayLocalVariablePrefix(_delayLocalVariablePrefix); 089 return manager.registerAction(copy).deepCopyChildren(this, systemNames, userNames); 090 } 091 092 private void setActionData(List<ActionData> actionDataList) { 093 if (!_actionEntries.isEmpty()) { 094 throw new RuntimeException("action system names cannot be set more than once"); 095 } 096 097 for (ActionData data : actionDataList) { 098 FemaleDigitalActionSocket socket = 099 InstanceManager.getDefault(DigitalActionManager.class) 100 .createFemaleSocket(this, this, data._socketName); 101 102 _actionEntries.add(new ActionEntry(data._delay, socket, data._socketSystemName)); 103 } 104 } 105 106 /** {@inheritDoc} */ 107 @Override 108 public Category getCategory() { 109 return Category.COMMON; 110 } 111 112 /** 113 * Get a new timer task. 114 */ 115 private ProtectedTimerTask getNewTimerTask(ConditionalNG conditionalNG, State state) { 116 return new ProtectedTimerTask() { 117 @Override 118 public void execute() { 119 try { 120 long currentTimerTime = System.currentTimeMillis() - state._currentTimerStart; 121 if (currentTimerTime < state._currentTimerDelay) { 122 scheduleTimer(conditionalNG, state, state._currentTimerDelay - currentTimerTime); 123 } else { 124 state._timerState = TimerState.Completed; 125 conditionalNG.execute(); 126 } 127 } catch (Exception e) { 128 log.error("Exception thrown", e); 129 } 130 } 131 }; 132 } 133 134 private void scheduleTimer(ConditionalNG conditionalNG, State state, long delay) { 135 synchronized(this) { 136 if (state._timerTask != null) { 137 state._timerTask.stopTimer(); 138 state._timerTask = null; 139 } 140 } 141 state._timerTask = getNewTimerTask(conditionalNG, state); 142 TimerUtil.schedule(state._timerTask, delay); 143 } 144 145 private void schedule(ConditionalNG conditionalNG, SymbolTable symbolTable, State state) { 146 synchronized(this) { 147 long delay; 148 149 if (_delayByLocalVariables) { 150 delay = jmri.util.TypeConversionUtil 151 .convertToLong(symbolTable.getValue( 152 _delayLocalVariablePrefix + Integer.toString(state._currentTimer+1))); 153 } else { 154 delay = _actionEntries.get(state._currentTimer)._delay; 155 } 156 157 state._currentTimerDelay = delay * _unit.getMultiply(); 158 state._currentTimerStart = System.currentTimeMillis(); 159 state._timerState = TimerState.WaitToRun; 160 scheduleTimer(conditionalNG, state, delay * _unit.getMultiply()); 161 } 162 } 163 164 private boolean start(State state) throws JmriException { 165 boolean lastStartIsActive = state._startIsActive; 166 state._startIsActive = _startExpressionSocket.isConnected() && _startExpressionSocket.evaluate(); 167 return state._startIsActive && !lastStartIsActive; 168 } 169 170 private boolean checkStart(ConditionalNG conditionalNG, SymbolTable symbolTable, State state) throws JmriException { 171 if (start(state)) state._timerState = TimerState.RunNow; 172 173 if (state._timerState == TimerState.RunNow) { 174 synchronized(this) { 175 if (state._timerTask != null) { 176 state._timerTask.stopTimer(); 177 state._timerTask = null; 178 } 179 } 180 state._currentTimer = 0; 181 while (state._currentTimer < _actionEntries.size()) { 182 ActionEntry ae = _actionEntries.get(state._currentTimer); 183 if (ae._delay > 0) { 184 schedule(conditionalNG, symbolTable, state); 185 return true; 186 } 187 else { 188 state._currentTimer++; 189 } 190 } 191 // If we get here, all timers have a delay of 0 ms 192 state._timerState = TimerState.Off; 193 return true; 194 } 195 196 return false; 197 } 198 199 private boolean stop(State state) throws JmriException { 200 boolean stop; 201 202 if (_startAndStopByStartExpression) { 203 stop = _startExpressionSocket.isConnected() && !_startExpressionSocket.evaluate(); 204 } else { 205 stop = _stopExpressionSocket.isConnected() && _stopExpressionSocket.evaluate(); 206 } 207 208 if (stop) { 209 synchronized(this) { 210 if (state._timerTask != null) state._timerTask.stopTimer(); 211 state._timerTask = null; 212 } 213 state._timerState = TimerState.Off; 214 return true; 215 } 216 return false; 217 } 218 219 /** {@inheritDoc} */ 220 @Override 221 public void execute() throws JmriException { 222 ConditionalNG conditionalNG = getConditionalNG(); 223 State state = _stateMap.computeIfAbsent(conditionalNG, o -> new State()); 224 225 if (stop(state)) { 226 state._startIsActive = false; 227 return; 228 } 229 230 if (checkStart(conditionalNG, conditionalNG.getSymbolTable(), state)) return; 231 232 if (state._timerState == TimerState.Off) return; 233 if (state._timerState == TimerState.Running) return; 234 235 int startTimer = state._currentTimer; 236 while (state._timerState == TimerState.Completed) { 237 // If the timer has passed full time, execute the action 238 if ((state._timerState == TimerState.Completed) && _actionEntries.get(state._currentTimer)._socket.isConnected()) { 239 _actionEntries.get(state._currentTimer)._socket.execute(); 240 } 241 242 // Move to them next timer 243 state._currentTimer++; 244 if (state._currentTimer >= _actionEntries.size()) { 245 state._currentTimer = 0; 246 if (!_runContinuously) { 247 state._timerState = TimerState.Off; 248 return; 249 } 250 } 251 252 ActionEntry ae = _actionEntries.get(state._currentTimer); 253 if (ae._delay > 0) { 254 schedule(conditionalNG, conditionalNG.getSymbolTable(), state); 255 return; 256 } 257 258 if (startTimer == state._currentTimer) { 259 // If we get here, all timers have a delay of 0 ms 260 state._timerState = TimerState.Off; 261 } 262 } 263 } 264 265 /** 266 * Get the delay. 267 * @param actionSocket the socket 268 * @return the delay 269 */ 270 public int getDelay(int actionSocket) { 271 return _actionEntries.get(actionSocket)._delay; 272 } 273 274 /** 275 * Set the delay. 276 * @param actionSocket the socket 277 * @param delay the delay 278 */ 279 public void setDelay(int actionSocket, int delay) { 280 _actionEntries.get(actionSocket)._delay = delay; 281 } 282 283 /** 284 * Get if to start immediately 285 * @return true if to start immediately 286 */ 287 public boolean isStartImmediately() { 288 return _startImmediately; 289 } 290 291 /** 292 * Set if to start immediately 293 * @param startImmediately true if to start immediately 294 */ 295 public void setStartImmediately(boolean startImmediately) { 296 _startImmediately = startImmediately; 297 } 298 299 /** 300 * Get if run continuously 301 * @return true if run continuously 302 */ 303 public boolean isRunContinuously() { 304 return _runContinuously; 305 } 306 307 /** 308 * Set if run continuously 309 * @param runContinuously true if run continuously 310 */ 311 public void setRunContinuously(boolean runContinuously) { 312 _runContinuously = runContinuously; 313 } 314 315 /** 316 * Is both start and stop is controlled by the start expression. 317 * @return true if to start immediately 318 */ 319 public boolean isStartAndStopByStartExpression() { 320 return _startAndStopByStartExpression; 321 } 322 323 /** 324 * Set if both start and stop is controlled by the start expression. 325 * @param startAndStopByStartExpression true if to start immediately 326 */ 327 public void setStartAndStopByStartExpression(boolean startAndStopByStartExpression) { 328 _startAndStopByStartExpression = startAndStopByStartExpression; 329 } 330 331 /** 332 * Get the unit 333 * @return the unit 334 */ 335 public TimerUnit getUnit() { 336 return _unit; 337 } 338 339 /** 340 * Set the unit 341 * @param unit the unit 342 */ 343 public void setUnit(TimerUnit unit) { 344 _unit = unit; 345 } 346 347 /** 348 * Is delays given by local variables? 349 * @return value true if delay is given by local variables 350 */ 351 public boolean isDelayByLocalVariables() { 352 return _delayByLocalVariables; 353 } 354 355 /** 356 * Set if delays should be given by local variables. 357 * @param value true if delay is given by local variables 358 */ 359 public void setDelayByLocalVariables(boolean value) { 360 _delayByLocalVariables = value; 361 } 362 363 /** 364 * Is both start and stop is controlled by the start expression. 365 * @return true if to start immediately 366 */ 367 public String getDelayLocalVariablePrefix() { 368 return _delayLocalVariablePrefix; 369 } 370 371 /** 372 * Set if both start and stop is controlled by the start expression. 373 * @param value true if to start immediately 374 */ 375 public void setDelayLocalVariablePrefix(String value) { 376 _delayLocalVariablePrefix = value; 377 } 378 379 @Override 380 public FemaleSocket getChild(int index) throws IllegalArgumentException, UnsupportedOperationException { 381 if (index == EXPRESSION_START) return _startExpressionSocket; 382 if (index == EXPRESSION_STOP) return _stopExpressionSocket; 383 if ((index < 0) || (index >= (NUM_STATIC_EXPRESSIONS + _actionEntries.size()))) { 384 throw new IllegalArgumentException( 385 String.format("index has invalid value: %d", index)); 386 } 387 return _actionEntries.get(index - NUM_STATIC_EXPRESSIONS)._socket; 388 } 389 390 @Override 391 public int getChildCount() { 392 return NUM_STATIC_EXPRESSIONS + _actionEntries.size(); 393 } 394 395 @Override 396 public void connected(FemaleSocket socket) { 397 if (socket == _startExpressionSocket) { 398 _startExpressionSocketSystemName = socket.getConnectedSocket().getSystemName(); 399 } else if (socket == _stopExpressionSocket) { 400 _stopExpressionSocketSystemName = socket.getConnectedSocket().getSystemName(); 401 } else { 402 for (ActionEntry entry : _actionEntries) { 403 if (socket == entry._socket) { 404 entry._socketSystemName = 405 socket.getConnectedSocket().getSystemName(); 406 } 407 } 408 } 409 } 410 411 @Override 412 public void disconnected(FemaleSocket socket) { 413 if (socket == _startExpressionSocket) { 414 _startExpressionSocketSystemName = null; 415 } else if (socket == _stopExpressionSocket) { 416 _stopExpressionSocketSystemName = null; 417 } else { 418 for (ActionEntry entry : _actionEntries) { 419 if (socket == entry._socket) { 420 entry._socketSystemName = null; 421 } 422 } 423 } 424 } 425 426 @Override 427 public String getShortDescription(Locale locale) { 428 return Bundle.getMessage(locale, "ActionTimer_Short"); 429 } 430 431 @Override 432 public String getLongDescription(Locale locale) { 433 String options = ""; 434 if (_delayByLocalVariables) { 435 options = Bundle.getMessage("ActionTimer_Options_DelayByLocalVariable", _delayLocalVariablePrefix); 436 } 437 if (_startAndStopByStartExpression) { 438 return Bundle.getMessage(locale, "ActionTimer_Long2", 439 Bundle.getMessage("ActionTimer_StartAndStopByStartExpression"), options); 440 } else { 441 return Bundle.getMessage(locale, "ActionTimer_Long", options); 442 } 443 } 444 445 public FemaleDigitalExpressionSocket getStartExpressionSocket() { 446 return _startExpressionSocket; 447 } 448 449 public String getStartExpressionSocketSystemName() { 450 return _startExpressionSocketSystemName; 451 } 452 453 public void setStartExpressionSocketSystemName(String systemName) { 454 _startExpressionSocketSystemName = systemName; 455 } 456 457 public FemaleDigitalExpressionSocket getStopExpressionSocket() { 458 return _stopExpressionSocket; 459 } 460 461 public String getStopExpressionSocketSystemName() { 462 return _stopExpressionSocketSystemName; 463 } 464 465 public void setStopExpressionSocketSystemName(String systemName) { 466 _stopExpressionSocketSystemName = systemName; 467 } 468 469 public int getNumActions() { 470 return _actionEntries.size(); 471 } 472 473 public void setNumActions(int num) { 474 List<FemaleSocket> addList = new ArrayList<>(); 475 List<FemaleSocket> removeList = new ArrayList<>(); 476 477 // Is there too many children? 478 while (_actionEntries.size() > num) { 479 ActionEntry ae = _actionEntries.get(num); 480 if (ae._socket.isConnected()) { 481 throw new IllegalArgumentException("Cannot remove sockets that are connected"); 482 } 483 removeList.add(_actionEntries.get(_actionEntries.size()-1)._socket); 484 _actionEntries.remove(_actionEntries.size()-1); 485 } 486 487 // Is there not enough children? 488 while (_actionEntries.size() < num) { 489 FemaleDigitalActionSocket socket = 490 InstanceManager.getDefault(DigitalActionManager.class) 491 .createFemaleSocket(this, this, getNewSocketName()); 492 _actionEntries.add(new ActionEntry(socket)); 493 addList.add(socket); 494 } 495 firePropertyChange(Base.PROPERTY_CHILD_COUNT, removeList, addList); 496 } 497 498 public FemaleDigitalActionSocket getActionSocket(int socket) { 499 return _actionEntries.get(socket)._socket; 500 } 501 502 public String getActionSocketSystemName(int socket) { 503 return _actionEntries.get(socket)._socketSystemName; 504 } 505 506 public void setActionSocketSystemName(int socket, String systemName) { 507 _actionEntries.get(socket)._socketSystemName = systemName; 508 } 509 510 /** {@inheritDoc} */ 511 @Override 512 public void setup() { 513 try { 514 if ( !_startExpressionSocket.isConnected() 515 || !_startExpressionSocket.getConnectedSocket().getSystemName() 516 .equals(_startExpressionSocketSystemName)) { 517 518 String socketSystemName = _startExpressionSocketSystemName; 519 _startExpressionSocket.disconnect(); 520 if (socketSystemName != null) { 521 MaleSocket maleSocket = 522 InstanceManager.getDefault(DigitalExpressionManager.class) 523 .getBySystemName(socketSystemName); 524 if (maleSocket != null) { 525 _startExpressionSocket.connect(maleSocket); 526 maleSocket.setup(); 527 } else { 528 log.error("cannot load digital expression {}", socketSystemName); 529 } 530 } 531 } else { 532 _startExpressionSocket.getConnectedSocket().setup(); 533 } 534 535 if ( !_stopExpressionSocket.isConnected() 536 || !_stopExpressionSocket.getConnectedSocket().getSystemName() 537 .equals(_stopExpressionSocketSystemName)) { 538 539 String socketSystemName = _stopExpressionSocketSystemName; 540 _stopExpressionSocket.disconnect(); 541 if (socketSystemName != null) { 542 MaleSocket maleSocket = 543 InstanceManager.getDefault(DigitalExpressionManager.class) 544 .getBySystemName(socketSystemName); 545 _stopExpressionSocket.disconnect(); 546 if (maleSocket != null) { 547 _stopExpressionSocket.connect(maleSocket); 548 maleSocket.setup(); 549 } else { 550 log.error("cannot load digital expression {}", socketSystemName); 551 } 552 } 553 } else { 554 _stopExpressionSocket.getConnectedSocket().setup(); 555 } 556 557 for (ActionEntry ae : _actionEntries) { 558 if ( !ae._socket.isConnected() 559 || !ae._socket.getConnectedSocket().getSystemName() 560 .equals(ae._socketSystemName)) { 561 562 String socketSystemName = ae._socketSystemName; 563 ae._socket.disconnect(); 564 if (socketSystemName != null) { 565 MaleSocket maleSocket = 566 InstanceManager.getDefault(DigitalActionManager.class) 567 .getBySystemName(socketSystemName); 568 ae._socket.disconnect(); 569 if (maleSocket != null) { 570 ae._socket.connect(maleSocket); 571 maleSocket.setup(); 572 } else { 573 log.error("cannot load digital action {}", socketSystemName); 574 } 575 } 576 } else { 577 ae._socket.getConnectedSocket().setup(); 578 } 579 } 580 } catch (SocketAlreadyConnectedException ex) { 581 // This shouldn't happen and is a runtime error if it does. 582 throw new RuntimeException("socket is already connected"); 583 } 584 } 585 586 /** {@inheritDoc} */ 587 @Override 588 public void registerListenersForThisClass() { 589 if (!_listenersAreRegistered) { 590 _stateMap.computeIfAbsent(getConditionalNG(), o -> new State()); 591 _stateMap.forEach((conditionalNG, state) -> { 592 // If _timerState is not TimerState.Off, the timer was running when listeners wss unregistered 593 if ((_startImmediately) || (state._timerState != TimerState.Off)) { 594 if (state._timerState == TimerState.Off) { 595 state._timerState = TimerState.RunNow; 596 } 597 conditionalNG.execute(); 598 } 599 }); 600 _listenersAreRegistered = true; 601 } 602 } 603 604 /** {@inheritDoc} */ 605 @Override 606 public void unregisterListenersForThisClass() { 607 synchronized(this) { 608 _stateMap.forEach((conditionalNG, state) -> { 609 // stopTimer() will not return until the timer task 610 // is cancelled and stopped. 611 if (state._timerTask != null) state._timerTask.stopTimer(); 612 state._timerTask = null; 613 state._timerState = TimerState.Off; 614 }); 615 } 616 _listenersAreRegistered = false; 617 } 618 619 /** {@inheritDoc} */ 620 @Override 621 public void disposeMe() { 622 synchronized(this) { 623 _stateMap.forEach((conditionalNG, state) -> { 624 if (state._timerTask != null) state._timerTask.stopTimer(); 625 state._timerTask = null; 626 }); 627 } 628 } 629 630 631 private static class ActionEntry { 632 private int _delay; 633 private String _socketSystemName; 634 private final FemaleDigitalActionSocket _socket; 635 636 private ActionEntry(int delay, FemaleDigitalActionSocket socket, String socketSystemName) { 637 _delay = delay; 638 _socketSystemName = socketSystemName; 639 _socket = socket; 640 } 641 642 private ActionEntry(FemaleDigitalActionSocket socket) { 643 this._socket = socket; 644 } 645 646 } 647 648 649 public static class ActionData { 650 private int _delay; 651 private String _socketName; 652 private String _socketSystemName; 653 654 public ActionData(int delay, String socketName, String socketSystemName) { 655 _delay = delay; 656 _socketName = socketName; 657 _socketSystemName = socketSystemName; 658 } 659 } 660 661 662 private enum TimerState { 663 Off, 664 RunNow, 665 WaitToRun, 666 Running, 667 Completed, 668 } 669 670 671 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ActionTimer.class); 672 673}