001package jmri.jmrix.can.cbus.node; 002 003import java.util.ArrayList; 004import javax.annotation.CheckForNull; 005import javax.annotation.Nonnull; 006import jmri.jmrix.can.CanSystemConnectionMemo; 007import jmri.util.ThreadingUtil; 008 009/** 010 * Class to represent a Processing of CAN Frames for a CbusNode. 011 * 012 * @author Steve Young Copyright (C) 2019,2020 013 */ 014public class CbusNodeEventManager { 015 private final CbusBasicNodeWithManagers _node; 016 private final CanSystemConnectionMemo _memo; 017 018 protected int nextEvInArray; 019 private ArrayList<CbusNodeEvent> _nodeEvents; 020 private boolean _eventIndexValid; 021 private ArrayList<CbusNodeEvent> eventsToTeachArray; 022 private int nextEvVar; 023 protected boolean TEACH_OUTSTANDING_EVS; 024 025 /** 026 * Create a new CbusNodeNVManager 027 * 028 * @param memo System connection 029 * @param node The Node 030 */ 031 public CbusNodeEventManager ( CanSystemConnectionMemo memo, CbusBasicNodeWithManagers node ){ 032 _node = node; 033 _memo = memo; 034 _nodeEvents = null; 035 _eventIndexValid = false; 036 TEACH_OUTSTANDING_EVS = false; 037 } 038 039 /** 040 * Returns total number of node events, 041 * including those with outstanding event variables. 042 * 043 * @return number of events, -1 if events not set 044 */ 045 public int getTotalNodeEvents(){ 046 if (_nodeEvents == null) { 047 return -1; 048 } 049 return _nodeEvents.size(); 050 } 051 052 /** 053 * Returns number of fully loaded events, ie no outstanding event variables. 054 * 055 * @return number of loaded events, -1 if events not set 056 */ 057 public int getLoadedNodeEvents(){ 058 if (_nodeEvents == null) { 059 return -1; 060 } 061 int count = 0; 062 for (int i = 0; i < _nodeEvents.size(); i++) { 063 if ( ( _nodeEvents.get(i).getNn() != -1 ) && ( _nodeEvents.get(i).getEn() != -1 )) { 064 count ++; 065 } 066 } 067 return count; 068 } 069 070 /** 071 * Returns outstanding events from initial event fetch. 072 * 073 * @return number of outstanding index events 074 */ 075 public int getOutstandingIndexNodeEvents(){ 076 return getTotalNodeEvents() - getLoadedNodeEvents(); 077 } 078 079 /** 080 * Add an event to the node, will not overwrite an existing event. 081 * Resets Event Index as Invalid for All Node Events 082 * 083 * @param newEvent the new event to be added 084 */ 085 public void addNewEvent( @Nonnull CbusNodeEvent newEvent ) { 086 if (_nodeEvents == null) { 087 _nodeEvents = new ArrayList<>(); 088 } 089 _nodeEvents.add(newEvent); 090 091 setEvIndexValid(false); // Also produces Property Change Event 092 } 093 094 /** 095 * Replace all node events with a number of new events with unknown 096 * node number, event number, index and event variables, 097 * ie. following a NUMEV response from the node. 098 * <p> 099 * Sets the Event Index as Invalid and notifies ALLEVUPDATE once, 100 * rather than once per event as with {@link #addNewEvent(CbusNodeEvent)}. 101 * 102 * @param numEvents the number of events on the node, 0 or more 103 * @param evVarsPerEvent the number of event variables per event 104 */ 105 public void setUnknownEvents( int numEvents, int evVarsPerEvent ) { 106 ArrayList<CbusNodeEvent> newEvents = new ArrayList<>(Math.max(numEvents, 0)); 107 for (int i = 0; i < numEvents; i++) { 108 newEvents.add( new CbusNodeEvent(_memo, -1, -1, _node.getNodeNumber(), -1, evVarsPerEvent) ); 109 } 110 _nodeEvents = newEvents; 111 setEvIndexValid(false); // Also produces Property Change Event 112 } 113 114 /** 115 * Remove an event from the CbusNode, does not update hardware. 116 * 117 * @param nn the event Node Number 118 * @param en the event Event Number 119 */ 120 public void removeEvent(int nn, int en){ 121 _nodeEvents.remove(getNodeEvent(nn, en)); 122 setEvIndexValid(false); // Also produces Property Change Event 123 } 124 125 /** 126 * Get a Node event from its Event and Node number combination 127 * 128 * @param en the Event event number 129 * @param nn the Event node number 130 * @return the node event else null if no Event / Node number combination. 131 */ 132 @CheckForNull 133 public CbusNodeEvent getNodeEvent(int nn, int en) { 134 if (_nodeEvents==null){ 135 return null; 136 } 137 for (int i = 0; i < _nodeEvents.size(); i++) { 138 if ( ( _nodeEvents.get(i).getNn() == nn ) && ( _nodeEvents.get(i).getEn() == en )) { 139 return _nodeEvents.get(i); 140 } 141 } 142 return null; 143 } 144 145 /** 146 * Provide a Node event from its Event and Node number combination 147 * <p> 148 * If an event for this number pair does not already exist on the node 149 * one will be created, else the existing will be returned. 150 * <p> 151 * Adds any new CbusNodeEvent to the node event array, 152 * which will also be created if it doesn't exist. 153 * 154 * @param en the Event event number 155 * @param nn the Event node number 156 * @return the node event 157 */ 158 @Nonnull 159 public CbusNodeEvent provideNodeEvent(int nn, int en) { 160 if (_nodeEvents == null) { 161 _nodeEvents = new ArrayList<>(); 162 } 163 CbusNodeEvent newev = getNodeEvent(nn,en); 164 if ( newev ==null){ 165 newev = new CbusNodeEvent(_memo,nn, en, _node.getNodeNumber(), 166 -1, _node.getNodeParamManager().getParameter(5) ); 167 addNewEvent(newev); 168 } 169 setEvIndexValid(false); // Also produces Property Change Event 170 return newev; 171 } 172 173 /** 174 * Update node with new Node Event. 175 * 176 * @param nn Node Number 177 * @param en Event Number 178 * @param evvarindex Event Variable Index 179 * @param evvarval Event Variable Value 180 */ 181 protected void updateNodeFromLearn(int nn, int en, int evvarindex, int evvarval ){ 182 CbusNodeEvent nodeEv = provideNodeEvent( nn , en ); 183 nodeEv.setEvVar( evvarindex , evvarval ); 184 _node.notifyPropertyChangeListener("ALLEVUPDATE",null,null); 185 186 } 187 188 /** 189 * Get a Node event from its Index Field 190 * <p> 191 * This is NOT the node array index. 192 * 193 * @param index the Node event index, as set by a node from a NERD request 194 * @return the node event, else null if the index is not located 195 */ 196 @CheckForNull 197 public CbusNodeEvent getNodeEventByIndex(int index) { 198 199 if ( _nodeEvents == null ){ 200 return null; 201 } 202 for (int i = 0; i < _nodeEvents.size(); i++) { 203 if ( _nodeEvents.get(i).getIndex() == index ) { 204 return _nodeEvents.get(i); 205 } 206 } 207 return null; 208 } 209 210 /** 211 * Get the Node event Array index from its Index Field 212 * 213 * @param index the Node event index, as set by a node from a NERD request 214 * @return the array index, else -1 if event index number not found in array 215 */ 216 protected int getEventRowFromIndex(int index ){ 217 for (int i = 0; i < _nodeEvents.size(); i++) { 218 if ( _nodeEvents.get(i).getIndex() == index ) { 219 return i; 220 } 221 } 222 return -1; 223 } 224 225 /** 226 * Get the Node event by ArrayList Index. 227 * 228 * @param index the index of the CbusNodeEvent within the ArrayList 229 * @return the Node Event 230 */ 231 @CheckForNull 232 public CbusNodeEvent getNodeEventByArrayID(int index) { 233 return _nodeEvents.get(index); 234 } 235 236 /** 237 * Get the Node event ArrayList 238 * 239 * @return the list of Node Events 240 */ 241 @CheckForNull 242 public ArrayList<CbusNodeEvent> getEventArray(){ 243 return _nodeEvents; 244 } 245 246 /** 247 * Get the Number of Outstanding Event Variables 248 * <p> 249 * Sometimes, the Event Variables have to be initialised with an unknown 250 * status, this returns a count of unknown Event Variables for the whole Node. 251 * 252 * @return -1 if main node events array null, else number Outstanding Ev Vars 253 */ 254 public int getOutstandingEvVars(){ 255 int count = 0; 256 ArrayList<CbusNodeEvent> evs = getEventArray(); 257 if ( evs == null ){ 258 return -1; 259 } 260 for (int i = 0; i < evs.size(); i++) { 261 count += evs.get(i).getOutstandingVars(); 262 } 263 return count; 264 } 265 266 /** 267 * The last message from the node CMDERR5 indicates that all remaining event variables 268 * for a particular event are not required. 269 * This sets the remaining ev vars to 0 so are not requested 270 */ 271 protected void remainingEvVarsNotNeeded(){ 272 ArrayList<CbusNodeEvent> evs = getEventArray(); 273 if (evs!=null) { 274 for (int i = 0; i < evs.size(); i++) { 275 if ( evs.get(i).getNextOutstanding() > 0 ) { 276 evs.get(i).allOutstandingEvVarsNotNeeded(); 277 278 // cancel Timer 279 _node.getNodeTimerManager().clearNextEvVarTimeout(); 280 // update GUI 281 _node.notifyPropertyChangeListener("SINGLEEVUPDATE",null,i); 282 return; 283 } 284 } 285 } 286 } 287 288 /** 289 * Send a request for the next unknown Event Variable to the Physical Node 290 * <p> 291 * If events are unknown, sends the NERD request and starts that timer, 292 * else requests the next Ev Var with unknown status ( represented as int value -1 ) 293 * Will NOT send if any Node is in Learn Mode or if there are any outstanding requests from the Node. 294 */ 295 protected void sendNextEvVarToFetch() { 296 ArrayList<CbusNodeEvent> _evs = getEventArray(); 297 // do not request if node is learn mode 298 if (( _node.getTableModel().getAnyNodeInLearnMode() > -1 ) 299 || ( _evs == null ) 300 || ( _node.getNodeTimerManager().hasActiveTimers()) ){ 301 return; 302 } 303 304 // if events on module, get their event, node and node index 305 // *** This could produce up to 255 responses per node *** 306 if ( ( getTotalNodeEvents() > 0 ) && getOutstandingIndexNodeEvents()>0 ) { 307 _node.send.nERD( _node.getNodeNumber() ); 308 // starts timeout 309 _node.getNodeTimerManager().setAllEvTimeout(); 310 return; 311 } 312 313 for (int i = 0; i < _evs.size(); i++) { 314 if ( _evs.get(i).getOutstandingVars() > 0 ) { 315 int index = _evs.get(i).getIndex(); 316 int nextevvar = _evs.get(i).getNextOutstanding(); 317 318 // index from NERD / ENRSP indexing may start at 0 319 if ( index > -1 ) { 320 321 // start timer 322 _node.getNodeTimerManager().setNextEvVarTimeout( nextevvar,_evs.get(i).toString() ); 323 _node.send.rEVAL( _node.getNodeNumber(), index, nextevvar ); 324 return; 325 } 326 else { // if index < 0 event index is invalid so attempt refetch. 327 // reset events 328 log.info("Invalid index, resetting events for node {}", _node ); 329 _nodeEvents = null; 330 return; 331 } 332 } 333 } 334 } 335 336 /** 337 * Used in CBUS_NEVAL response from Node. 338 * Set the value of an event variable by event Index and event Variable Index 339 * @param eventIndex Event Index 340 * @param eventVarIndex Event Variable Index 341 * @param newVal New Value 342 */ 343 protected void setEvVarByIndex(int eventIndex, int eventVarIndex, int newVal) { 344 CbusNodeEvent nodeEvByIndex = getNodeEventByIndex(eventIndex); 345 if ( nodeEvByIndex != null ) { 346 nodeEvByIndex.setEvVar(eventVarIndex,newVal); 347 _node.notifyPropertyChangeListener("SINGLEEVUPDATE",null,getEventRowFromIndex(eventIndex)); 348 } 349 } 350 351 /** 352 * Used to process a CBUS_ENRSP response from node 353 * 354 * If existing index known, use that slot in the event array, 355 * else if event array has empty slot for that index, use that slot. 356 * @param nn Node Number 357 * @param en Event Number 358 * @param index Index Number 359 */ 360 protected void setNextEmptyNodeEvent(int nn, int en, int index){ 361 ArrayList<CbusNodeEvent> _evs = getEventArray(); 362 if ( _evs == null ){ 363 log.error("Indexed events are not expected as total number of events unknown"); 364 return; 365 } else { 366 for (int i = 0; i < _evs.size(); i++) { 367 if ( _evs.get(i).getIndex() == index ) { 368 _evs.get(i).setNn(nn); 369 _evs.get(i).setEn(en); 370 _node.notifyPropertyChangeListener("SINGLEEVUPDATE",null,i); 371 return; 372 } 373 } 374 } 375 376 for (int i = 0; i < _nodeEvents.size(); i++) { 377 if ( ( _nodeEvents.get(i).getNn() == -1 ) && ( _nodeEvents.get(i).getEn() == -1 ) ) { 378 _nodeEvents.get(i).setNn(nn); 379 _nodeEvents.get(i).setEn(en); 380 _nodeEvents.get(i).setIndex(index); 381 382 _node.notifyPropertyChangeListener("SINGLEEVUPDATE",null,i); 383 return; 384 } 385 } 386 log.error("Issue setting node event, index {} not valid",index); 387 _nodeEvents = null; 388 } 389 390 /** 391 * Get if the Node event index is valid. 392 * @return true if event index is valid, else false if invalid or no events on node. 393 */ 394 protected boolean isEventIndexValid(){ 395 return _eventIndexValid; 396 } 397 398 /** 399 * Set the Node event index flag as valid or invalid. 400 * <p> 401 * Resets all Node Event Indexes to -1 if invalid. 402 * @param newval true if Event Index Valid, else false 403 */ 404 protected void setEvIndexValid( boolean newval ) { 405 _eventIndexValid = newval; 406 if (!newval && _nodeEvents != null ){ // event index no longer valid so clear values in individual events 407 for (int i = 0; i < _nodeEvents.size(); i++) { 408 _nodeEvents.get(i).setIndex(-1); 409 } 410 } 411 _node.notifyPropertyChangeListener("ALLEVUPDATE",null,null); 412 } 413 414 /** 415 * Send and teach updated Events to this node 416 * 417 * @param evArray array of CbusNodeEvents to be taught 418 */ 419 public void sendNewEvSToNode( @Nonnull ArrayList<CbusNodeEvent> evArray ){ 420 eventsToTeachArray = evArray; 421 422 if (eventsToTeachArray==null){ 423 _node.getNodeTimerManager().sendEvErrorCount=1; 424 teachEventsComplete(); 425 return; 426 } 427 428 if (eventsToTeachArray.isEmpty()){ 429 teachEventsComplete(); 430 return; 431 } 432 433 // check other nodes in learn mode 434 if ( _node.getTableModel().getAnyNodeInLearnMode() > -1 ) { 435 String err = "Cancelling teach event. Node " + _node.getTableModel().getAnyNodeInLearnMode() + " is already in Learn Mode"; 436 log.warn("Unable to teach: {}",err); 437 _node.notifyPropertyChangeListener("ADDEVCOMPLETE", null, err); 438 return; 439 } 440 441 TEACH_OUTSTANDING_EVS = true; 442 nextEvInArray = 0; 443 nextEvVar = 1; // start at 1 as 0 is used for total ev vars 444 _node.getNodeTimerManager().sendEvErrorCount = 0; 445 446 _node.send.nodeEnterLearnEvMode( _node.getNodeNumber() ); // no response expected but we add a mini delay for other traffic 447 448 log.debug("sendNewEvSToNode {}",evArray); 449 450 ThreadingUtil.runOnLayoutDelayed( () -> { 451 teachNewEvLoop(); 452 }, 50 ); 453 454 } 455 456 /** 457 * Send a message to delete an event stored on this node 458 * 459 * @param nn event node number 460 * @param en event event number 461 */ 462 public void deleteEvOnNode( int nn, int en){ 463 464 // check other nodes in learn mode 465 if ( _node.getTableModel().getAnyNodeInLearnMode() > -1 ) { 466 String err = "Cancelling delete event. Node " + _node.getTableModel().getAnyNodeInLearnMode() + " is already in Learn Mode"; 467 log.warn("Unable to teach: {}",err); 468 _node.notifyPropertyChangeListener("DELETEEVCOMPLETE", null, err); 469 return; 470 } 471 472 _node.send.nodeEnterLearnEvMode( _node.getNodeNumber() ); 473 // no response expected but we add a mini delay for other traffic 474 ThreadingUtil.runOnLayoutDelayed( () -> { 475 _node.send.nodeUnlearnEvent( nn, en ); 476 setEvIndexValid(false); 477 }, 50 ); 478 log.info("Deleted Event {} on Node {}", 479 new jmri.jmrix.can.cbus.CbusEvent(nn,en),_node.getNodeNumber()); 480 ThreadingUtil.runOnGUIDelayed( () -> { 481 _node.send.nodeExitLearnEvMode( _node.getNodeNumber() ); 482 // notify ui 483 _node.notifyPropertyChangeListener("DELETEEVCOMPLETE", null, ""); 484 }, 100 ); 485 } 486 487 private void teachEventsComplete(){ 488 489 TEACH_OUTSTANDING_EVS = false; 490 _node.send.nodeExitLearnEvMode( _node.getNodeNumber() ); 491 String err; 492 if ( _node.getNodeTimerManager().sendEvErrorCount==0 ) { 493 log.info("Completed Event Write with No errors, node {}.", _node ); 494 err = ""; 495 } 496 else { 497 err = "Event Write Failed with "+ _node.getNodeTimerManager().sendEvErrorCount +" errors."; 498 log.error("{} Node {}.", err, _node); 499 500 } 501 // notify ui's 502 ThreadingUtil.runOnGUIDelayed( () -> { 503 _node.notifyPropertyChangeListener("ADDEVCOMPLETE", null, err); 504 _node.notifyPropertyChangeListener("ADDALLEVCOMPLETE", null, _node.getNodeTimerManager().sendEvErrorCount); 505 _node.getNodeTimerManager().sendEvErrorCount=0; 506 },50 ); 507 } 508 509 /** 510 * Loop for event teaching, triggered from various places 511 */ 512 protected void teachNewEvLoop(){ 513 514 if ( nextEvVar > _node.getNodeParamManager().getParameter(5) ) { 515 nextEvVar = 1; 516 nextEvInArray++; 517 } 518 519 if ( nextEvInArray >= eventsToTeachArray.size() ) { 520 log.debug("all done"); 521 teachEventsComplete(); 522 return; 523 } 524 525 CbusNodeEvent wholeEvent = eventsToTeachArray.get(nextEvInArray); 526 log.debug("event variable array length: {}", wholeEvent.getEvVarArray().length); 527 log.debug("Number of event vars from node parameters: {}",_node.getNodeParamManager().getParameter(CbusNodeConstants.EV_PER_EN_IDX) ); 528 if (wholeEvent.getEvVarArray().length < nextEvVar ){ 529 log.warn("Incorrect number of event variables ({}) for {}", 530 wholeEvent.getEvVarArray().length ,wholeEvent ); 531 _node.getNodeTimerManager().sendEvErrorCount++; 532 nextEvVar = 1; 533 nextEvInArray++; 534 teachNewEvLoop(); 535 return; 536 } 537 538 log.debug("teach event var {} val {} ",nextEvVar,wholeEvent.getEvVar(nextEvVar)); 539 CbusNodeEvent existingEvent = getNodeEvent( wholeEvent.getNn(), wholeEvent.getEn() ); 540 541 log.debug("teach event {} with existing event {}",wholeEvent,existingEvent); 542 543 // increment and restart loop if no change 544 if ((existingEvent!=null) && (existingEvent.getEvVar(nextEvVar)==wholeEvent.getEvVar(nextEvVar))){ 545 nextEvVar++; 546 teachNewEvLoop(); 547 return; 548 } 549 550 // start timeout , send message, increment for next run, only sent when WRACK received 551 _node.getNodeTimerManager().setsendEditEvTimeout(); 552 _node.send.nodeTeachEventLearnMode(wholeEvent.getNn(),wholeEvent.getEn(),nextEvVar,wholeEvent.getEvVar(nextEvVar)); 553 nextEvVar++; 554 } 555 556 /** 557 * Resets Node Events with null array. 558 * For when a CbusNode is reset to unknown events. 559 */ 560 public void resetNodeEvents() { 561 _nodeEvents = null; 562 _node.notifyPropertyChangeListener("ALLEVUPDATE",null,null); 563 } 564 565 /** 566 * Resets Node Events with zero length array. 567 * For when a CbusNode is reset to 0 events 568 * 569 */ 570 public void resetNodeEventsToZero() { 571 _nodeEvents = null; 572 _nodeEvents = new ArrayList<>(); 573 _node.notifyPropertyChangeListener("ALLEVUPDATE",null,null); 574 } 575 576 /** 577 * the next event index for a CbusDummyNode NODE to allocate, 578 * NOT a software tool. 579 * @return next available event index 580 */ 581 public int getNextFreeIndex(){ 582 int newIndex = 1; 583 for (int i = 0; i < getTotalNodeEvents(); i++) { 584 CbusNodeEvent a = getNodeEventByArrayID(i); 585 if ( (a!=null) && newIndex <= a.getIndex() ) { 586 newIndex = a.getIndex()+1; 587 } 588 } 589 log.debug("dummy node sets index {}",newIndex); 590 return newIndex; 591 } 592 593 /** 594 * @return descriptive string 595 */ 596 @Override 597 public String toString() { 598 return "Node Events"; 599 } 600 601 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CbusNodeEventManager.class); 602 603}