001package jmri.jmrit.logixng.expressions; 002 003import java.util.*; 004 005import javax.annotation.Nonnull; 006 007import jmri.InstanceManager; 008import jmri.JmriException; 009import jmri.jmrit.logixng.*; 010import jmri.jmrit.logixng.util.*; 011import jmri.jmrit.logixng.util.parser.*; 012import jmri.util.TimerUtil; 013import jmri.util.TypeConversionUtil; 014 015/** 016 * An expression that waits some time before returning True. 017 * 018 * This expression returns False until some time has elapsed. Then it returns 019 * True once. After that, it returns False again until some time has elapsed. 020 * 021 * @author Daniel Bergqvist Copyright 2023 022 */ 023public class Timer extends AbstractDigitalExpression { 024 025 private static class StateAndTimerTask{ 026 ProtectedTimerTask _timerTask; 027 State _currentState = State.IDLE; 028 } 029 030 private enum State { IDLE, RUNNING, COMPLETED } 031 032 private final Map<ConditionalNG, StateAndTimerTask> _stateAndTimerTask = new HashMap<>(); 033 private int _delay; 034 private NamedBeanAddressing _stateAddressing = NamedBeanAddressing.Direct; 035 private TimerUnit _unit = TimerUnit.MilliSeconds; 036 private String _stateReference = ""; 037 private String _stateLocalVariable = ""; 038 private String _stateFormula = ""; 039 private ExpressionNode _stateExpressionNode; 040 041 042 public Timer(String sys, String user) 043 throws BadUserNameException, BadSystemNameException { 044 super(sys, user); 045 } 046 047 @Override 048 public Base getDeepCopy(Map<String, String> systemNames, Map<String, String> userNames) throws JmriException { 049 DigitalExpressionManager manager = InstanceManager.getDefault(DigitalExpressionManager.class); 050 String sysName = systemNames.get(getSystemName()); 051 String userName = userNames.get(getSystemName()); 052 if (sysName == null) sysName = manager.getAutoSystemName(); 053 Timer copy = new Timer(sysName, userName); 054 copy.setComment(getComment()); 055 copy.setDelayAddressing(_stateAddressing); 056 copy.setDelay(_delay); 057 copy.setDelayFormula(_stateFormula); 058 copy.setDelayLocalVariable(_stateLocalVariable); 059 copy.setDelayReference(_stateReference); 060 copy.setUnit(_unit); 061 return manager.registerExpression(copy).deepCopyChildren(this, systemNames, userNames); 062 } 063 064 /** {@inheritDoc} */ 065 @Override 066 public Category getCategory() { 067 return Category.COMMON; 068 } 069 070 /** 071 * Get a new timer task. 072 * @param conditionalNG the ConditionalNG 073 * @param timerDelay the time the timer should wait 074 * @param timerStart the time when the timer was started 075 */ 076 private ProtectedTimerTask getNewTimerTask(ConditionalNG conditionalNG, long timerDelay, long timerStart) throws JmriException { 077 078 return new ProtectedTimerTask() { 079 @Override 080 public void execute() { 081 try { 082 synchronized(Timer.this) { 083 StateAndTimerTask stateAndTimerTask = _stateAndTimerTask.get(conditionalNG); 084 stateAndTimerTask._timerTask = null; 085 086 long currentTime = System.currentTimeMillis(); 087 long currentTimerTime = currentTime - timerStart; 088 if (currentTimerTime < timerDelay) { 089 scheduleTimer(conditionalNG, timerDelay, timerStart); 090 } else { 091 stateAndTimerTask._currentState = State.COMPLETED; 092 if (conditionalNG.isListenersRegistered()) { 093 conditionalNG.execute(); 094 } 095 } 096 } 097 } catch (RuntimeException | JmriException e) { 098 log.error("Exception thrown", e); 099 } 100 } 101 }; 102 } 103 104 private void scheduleTimer(ConditionalNG conditionalNG, long timerDelay, long timerStart) throws JmriException { 105 synchronized(Timer.this) { 106 StateAndTimerTask stateAndTimerTask = _stateAndTimerTask.get(conditionalNG); 107 if (stateAndTimerTask._timerTask != null) { 108 stateAndTimerTask._timerTask.stopTimer(); 109 } 110 long currentTime = System.currentTimeMillis(); 111 long currentTimerTime = currentTime - timerStart; 112 stateAndTimerTask._timerTask = getNewTimerTask(conditionalNG, timerDelay, timerStart); 113 TimerUtil.schedule(stateAndTimerTask._timerTask, timerDelay - currentTimerTime); 114 } 115 } 116 117 private long getNewDelay(ConditionalNG conditionalNG) throws JmriException { 118 119 switch (_stateAddressing) { 120 case Direct: 121 return _delay; 122 123 case Reference: 124 return TypeConversionUtil.convertToLong(ReferenceUtil.getReference( 125 conditionalNG.getSymbolTable(), _stateReference)); 126 127 case LocalVariable: 128 SymbolTable symbolTable = conditionalNG.getSymbolTable(); 129 return TypeConversionUtil 130 .convertToLong(symbolTable.getValue(_stateLocalVariable)); 131 132 case Formula: 133 return _stateExpressionNode != null 134 ? TypeConversionUtil.convertToLong( 135 _stateExpressionNode.calculate( 136 conditionalNG.getSymbolTable())) 137 : 0; 138 139 default: 140 throw new IllegalArgumentException("invalid _addressing state: " + _stateAddressing.name()); 141 } 142 } 143 144 /** {@inheritDoc} */ 145 @Override 146 public boolean evaluate() throws JmriException { 147 synchronized(this) { 148 ConditionalNG conditionalNG = getConditionalNG(); 149 StateAndTimerTask stateAndTimerTask = _stateAndTimerTask 150 .computeIfAbsent(conditionalNG, o -> new StateAndTimerTask()); 151 152 switch (stateAndTimerTask._currentState) { 153 case RUNNING: 154 return false; 155 case COMPLETED: 156 stateAndTimerTask._currentState = State.IDLE; 157 return true; 158 case IDLE: 159 stateAndTimerTask._currentState = State.RUNNING; 160 if (stateAndTimerTask._timerTask != null) { 161 stateAndTimerTask._timerTask.stopTimer(); 162 } 163 long timerStart = System.currentTimeMillis(); 164 long timerDelay = getNewDelay(conditionalNG) * _unit.getMultiply(); 165 scheduleTimer(conditionalNG, timerDelay, timerStart); 166 return false; 167 default: 168 throw new UnsupportedOperationException("currentState has invalid state: "+stateAndTimerTask._currentState.name()); 169 } 170 } 171 } 172 173 public void setDelayAddressing(NamedBeanAddressing addressing) throws ParserException { 174 _stateAddressing = addressing; 175 parseDelayFormula(); 176 } 177 178 public NamedBeanAddressing getDelayAddressing() { 179 return _stateAddressing; 180 } 181 182 /** 183 * Get the delay. 184 * @return the delay 185 */ 186 public int getDelay() { 187 return _delay; 188 } 189 190 /** 191 * Set the delay. 192 * @param delay the delay 193 */ 194 public void setDelay(int delay) { 195 _delay = delay; 196 } 197 198 public void setDelayReference(@Nonnull String reference) { 199 if ((! reference.isEmpty()) && (! ReferenceUtil.isReference(reference))) { 200 throw new IllegalArgumentException("The reference \"" + reference + "\" is not a valid reference"); 201 } 202 _stateReference = reference; 203 } 204 205 public String getDelayReference() { 206 return _stateReference; 207 } 208 209 public void setDelayLocalVariable(@Nonnull String localVariable) { 210 _stateLocalVariable = localVariable; 211 } 212 213 public String getDelayLocalVariable() { 214 return _stateLocalVariable; 215 } 216 217 public void setDelayFormula(@Nonnull String formula) throws ParserException { 218 _stateFormula = formula; 219 parseDelayFormula(); 220 } 221 222 public String getDelayFormula() { 223 return _stateFormula; 224 } 225 226 private void parseDelayFormula() throws ParserException { 227 if (_stateAddressing == NamedBeanAddressing.Formula) { 228 Map<String, Variable> variables = new HashMap<>(); 229 230 RecursiveDescentParser parser = new RecursiveDescentParser(variables); 231 _stateExpressionNode = parser.parseExpression(_stateFormula); 232 } else { 233 _stateExpressionNode = null; 234 } 235 } 236 237 /** 238 * Get the unit 239 * @return the unit 240 */ 241 public TimerUnit getUnit() { 242 return _unit; 243 } 244 245 /** 246 * Set the unit 247 * @param unit the unit 248 */ 249 public void setUnit(TimerUnit unit) { 250 _unit = unit; 251 } 252 253 @Override 254 public String getShortDescription(Locale locale) { 255 return Bundle.getMessage(locale, "Timer_Short"); 256 } 257 258 @Override 259 public String getLongDescription(Locale locale) { 260 String delay; 261 262 switch (_stateAddressing) { 263 case Direct: 264 delay = Bundle.getMessage(locale, "Timer_DelayByDirect", _unit.getTimeWithUnit(_delay)); 265 break; 266 267 case Reference: 268 delay = Bundle.getMessage(locale, "Timer_DelayByReference", _stateReference, _unit.toString()); 269 break; 270 271 case LocalVariable: 272 delay = Bundle.getMessage(locale, "Timer_DelayByLocalVariable", _stateLocalVariable, _unit.toString()); 273 break; 274 275 case Formula: 276 delay = Bundle.getMessage(locale, "Timer_DelayByFormula", _stateFormula, _unit.toString()); 277 break; 278 279 default: 280 throw new IllegalArgumentException("invalid _stateAddressing state: " + _stateAddressing.name()); 281 } 282 283 return Bundle.getMessage(locale, "Timer_Long", delay); 284 } 285 286 /** {@inheritDoc} */ 287 @Override 288 public void setup() { 289 // Do nothing 290 } 291 292 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Timer.class); 293 294}