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;
015import javax.swing.text.DefaultFormatter;
016
017import jmri.jmrix.can.CanListener;
018import jmri.jmrix.can.CanMessage;
019import jmri.jmrix.can.CanSystemConnectionMemo;
020import jmri.jmrix.can.CanReply;
021import jmri.jmrix.can.cbus.CbusConstants;
022import jmri.jmrix.can.cbus.CbusMessage;
023import jmri.jmrix.can.cbus.CbusPreferences;
024import jmri.jmrix.can.cbus.CbusSend;
025import jmri.util.TimerUtil;
026import jmri.util.swing.JmriJOptionPane;
027
028public class CbusAllocateNodeNumber implements CanListener {
029
030    private final CbusNodeTableDataModel nodeModel;
031    private final CanSystemConnectionMemo _memo;
032    private final CbusSend send;
033
034    private JLabel rqNNtext;
035    private int baseNodeNum;
036    private boolean WAITINGRESPONSE_RQNN_PARAMS;
037    private boolean NODE_NUM_DIALOGUE_OPEN;
038    private boolean WAITING_RESPONSE_NAME;
039    private int[] _paramsArr;
040    private String _tempNodeName;
041    private JLabel rqNNspinnerlabel;
042    private int _timeout;
043
044    public CbusAllocateNodeNumber(CanSystemConnectionMemo memo, CbusNodeTableDataModel model) {
045
046        nodeModel = model;
047        // connect to the CanInterface
048        _memo = memo;
049        addTc(memo);
050        send = new CbusSend(memo);
051
052        baseNodeNum = 256;
053        _paramsArr = null;
054        WAITINGRESPONSE_RQNN_PARAMS = false;
055        NODE_NUM_DIALOGUE_OPEN = false;
056        WAITING_RESPONSE_NAME = false;
057        _timeout = CbusNodeTimerManager.SINGLE_MESSAGE_TIMEOUT_TIME;
058        _tempNodeName="";
059    }
060
061
062    /**
063     *
064     * @param nn -1 if already in setup from unknown, 0 if entering from SLiM,
065     * else previous node number
066     * @param nodeText
067     */
068    private void startnodeallocation(int nn, String nodeText) {
069
070        if (NODE_NUM_DIALOGUE_OPEN) {
071            return;
072        }
073
074        NODE_NUM_DIALOGUE_OPEN=true;
075        _tempNodeName="";
076
077        JPanel rqNNpane = new JPanel();
078        JPanel bottomrqNNpane = new JPanel();
079        rqNNspinnerlabel = new JLabel(Bundle.getMessage("NdRqNnSelect"));
080
081        bottomrqNNpane.setLayout(new GridLayout(2, 1));
082        rqNNpane.setLayout(new BorderLayout());
083        rqNNtext = new JLabel(Bundle.getMessage("NdRqNdDetails"));
084
085        String popuplabel;
086
087        baseNodeNum =  nodeModel.getNextAvailableNodeNumber(baseNodeNum);
088
089        switch (nn) {
090            case 0:
091                popuplabel=Bundle.getMessage("NdEntrSlimTitle");
092                _paramsArr = null; // reset just in case
093                break;
094            case -1:
095                popuplabel="Node found in Setup Mode";
096                // not resetting _paramsArr as may be set from found in setup
097                if ( nodeText != null ) {
098                    rqNNtext.setText(nodeText);
099                }
100                break;
101            default:
102                popuplabel=Bundle.getMessage("NdEntrNumTitle",String.valueOf(nn));
103                _paramsArr = null; // reset just in case
104                baseNodeNum = nn;
105                break;
106        }
107
108        JSpinner rqnnSpinner = getNewRqnnSpinner();
109        rqnnSpinner.firePropertyChange("open", false, true); // reset node text
110        rqNNpane.add(rqNNtext, BorderLayout.CENTER);
111        bottomrqNNpane.add(rqNNspinnerlabel);
112        bottomrqNNpane.add(rqnnSpinner);
113
114        rqNNpane.add(bottomrqNNpane, BorderLayout.PAGE_END);
115
116        Toolkit.getDefaultToolkit().beep();
117
118        if ( _paramsArr==null ) {
119            WAITINGRESPONSE_RQNN_PARAMS=true;
120            send.nodeRequestParamSetup();
121        }
122
123        int option = JmriJOptionPane.showConfirmDialog(null,
124            rqNNpane,
125            popuplabel,
126            JmriJOptionPane.OK_CANCEL_OPTION);
127
128        if (option == JmriJOptionPane.OK_OPTION) {
129            int newval = (Integer) rqnnSpinner.getValue();
130            baseNodeNum = newval;
131            setSendSNNTimeout();
132            send.nodeSetNodeNumber(newval);
133        }
134        NODE_NUM_DIALOGUE_OPEN=false;
135        WAITINGRESPONSE_RQNN_PARAMS=false;
136    }
137
138    private JSpinner getNewRqnnSpinner() {
139
140        JSpinner rqnnSpinner = new JSpinner(new SpinnerNumberModel(baseNodeNum, 1, 65535, 1));
141        rqnnSpinner.setToolTipText((Bundle.getMessage("ToolTipNodeNumber")));
142
143        JSpinner.NumberEditor editor = new JSpinner.NumberEditor(rqnnSpinner, "#");
144        rqnnSpinner.setEditor(editor);
145
146        JFormattedTextField rqfield = (JFormattedTextField) editor.getComponent(0);
147        DefaultFormatter rqformatter = (DefaultFormatter) rqfield.getFormatter();
148        rqformatter.setCommitsOnValidEdit(true);
149        rqfield.setBackground(Color.white);
150        rqnnSpinner.addChangeListener((ChangeEvent e) -> {
151            int newval = (Integer) rqnnSpinner.getValue();
152
153            if (!CbusNodeConstants.getReservedModule(newval).isEmpty()) {
154                rqNNspinnerlabel.setText(CbusNodeConstants.getReservedModule(newval));
155                rqfield.setBackground(Color.yellow);
156            }
157            else {
158                rqNNspinnerlabel.setText(Bundle.getMessage("NdRqNnSelect"));
159                rqfield.setBackground(Color.white);
160            }
161            if ( !nodeModel.getNodeNumberName(newval).isEmpty() ) {
162                rqNNspinnerlabel.setText(Bundle.getMessage("NdNumInUse",nodeModel.getNodeNumberName(newval)));
163                rqfield.setBackground(Color.red);
164            }
165        });
166        return rqnnSpinner;
167    }
168
169    protected TimerTask sendSNNTask;
170
171    private void clearSendSNNTimeout(){
172        if (sendSNNTask != null ) {
173            sendSNNTask.cancel();
174            sendSNNTask = null;
175        }
176    }
177
178    private void setSendSNNTimeout() {
179        sendSNNTask = new TimerTask() {
180            @Override
181            public void run() {
182                sendSNNTask = null;
183                log.error("No confirmation from node when setting node number {}", baseNodeNum );
184                JmriJOptionPane.showMessageDialog(null,
185                    Bundle.getMessage("NnAllocError",baseNodeNum), Bundle.getMessage("WarningTitle"),
186                    JmriJOptionPane.ERROR_MESSAGE);
187                clearSendSNNTimeout();
188            }
189        };
190        TimerUtil.schedule(sendSNNTask, _timeout);
191    }
192
193    /**
194     * Set the SNN timeout, for Testing purposes
195     * @param newVal Timeout value in ms
196     */
197    protected void setTimeout( int newVal){
198        _timeout = newVal;
199    }
200
201    /**
202     * If popup not open send a setup param request to try and catch nodes awaiting number allocation
203     * when an all node respond message is sent.
204     * @param m Outgoing CanMessage
205     */
206    @Override
207    public void message(CanMessage m) { // outgoing cbus message
208        if ( m.extendedOrRtr() ) {
209            return;
210        }
211        if (CbusMessage.getOpcode(m) == CbusConstants.CBUS_QNN) {
212            if (!NODE_NUM_DIALOGUE_OPEN) {
213                send.nodeRequestParamSetup();
214            }
215        }
216    }
217
218    /**
219     * Capture CBUS_RQNN, CBUS_PARAMS, CBUS_NNACK, CBUS_NAME
220     * @param m incoming CanReply
221     */
222    @Override
223    public void reply(CanReply m) {
224        if ( m.extendedOrRtr() ) {
225            return;
226        }
227        // run on GUI not Layout thread as pretty much all of this is GUI based.
228        // and could be awaiting from response from JDialog.
229        jmri.util.ThreadingUtil.runOnGUIEventually( ()->{
230            processAllocateFrame(m);
231        });
232    }
233
234    private void processAllocateFrame(CanReply m){
235        switch (CbusMessage.getOpcode(m)) {
236            case CbusConstants.CBUS_RQNN:
237                // node requesting a number, nn is existing number
238                startnodeallocation( ( m.getElement(1) * 256 ) + m.getElement(2), null );
239                break;
240            case CbusConstants.CBUS_PARAMS:
241                processNodeParams(m);
242                break;
243            case CbusConstants.CBUS_NNACK: // node number acknowledge
244                clearSendSNNTimeout();
245                // if nodes are allowed to be added to node table, add.
246                // this is done here so any known parameters can be passed directly rather than re-requested
247                if ( _memo.get(CbusPreferences.class).getAddNodes() ) {
248                    int nodeNum = m.getElement(1) * 256 + m.getElement(2);
249                    nodeModel.setRequestNodeDisplay(nodeNum);
250                    // provide will add to table
251                    CbusNode nd = nodeModel.provideNodeByNodeNum( nodeNum );
252                    nd.getCanListener().setParamsFromSetup(_paramsArr);
253                    nd.setNodeNameFromName(_tempNodeName);
254                    nd.resetNodeAll();
255                    nodeModel.startUrgentFetch();
256                    nodeModel.setRequestNodeDisplay(-1);
257                    send.searchForCommandStations();
258                }
259                _paramsArr = null;
260                break;
261            case CbusConstants.CBUS_NAME:
262                processNodeName(m);
263                break;
264            default:
265                break;
266        }
267    }
268
269    private void processNodeParams(CanReply m) {
270        _paramsArr = new int[] { m.getElement(1),m.getElement(2),
271            m.getElement(3),m.getElement(4), m.getElement(5),
272            m.getElement(6),m.getElement(7) };
273
274        StringBuilder nodepropbuilder = new StringBuilder(40);
275        nodepropbuilder.append (CbusNodeConstants.getManu( _paramsArr[0] ));
276        nodepropbuilder.append (" ");
277        nodepropbuilder.append( CbusNodeConstants.getModuleType( _paramsArr[0] , _paramsArr[2] ));
278
279        if (WAITINGRESPONSE_RQNN_PARAMS) {
280            rqNNtext.setText(nodepropbuilder.toString());
281            WAITINGRESPONSE_RQNN_PARAMS=false;
282        }
283        else if (!NODE_NUM_DIALOGUE_OPEN) {
284            startnodeallocation( -1, nodepropbuilder.toString() );
285        }
286
287        if ( CbusNodeConstants.getModuleType( _paramsArr[0] , _paramsArr[2] ).isEmpty() ) {
288            WAITING_RESPONSE_NAME = true;
289            send.rQmn(); // request node type name if not recognised
290        }
291    }
292
293    private void processNodeName(CanReply m){
294        if (WAITING_RESPONSE_NAME) {
295            WAITING_RESPONSE_NAME = false;
296            StringBuilder rval = new StringBuilder(10);
297            rval.append("CAN");
298            rval.append(String.format("%c", (char) m.getElement(1) ));
299            rval.append(String.format("%c", (char) m.getElement(2) ));
300            rval.append(String.format("%c", (char) m.getElement(3) ));
301            rval.append(String.format("%c", (char) m.getElement(4) ));
302            rval.append(String.format("%c", (char) m.getElement(5) ));
303            rval.append(String.format("%c", (char) m.getElement(6) ));
304            rval.append(String.format("%c", (char) m.getElement(7) ));
305            _tempNodeName = rval.toString().trim();
306
307            StringBuilder nodepropbuilder = new StringBuilder(40);
308            nodepropbuilder.append (CbusNodeConstants.getManu( _paramsArr[0] ));
309            nodepropbuilder.append (" ");
310            nodepropbuilder.append (_tempNodeName);
311
312            rqNNtext.setText(nodepropbuilder.toString());
313        }
314    }
315
316    public void dispose(){
317        clearSendSNNTimeout();
318        removeTc(_memo);
319    }
320
321    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CbusAllocateNodeNumber.class);
322
323}