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}