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