001package jmri.jmrix.can.cbus.node; 002 003import java.beans.PropertyChangeListener; 004import java.beans.PropertyChangeEvent; 005import java.io.File; 006import java.util.ArrayList; 007import java.util.Arrays; 008import java.util.List; 009import java.util.TimerTask; 010import javax.annotation.Nonnull; 011import jmri.jmrix.can.*; 012import jmri.jmrix.can.cbus.CbusConstants; 013import jmri.jmrix.can.cbus.CbusMessage; 014import jmri.jmrix.can.cbus.CbusPreferences; 015import jmri.jmrix.can.cbus.CbusSend; 016import jmri.jmrix.can.cbus.swing.nodeconfig.NodeConfigToolPane; 017import jmri.util.*; 018 019/** 020 * Table data model for display of CBUS Nodes 021 * 022 * @author Steve Young (c) 2019 023 * 024 */ 025public class CbusNodeTableDataModel extends CbusBasicNodeTableFetch 026 implements CanListener, PropertyChangeListener, jmri.Disposable { 027 028 private final CbusSend send; 029 private ArrayList<Integer> _nodesFound; 030 private CbusAllocateNodeNumber allocate; 031 protected CbusPreferences preferences; 032 033 public CbusNodeTableDataModel(@Nonnull CanSystemConnectionMemo memo, int row, int column) { 034 this(memo,row); 035 } 036 037 /** 038 * Create a new CbusNodeTableDataModel. 039 * @param memo system connection. 040 * @param initialArraySize initial Array Size. 041 */ 042 public CbusNodeTableDataModel(@Nonnull CanSystemConnectionMemo memo, int initialArraySize ) { 043 super(memo, initialArraySize, 0); 044 log.debug("Starting MERG CBUS Node Table memo \"{}\" ",memo); 045 _nodesFound = new ArrayList<>(initialArraySize); 046 // connect to the CanInterface 047 addTc(memo); 048 049 send = new CbusSend(memo); 050 051 startup(); 052 } 053 054 private void startup(){ 055 056 preferences = _memo.get(CbusPreferences.class); 057 if (preferences == null ) { 058 log.error("no prefs"); 059 return; 060 } 061 062 setBackgroundAllocateListener( preferences.getAllocateNNListener() ); 063 if ( preferences.getStartupSearchForCs() ) { 064 send.searchForCommandStations(); 065 } 066 if ( preferences.getStartupSearchForNodes() ) { 067 send.searchForNodes(); 068 setSearchForNodesTimeout( 5000 ); 069 } else 070 if ( preferences.getSearchForNodesBackupXmlOnStartup() ) { 071 // it's preferable to do this AFTER the network search timeout, 072 // however we also test here in case there is no timeout 073 startupSearchNodeXmlFile(); 074 } 075 } 076 077 // start listener for nodes requesting a new node number 078 public void setBackgroundAllocateListener( boolean newState ){ 079 if (newState && !java.awt.GraphicsEnvironment.isHeadless() ) { 080 if (allocate == null) { 081 allocate = new CbusAllocateNodeNumber( _memo, this ); 082 } else { 083 } 084 } else { 085 if ( allocate != null ) { 086 allocate.dispose(); 087 } 088 allocate = null; 089 } 090 } 091 092 /** 093 * Unused, even simulated nodes / command stations normally respond with CanReply 094 * @param m canmessage 095 */ 096 @Override 097 public void message(CanMessage m) { // outgoing cbus message 098 } 099 100 private int csFound=0; 101 private int ndFound = 0; 102 103 /** 104 * Listen on the network for incoming STAT and PNN OPC's 105 * @param m incoming CanReply 106 */ 107 @Override 108 public void reply(CanReply m) { // incoming cbus message 109 if ( m.extendedOrRtr() ) { 110 return; 111 } 112 int nodenum = ( m.getElement(1) * 256 ) + m.getElement(2); 113 switch (CbusMessage.getOpcode(m)) { 114 case CbusConstants.CBUS_STAT: 115 // log.debug("Command Station Updates Status {}",m); 116 117 if ( preferences.getAddCommandStations() ) { 118 119 int csnum = m.getElement(3); 120 // provides a command station by cs number, NOT node number 121 CbusNode cs = provideCsByNum(csnum,nodenum); 122 cs.setFW(m.getElement(5),m.getElement(6),m.getElement(7)); 123 cs.setCsFlags(m.getElement(4)); 124 cs.setCanId(CbusMessage.getId(m)); 125 126 } _nodesFound.add(nodenum); 127 csFound++; 128 break; 129 case CbusConstants.CBUS_PNN: 130 log.debug("Node Report message {}",m); 131 if ( searchForNodesTask != null && preferences.getAddNodes() ) { 132 // provides a node by node number 133 CbusNode nd = provideNodeByNodeNum(nodenum); 134 nd.setManuModule(m.getElement(3),m.getElement(4)); 135 nd.setNodeFlags(m.getElement(5)); 136 nd.setCanId(CbusMessage.getId(m)); 137 } _nodesFound.add(nodenum); 138 ndFound++; 139 break; 140 case CbusConstants.CBUS_NNREL: 141 // from node advising releasing node number 142 if ( getNodeRowFromNodeNum(nodenum) >-1 ) { 143 log.info("{} : NNREL",Bundle.getMessage("NdRelease", getNodeName(nodenum), nodenum ) ); 144 removeRow( getNodeRowFromNodeNum(nodenum),false ); 145 } 146 break; 147 default: 148 break; 149 } 150 } 151 152 /** {@inheritDoc} */ 153 @Override 154 public void propertyChange(PropertyChangeEvent ev){ 155 if (!(ev.getSource() instanceof CbusNode)) { 156 return; 157 } 158 159 int evRow = getNodeRowFromNodeNum((( CbusNode ) ev.getSource()).getNodeNumber()); 160 if (evRow<0){ 161 return; 162 } 163 ThreadingUtil.runOnGUIEventually( ()->{ 164 switch (ev.getPropertyName()) { 165 case "SINGLENVUPDATE": 166 case "ALLNVUPDATE": 167 log.debug("Table data model recieves property change row: {}", evRow); 168 fireTableCellUpdated(evRow, BYTES_REMAINING_COLUMN); 169 fireTableCellUpdated(evRow, NODE_TOTAL_BYTES_COLUMN); 170 break; 171 case "ALLEVUPDATE": 172 case "SINGLEEVUPDATE": 173 fireTableCellUpdated(evRow, NODE_EVENT_INDEX_VALID_COLUMN); 174 fireTableCellUpdated(evRow, NODE_EVENTS_COLUMN); 175 fireTableCellUpdated(evRow, BYTES_REMAINING_COLUMN); 176 fireTableCellUpdated(evRow, NODE_TOTAL_BYTES_COLUMN); 177 break; 178 case "BACKUPS": 179 fireTableCellUpdated(evRow, SESSION_BACKUP_STATUS_COLUMN); 180 fireTableCellUpdated(evRow, NUMBER_BACKUPS_COLUMN); 181 fireTableCellUpdated(evRow, LAST_BACKUP_COLUMN); 182 break; 183 case "PARAMETER": 184 fireTableRowsUpdated(evRow,evRow); 185 break; 186 case "LEARNMODE": 187 fireTableCellUpdated(evRow,NODE_IN_LEARN_MODE_COLUMN); 188 break; 189 case "NAMECHANGE": 190 fireTableCellUpdated(evRow,NODE_USER_NAME_COLUMN); 191 break; 192 case "CANID": 193 fireTableCellUpdated(evRow,CANID_COLUMN); 194 break; 195 default: 196 break; 197 } 198 }); 199 } 200 201 private NodeConfigToolPane searchFeedbackPanel; 202 203 /** 204 * Sends a search for Nodes with timeout 205 * @param panel Feedback pane, can be null 206 * @param timeout in ms 207 */ 208 public void startASearchForNodes( NodeConfigToolPane panel, int timeout ){ 209 searchFeedbackPanel = panel; 210 csFound=0; 211 ndFound=0; 212 setSearchForNodesTimeout( timeout ); 213 send.searchForCommandStations(); 214 send.searchForNodes(); 215 } 216 217 private TimerTask searchForNodesTask; 218 219 /** 220 * Loop through main table, add a not found note to any nodes 221 * which are on the table but not on this list. 222 */ 223 private void checkOnlineNodesVsTable(){ 224 log.debug("{} Nodes found, {}",_nodesFound.size(),_nodesFound); 225 for (int i = 0; i < getRowCount(); i++) { 226 if ( ! _nodesFound.contains(_mainArray.get(i).getNodeNumber() )) { 227 log.debug("No network response from Node {}",_mainArray.get(i)); 228 _mainArray.get(i).nodeOnNetwork(false); 229 } 230 } 231 // if node heard but flagged as off-network, reset 232 _nodesFound.stream().map((foundNodeNum) -> getNodeByNodeNum(foundNodeNum)).filter((foundNode) 233 -> ( foundNode != null && foundNode.getNodeBackupManager().getSessionBackupStatus() == CbusNodeConstants.BackupType.NOTONNETWORK )).map((foundNode) -> { 234 foundNode.resetNodeAll(); 235 return foundNode; 236 }).forEachOrdered((_item) -> { 237 startBackgroundFetch(); 238 }); 239 } 240 241 /** 242 * Clears Node Search Timer 243 */ 244 private void clearSearchForNodesTimeout(){ 245 if (searchForNodesTask != null ) { 246 searchForNodesTask.cancel(); 247 searchForNodesTask = null; 248 } 249 } 250 251 /** 252 * Starts Search for Nodes Timer 253 * @param timeout value in msec to wait for responses 254 */ 255 private void setSearchForNodesTimeout( int timeout) { 256 _nodesFound = new ArrayList<>(5); 257 searchForNodesTask = new TimerTask() { 258 @Override 259 public void run() { 260 // searchForNodesTask = null; 261 // log.info("Node search complete " ); 262 if ( searchFeedbackPanel !=null ) { 263 searchFeedbackPanel.notifyNodeSearchComplete(csFound,ndFound); 264 } 265 266 // it's preferable to perform this check here, AFTER the network search timeout 267 // as JMRI may be starting up and this is not time sensitive. 268 if ( preferences.getSearchForNodesBackupXmlOnStartup() ) { 269 startupSearchNodeXmlFile(); 270 } 271 272 checkOnlineNodesVsTable(); 273 clearSearchForNodesTimeout(); 274 } 275 }; 276 TimerUtil.schedule(searchForNodesTask, timeout); 277 } 278 279 private boolean searchXmlComplete = false; 280 281 /** 282 * Search the directory for nodes, ie userPref/cbus/123.xml 283 * Add any found to the Node Manager Table 284 * (Modelled after a method in jmri.jmrit.dispatcher.TrainInfoFile ) 285 */ 286 public void startupSearchNodeXmlFile() { 287 // ensure preferences will be found for read 288 FileUtil.createDirectory(new CbusNodeBackupFile(_memo).getFileLocation()); 289 // create an array of file names from node dir in preferences, then loop 290 List<String> names = new ArrayList<>(5); 291 File fp = new File(new CbusNodeBackupFile(_memo).getFileLocation()); 292 if (fp.exists()) { 293 String[] fpList = fp.list(new XmlFilenameFilter()); 294 if (fpList !=null ) { 295 names.addAll(Arrays.asList(fpList)); 296 } 297 } 298 names.forEach((nb) -> { 299 log.debug("Node: {}",nb); 300 int nodeNum = jmri.util.StringUtil.getFirstIntFromString(nb); 301 CbusNode nd = provideNodeByNodeNum(nodeNum); 302 nd.getNodeBackupManager().doLoad(); 303 log.debug("CbusNode {} added to table",nd); 304 }); 305 searchXmlComplete = true; 306 } 307 308 public boolean startupComplete(){ 309 return !(!searchXmlComplete && searchForNodesTask != null); 310 } 311 312 /** 313 * Disconnect from the network 314 * <p> 315 * Close down any background listeners 316 * <p> 317 * Cancel outstanding Timers 318 */ 319 @Override 320 public void dispose() { 321 322 clearSearchForNodesTimeout(); 323 if ( trickleFetch != null ) { 324 trickleFetch.dispose(); 325 trickleFetch = null; 326 } 327 328 setBackgroundAllocateListener(false); // stop listening for node number requests 329 330 removeTc(_memo); 331 332 for (int i = 0; i < getRowCount(); i++) { 333 _mainArray.get(i).removePropertyChangeListener(this); 334 _mainArray.get(i).dispose(); 335 } 336 // _mainArray = null; 337 338 } 339 340 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CbusNodeTableDataModel.class); 341}