001package jmri.jmrix.can.cbus.node;
002
003import java.awt.BorderLayout;
004import java.awt.Color;
005import java.awt.GridLayout;
006import java.awt.Toolkit;
007import java.util.TimerTask;
008
009import javax.swing.JFormattedTextField;
010import javax.swing.JLabel;
011import javax.swing.JPanel;
012import javax.swing.JSpinner;
013import javax.swing.SpinnerNumberModel;
014import javax.swing.event.ChangeEvent;
015
016import jmri.InvokeOnAnyThread;
017import jmri.InvokeOnGuiThread;
018import jmri.jmrix.can.CanListener;
019import jmri.jmrix.can.CanMessage;
020import jmri.jmrix.can.CanSystemConnectionMemo;
021import jmri.jmrix.can.CanReply;
022import jmri.jmrix.can.cbus.CbusConstants;
023import jmri.jmrix.can.cbus.CbusMessage;
024import jmri.jmrix.can.cbus.CbusPreferences;
025import jmri.jmrix.can.cbus.CbusSend;
026import jmri.util.ThreadingUtil;
027import jmri.util.TimerUtil;
028import jmri.util.swing.JmriJOptionPane;
029import jmri.util.swing.JSpinnerUtil;
030
031/**
032 * Listens for CBUS Nodes requesting a Node Number, and asks the user for one.
033 * <p>
034 * Incoming frames, the node number dialogue and the Set Node Number timeout
035 * are all handled on the GUI thread, which owns the allocation state.
036 * Once {@link #dispose()} has been called, frames and timeouts that were
037 * already queued for the GUI thread are ignored.
038 */
039public class CbusAllocateNodeNumber implements CanListener {
040
041    private final CbusNodeTableDataModel nodeModel;
042    private final CanSystemConnectionMemo _memo;
043    private final CbusSend send;
044
045    private JLabel rqNNtext;
046    private int baseNodeNum;
047    private boolean waitingForParams;
048    private volatile boolean dialogueOpen; // read by outgoing message thread
049    private boolean waitingForName;
050    private int[] _paramsArr;
051    private String _tempNodeName;
052    private JLabel rqNNspinnerlabel;
053    private int _timeout;
054    private volatile boolean disposed;
055
056    public CbusAllocateNodeNumber(CanSystemConnectionMemo memo, CbusNodeTableDataModel model) {
057
058        nodeModel = model;
059        // connect to the CanInterface
060        _memo = memo;
061        addTc(memo);
062        send = new CbusSend(memo);
063
064        baseNodeNum = 256;
065        _paramsArr = null;
066        waitingForParams = false;
067        dialogueOpen = false;
068        waitingForName = false;
069        _timeout = CbusNodeTimerManager.SINGLE_MESSAGE_TIMEOUT_TIME;
070        _tempNodeName="";
071    }
072
073
074    /**
075     *
076     * @param nn -1 if already in setup from unknown, 0 if entering from SLiM,
077     * else previous node number
078     * @param nodeText
079     */
080    private void startNodeAllocation(int nn, String nodeText) {
081
082        if (dialogueOpen) {
083            return;
084        }
085
086        dialogueOpen=true;
087        _tempNodeName="";
088
089        JPanel rqNNpane = new JPanel();
090        JPanel bottomrqNNpane = new JPanel();
091        rqNNspinnerlabel = new JLabel(Bundle.getMessage("NdRqNnSelect"));
092
093        bottomrqNNpane.setLayout(new GridLayout(2, 1));
094        rqNNpane.setLayout(new BorderLayout());
095        rqNNtext = new JLabel(Bundle.getMessage("NdRqNdDetails"));
096
097        String popuplabel;
098
099        baseNodeNum =  nodeModel.getNextAvailableNodeNumber(baseNodeNum);
100
101        switch (nn) {
102            case 0:
103                popuplabel=Bundle.getMessage("NdEntrSlimTitle");
104                _paramsArr = null; // reset just in case
105                break;
106            case -1:
107                popuplabel="Node found in Setup Mode";
108                // not resetting _paramsArr as may be set from found in setup
109                if ( nodeText != null ) {
110                    rqNNtext.setText(nodeText);
111                }
112                break;
113            default:
114                popuplabel=Bundle.getMessage("NdEntrNumTitle",String.valueOf(nn));
115                _paramsArr = null; // reset just in case
116                baseNodeNum = nn;
117                break;
118        }
119
120        JSpinner rqnnSpinner = getNewRqnnSpinner();
121        rqnnSpinner.firePropertyChange("open", false, true); // reset node text
122        rqNNpane.add(rqNNtext, BorderLayout.CENTER);
123        bottomrqNNpane.add(rqNNspinnerlabel);
124        bottomrqNNpane.add(rqnnSpinner);
125
126        rqNNpane.add(bottomrqNNpane, BorderLayout.PAGE_END);
127
128        rqNNpane.setPreferredSize( new java.awt.Dimension(280, 80));
129
130        Toolkit.getDefaultToolkit().beep();
131
132        if ( _paramsArr==null ) {
133            waitingForParams=true;
134            send.nodeRequestParamSetup();
135        }
136
137        int option = JmriJOptionPane.showConfirmDialog(null,
138            rqNNpane,
139            popuplabel,
140            JmriJOptionPane.OK_CANCEL_OPTION);
141
142        // the dialogue is modal, dispose() may have been called while it was open
143        if (option == JmriJOptionPane.OK_OPTION && disposed) {
144            log.debug("Node Number not sent, node number allocation has been disposed");
145        }
146        if (option == JmriJOptionPane.OK_OPTION && !disposed) {
147            int newval = (Integer) rqnnSpinner.getValue();
148            baseNodeNum = newval;
149            setSendSNNTimeout();
150            send.nodeSetNodeNumber(newval);
151        } else {
152            // cancelled or disposed, discard any parameters / name received from the node in setup
153            // so they cannot be applied to a different node later.
154            _paramsArr = null;
155            _tempNodeName = "";
156            waitingForName = false;
157        }
158        dialogueOpen=false;
159        waitingForParams=false;
160    }
161
162    private JSpinner getNewRqnnSpinner() {
163
164        JSpinner rqnnSpinner = new JSpinner(new SpinnerNumberModel(baseNodeNum, 1, 65535, 1));
165        rqnnSpinner.setToolTipText((Bundle.getMessage("ToolTipNodeNumber")));
166
167        JSpinner.NumberEditor editor = new JSpinner.NumberEditor(rqnnSpinner, "#");
168        rqnnSpinner.setEditor(editor);
169        JSpinnerUtil.setCommitsOnValidEdit(rqnnSpinner, true);
170
171        JFormattedTextField rqfield = (JFormattedTextField) editor.getComponent(0);
172        rqfield.setBackground(Color.white);
173        rqnnSpinner.addChangeListener((ChangeEvent e) -> {
174            int newval = (Integer) rqnnSpinner.getValue();
175
176            if (!CbusNodeConstants.getReservedModule(newval).isEmpty()) {
177                rqNNspinnerlabel.setText(CbusNodeConstants.getReservedModule(newval));
178                rqfield.setBackground(Color.yellow);
179            }
180            else {
181                rqNNspinnerlabel.setText(Bundle.getMessage("NdRqNnSelect"));
182                rqfield.setBackground(Color.white);
183            }
184            if ( !nodeModel.getNodeNumberName(newval).isEmpty() ) {
185                rqNNspinnerlabel.setText(Bundle.getMessage("NdNumInUse",nodeModel.getNodeNumberName(newval)));
186                rqfield.setBackground(Color.red);
187            }
188        });
189        return rqnnSpinner;
190    }
191
192    private volatile TimerTask sendSNNTask; // cancelled by dispose() on any thread
193
194    private void clearSendSNNTimeout(){
195        TimerTask task = sendSNNTask;
196        sendSNNTask = null;
197        if ( task != null ) {
198            task.cancel();
199        }
200    }
201
202    private void setSendSNNTimeout() {
203        clearSendSNNTimeout();
204        TimerTask task = new TimerTask() {
205            @Override
206            public void run() {
207                // hand over to the GUI thread, which owns the allocation state, so the
208                // shared timer thread is not held while the error dialogue is open
209                ThreadingUtil.runOnGUIEventually( () -> sendSNNTimedOut(this) );
210            }
211        };
212        sendSNNTask = task;
213        TimerUtil.schedule(task, _timeout);
214    }
215
216    /**
217     * Report that the Node did not confirm the Set Node Number.
218     * @param task the timer that expired. Ignored if it has since been
219     * cleared by a Node Number Acknowledge, replaced, or disposed.
220     */
221    @InvokeOnGuiThread
222    private void sendSNNTimedOut(TimerTask task) {
223        if ( disposed || task != sendSNNTask ) {
224            return;
225        }
226        sendSNNTask = null;
227        log.error("No confirmation from node when setting node number {}", baseNodeNum );
228        JmriJOptionPane.showMessageDialog(null,
229            Bundle.getMessage("NnAllocError",baseNodeNum), Bundle.getMessage("WarningTitle"),
230            JmriJOptionPane.ERROR_MESSAGE);
231    }
232
233    /**
234     * Set the SNN timeout, for Testing purposes
235     * @param newVal Timeout value in ms
236     */
237    protected void setTimeout( int newVal){
238        _timeout = newVal;
239    }
240
241    /**
242     * If popup not open send a setup param request to try and catch nodes awaiting number allocation
243     * when an all node respond message is sent.
244     * @param m Outgoing CanMessage
245     */
246    @Override
247    public void message(CanMessage m) { // outgoing cbus message
248        if ( disposed || m.extendedOrRtr() ) {
249            return;
250        }
251        if (CbusMessage.getOpcode(m) == CbusConstants.CBUS_QNN && !dialogueOpen ) {
252            send.nodeRequestParamSetup();
253        }
254    }
255
256    /**
257     * Capture CBUS_RQNN, CBUS_PARAMS, CBUS_NNACK, CBUS_NAME
258     * @param m incoming CanReply
259     */
260    @Override
261    public void reply(CanReply m) {
262        if ( disposed || m.extendedOrRtr() ) {
263            return;
264        }
265        // run on GUI not Layout thread as pretty much all of this is GUI based.
266        // and could be awaiting from response from JDialog.
267        ThreadingUtil.runOnGUIEventually( ()->{
268            processAllocateFrame(m);
269        });
270    }
271
272    private void processAllocateFrame(CanReply m){
273        if ( disposed ) { // frame was queued before dispose
274            return;
275        }
276        switch (CbusMessage.getOpcode(m)) {
277            case CbusConstants.CBUS_RQNN:
278                // node requesting a number, nn is existing number
279                startNodeAllocation( ( m.getElement(1) * 256 ) + m.getElement(2), null );
280                break;
281            case CbusConstants.CBUS_PARAMS:
282                processNodeParams(m);
283                break;
284            case CbusConstants.CBUS_NNACK: // node number acknowledge
285                processNodeNumberAck(m);
286                break;
287            case CbusConstants.CBUS_NAME:
288                processNodeName(m);
289                break;
290            default:
291                break;
292        }
293    }
294
295    /**
296     * Process a Node Number Acknowledge.
297     * <p>
298     * A Node sends NNACK in response to a SNN ( Set Node Number ),
299     * and also in response to an ENUM ( CAN ID self enumeration ) request.
300     * <p>
301     * Only when the NNACK is in response to a SNN sent from here, ie. the SNN timer
302     * is running and the Node Number matches, are the parameters and name obtained
303     * from the Node in setup applied and the node reset for a full re-fetch.
304     * <p>
305     * An NNACK for a node not in the node table, eg. a Node Number set by other software,
306     * adds the node to the table if enabled in preferences.
307     * An NNACK from a node already in the table is otherwise ignored here,
308     * the node CAN listener handles the ENUM response.
309     *
310     * @param m the NNACK CanReply
311     */
312    private void processNodeNumberAck(CanReply m) {
313        int nodeNum = m.getElement(1) * 256 + m.getElement(2);
314        boolean snnAcknowledged = ( sendSNNTask != null && nodeNum == baseNodeNum );
315        if ( snnAcknowledged ) {
316            clearSendSNNTimeout();
317        }
318        // if nodes are allowed to be added to node table, add.
319        // this is done here so any known parameters can be passed directly rather than re-requested
320        if ( _memo.get(CbusPreferences.class).getAddNodes() ) {
321            if ( snnAcknowledged ) {
322                nodeModel.setRequestNodeDisplay(nodeNum);
323                // provide will add to table
324                CbusNode nd = nodeModel.provideNodeByNodeNum( nodeNum );
325                nd.getCanListener().setParamsFromSetup(_paramsArr);
326                nd.setNodeNameFromName(_tempNodeName);
327                nd.resetNodeAll();
328                nodeModel.startUrgentFetch();
329                nodeModel.setRequestNodeDisplay(-1);
330                send.searchForCommandStations();
331            } else if ( nodeModel.getNodeByNodeNum( nodeNum ) == null ) {
332                // node number not set by us, eg. by other software, add to table.
333                // setup parameters are not applied as they may be from a different node.
334                log.debug("NNACK from node {} not in table, adding", nodeNum);
335                nodeModel.provideNodeByNodeNum( nodeNum );
336                nodeModel.startUrgentFetch();
337                send.searchForCommandStations();
338            } else {
339                log.debug("NNACK from existing node {}, not in response to SNN, ignored", nodeNum);
340            }
341        }
342        if ( snnAcknowledged ) {
343            _paramsArr = null;
344            _tempNodeName = "";
345        }
346    }
347
348    private void processNodeParams(CanReply m) {
349        _paramsArr = new int[] { m.getElement(1),m.getElement(2),
350            m.getElement(3),m.getElement(4), m.getElement(5),
351            m.getElement(6),m.getElement(7) };
352
353        StringBuilder nodepropbuilder = new StringBuilder(40);
354        nodepropbuilder.append (CbusNodeConstants.getManu( _paramsArr[0] ));
355        nodepropbuilder.append (" ");
356        nodepropbuilder.append( CbusNodeConstants.getModuleType( _paramsArr[0] , _paramsArr[2] ));
357
358        if (waitingForParams) {
359            rqNNtext.setText(nodepropbuilder.toString());
360            waitingForParams=false;
361        }
362        else if (!dialogueOpen) {
363            startNodeAllocation( -1, nodepropbuilder.toString() );
364        }
365
366        if ( CbusNodeConstants.getModuleType( _paramsArr[0] , _paramsArr[2] ).isEmpty() ) {
367            waitingForName = true;
368            send.rQmn(); // request node type name if not recognised
369        }
370    }
371
372    private void processNodeName(CanReply m){
373        if (waitingForName) {
374            waitingForName = false;
375            StringBuilder rval = new StringBuilder(10);
376            rval.append("CAN");
377            rval.append(String.format("%c", (char) m.getElement(1) ));
378            rval.append(String.format("%c", (char) m.getElement(2) ));
379            rval.append(String.format("%c", (char) m.getElement(3) ));
380            rval.append(String.format("%c", (char) m.getElement(4) ));
381            rval.append(String.format("%c", (char) m.getElement(5) ));
382            rval.append(String.format("%c", (char) m.getElement(6) ));
383            rval.append(String.format("%c", (char) m.getElement(7) ));
384            _tempNodeName = rval.toString().trim();
385
386            StringBuilder nodepropbuilder = new StringBuilder(40);
387            if ( _paramsArr != null ) {
388                nodepropbuilder.append (CbusNodeConstants.getManu( _paramsArr[0] ));
389                nodepropbuilder.append (" ");
390            }
391            nodepropbuilder.append (_tempNodeName);
392
393            if ( rqNNtext != null ) {
394                rqNNtext.setText(nodepropbuilder.toString());
395            }
396        }
397    }
398
399    /**
400     * Stop listening and cancel any Set Node Number timeout.
401     * Frames and timeouts already queued for the GUI thread are then ignored.
402     */
403    @InvokeOnAnyThread
404    public void dispose(){
405        disposed = true;
406        removeTc(_memo);
407        clearSendSNNTimeout();
408    }
409
410    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CbusAllocateNodeNumber.class);
411
412}