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.jmrix.can.CanListener;
017import jmri.jmrix.can.CanMessage;
018import jmri.jmrix.can.CanSystemConnectionMemo;
019import jmri.jmrix.can.CanReply;
020import jmri.jmrix.can.cbus.CbusConstants;
021import jmri.jmrix.can.cbus.CbusMessage;
022import jmri.jmrix.can.cbus.CbusPreferences;
023import jmri.jmrix.can.cbus.CbusSend;
024import jmri.util.TimerUtil;
025import jmri.util.swing.JmriJOptionPane;
026import jmri.util.swing.JSpinnerUtil;
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        rqNNpane.setPreferredSize( new java.awt.Dimension(280, 80));
117
118        Toolkit.getDefaultToolkit().beep();
119
120        if ( _paramsArr==null ) {
121            WAITINGRESPONSE_RQNN_PARAMS=true;
122            send.nodeRequestParamSetup();
123        }
124
125        int option = JmriJOptionPane.showConfirmDialog(null,
126            rqNNpane,
127            popuplabel,
128            JmriJOptionPane.OK_CANCEL_OPTION);
129
130        if (option == JmriJOptionPane.OK_OPTION) {
131            int newval = (Integer) rqnnSpinner.getValue();
132            baseNodeNum = newval;
133            setSendSNNTimeout();
134            send.nodeSetNodeNumber(newval);
135        }
136        NODE_NUM_DIALOGUE_OPEN=false;
137        WAITINGRESPONSE_RQNN_PARAMS=false;
138    }
139
140    private JSpinner getNewRqnnSpinner() {
141
142        JSpinner rqnnSpinner = new JSpinner(new SpinnerNumberModel(baseNodeNum, 1, 65535, 1));
143        rqnnSpinner.setToolTipText((Bundle.getMessage("ToolTipNodeNumber")));
144
145        JSpinner.NumberEditor editor = new JSpinner.NumberEditor(rqnnSpinner, "#");
146        rqnnSpinner.setEditor(editor);
147        JSpinnerUtil.setCommitsOnValidEdit(rqnnSpinner, true);
148
149        JFormattedTextField rqfield = (JFormattedTextField) editor.getComponent(0);
150        rqfield.setBackground(Color.white);
151        rqnnSpinner.addChangeListener((ChangeEvent e) -> {
152            int newval = (Integer) rqnnSpinner.getValue();
153
154            if (!CbusNodeConstants.getReservedModule(newval).isEmpty()) {
155                rqNNspinnerlabel.setText(CbusNodeConstants.getReservedModule(newval));
156                rqfield.setBackground(Color.yellow);
157            }
158            else {
159                rqNNspinnerlabel.setText(Bundle.getMessage("NdRqNnSelect"));
160                rqfield.setBackground(Color.white);
161            }
162            if ( !nodeModel.getNodeNumberName(newval).isEmpty() ) {
163                rqNNspinnerlabel.setText(Bundle.getMessage("NdNumInUse",nodeModel.getNodeNumberName(newval)));
164                rqfield.setBackground(Color.red);
165            }
166        });
167        return rqnnSpinner;
168    }
169
170    private TimerTask sendSNNTask;
171
172    private void clearSendSNNTimeout(){
173        if (sendSNNTask != null ) {
174            sendSNNTask.cancel();
175            sendSNNTask = null;
176        }
177    }
178
179    private void setSendSNNTimeout() {
180        sendSNNTask = new TimerTask() {
181            @Override
182            public void run() {
183                sendSNNTask = null;
184                log.error("No confirmation from node when setting node number {}", baseNodeNum );
185                JmriJOptionPane.showMessageDialog(null,
186                    Bundle.getMessage("NnAllocError",baseNodeNum), Bundle.getMessage("WarningTitle"),
187                    JmriJOptionPane.ERROR_MESSAGE);
188                clearSendSNNTimeout();
189            }
190        };
191        TimerUtil.schedule(sendSNNTask, _timeout);
192    }
193
194    /**
195     * Set the SNN timeout, for Testing purposes
196     * @param newVal Timeout value in ms
197     */
198    protected void setTimeout( int newVal){
199        _timeout = newVal;
200    }
201
202    /**
203     * If popup not open send a setup param request to try and catch nodes awaiting number allocation
204     * when an all node respond message is sent.
205     * @param m Outgoing CanMessage
206     */
207    @Override
208    public void message(CanMessage m) { // outgoing cbus message
209        if ( m.extendedOrRtr() ) {
210            return;
211        }
212        if (CbusMessage.getOpcode(m) == CbusConstants.CBUS_QNN && !NODE_NUM_DIALOGUE_OPEN ) {
213            send.nodeRequestParamSetup();
214        }
215    }
216
217    /**
218     * Capture CBUS_RQNN, CBUS_PARAMS, CBUS_NNACK, CBUS_NAME
219     * @param m incoming CanReply
220     */
221    @Override
222    public void reply(CanReply m) {
223        if ( m.extendedOrRtr() ) {
224            return;
225        }
226        // run on GUI not Layout thread as pretty much all of this is GUI based.
227        // and could be awaiting from response from JDialog.
228        jmri.util.ThreadingUtil.runOnGUIEventually( ()->{
229            processAllocateFrame(m);
230        });
231    }
232
233    private void processAllocateFrame(CanReply m){
234        switch (CbusMessage.getOpcode(m)) {
235            case CbusConstants.CBUS_RQNN:
236                // node requesting a number, nn is existing number
237                startnodeallocation( ( m.getElement(1) * 256 ) + m.getElement(2), null );
238                break;
239            case CbusConstants.CBUS_PARAMS:
240                processNodeParams(m);
241                break;
242            case CbusConstants.CBUS_NNACK: // node number acknowledge
243                clearSendSNNTimeout();
244                // if nodes are allowed to be added to node table, add.
245                // this is done here so any known parameters can be passed directly rather than re-requested
246                if ( _memo.get(CbusPreferences.class).getAddNodes() ) {
247                    int nodeNum = m.getElement(1) * 256 + m.getElement(2);
248                    nodeModel.setRequestNodeDisplay(nodeNum);
249                    // provide will add to table
250                    CbusNode nd = nodeModel.provideNodeByNodeNum( nodeNum );
251                    nd.getCanListener().setParamsFromSetup(_paramsArr);
252                    nd.setNodeNameFromName(_tempNodeName);
253                    nd.resetNodeAll();
254                    nodeModel.startUrgentFetch();
255                    nodeModel.setRequestNodeDisplay(-1);
256                    send.searchForCommandStations();
257                }
258                _paramsArr = null;
259                break;
260            case CbusConstants.CBUS_NAME:
261                processNodeName(m);
262                break;
263            default:
264                break;
265        }
266    }
267
268    private void processNodeParams(CanReply m) {
269        _paramsArr = new int[] { m.getElement(1),m.getElement(2),
270            m.getElement(3),m.getElement(4), m.getElement(5),
271            m.getElement(6),m.getElement(7) };
272
273        StringBuilder nodepropbuilder = new StringBuilder(40);
274        nodepropbuilder.append (CbusNodeConstants.getManu( _paramsArr[0] ));
275        nodepropbuilder.append (" ");
276        nodepropbuilder.append( CbusNodeConstants.getModuleType( _paramsArr[0] , _paramsArr[2] ));
277
278        if (WAITINGRESPONSE_RQNN_PARAMS) {
279            rqNNtext.setText(nodepropbuilder.toString());
280            WAITINGRESPONSE_RQNN_PARAMS=false;
281        }
282        else if (!NODE_NUM_DIALOGUE_OPEN) {
283            startnodeallocation( -1, nodepropbuilder.toString() );
284        }
285
286        if ( CbusNodeConstants.getModuleType( _paramsArr[0] , _paramsArr[2] ).isEmpty() ) {
287            WAITING_RESPONSE_NAME = true;
288            send.rQmn(); // request node type name if not recognised
289        }
290    }
291
292    private void processNodeName(CanReply m){
293        if (WAITING_RESPONSE_NAME) {
294            WAITING_RESPONSE_NAME = false;
295            StringBuilder rval = new StringBuilder(10);
296            rval.append("CAN");
297            rval.append(String.format("%c", (char) m.getElement(1) ));
298            rval.append(String.format("%c", (char) m.getElement(2) ));
299            rval.append(String.format("%c", (char) m.getElement(3) ));
300            rval.append(String.format("%c", (char) m.getElement(4) ));
301            rval.append(String.format("%c", (char) m.getElement(5) ));
302            rval.append(String.format("%c", (char) m.getElement(6) ));
303            rval.append(String.format("%c", (char) m.getElement(7) ));
304            _tempNodeName = rval.toString().trim();
305
306            StringBuilder nodepropbuilder = new StringBuilder(40);
307            nodepropbuilder.append (CbusNodeConstants.getManu( _paramsArr[0] ));
308            nodepropbuilder.append (" ");
309            nodepropbuilder.append (_tempNodeName);
310
311            rqNNtext.setText(nodepropbuilder.toString());
312        }
313    }
314
315    public void dispose(){
316        clearSendSNNTimeout();
317        removeTc(_memo);
318    }
319
320    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CbusAllocateNodeNumber.class);
321
322}