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        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
148        JFormattedTextField rqfield = (JFormattedTextField) editor.getComponent(0);
149        DefaultFormatter rqformatter = (DefaultFormatter) rqfield.getFormatter();
150        rqformatter.setCommitsOnValidEdit(true);
151        rqfield.setBackground(Color.white);
152        rqnnSpinner.addChangeListener((ChangeEvent e) -> {
153            int newval = (Integer) rqnnSpinner.getValue();
154
155            if (!CbusNodeConstants.getReservedModule(newval).isEmpty()) {
156                rqNNspinnerlabel.setText(CbusNodeConstants.getReservedModule(newval));
157                rqfield.setBackground(Color.yellow);
158            }
159            else {
160                rqNNspinnerlabel.setText(Bundle.getMessage("NdRqNnSelect"));
161                rqfield.setBackground(Color.white);
162            }
163            if ( !nodeModel.getNodeNumberName(newval).isEmpty() ) {
164                rqNNspinnerlabel.setText(Bundle.getMessage("NdNumInUse",nodeModel.getNodeNumberName(newval)));
165                rqfield.setBackground(Color.red);
166            }
167        });
168        return rqnnSpinner;
169    }
170
171    private TimerTask sendSNNTask;
172
173    private void clearSendSNNTimeout(){
174        if (sendSNNTask != null ) {
175            sendSNNTask.cancel();
176            sendSNNTask = null;
177        }
178    }
179
180    private void setSendSNNTimeout() {
181        sendSNNTask = new TimerTask() {
182            @Override
183            public void run() {
184                sendSNNTask = null;
185                log.error("No confirmation from node when setting node number {}", baseNodeNum );
186                JmriJOptionPane.showMessageDialog(null,
187                    Bundle.getMessage("NnAllocError",baseNodeNum), Bundle.getMessage("WarningTitle"),
188                    JmriJOptionPane.ERROR_MESSAGE);
189                clearSendSNNTimeout();
190            }
191        };
192        TimerUtil.schedule(sendSNNTask, _timeout);
193    }
194
195    /**
196     * Set the SNN timeout, for Testing purposes
197     * @param newVal Timeout value in ms
198     */
199    protected void setTimeout( int newVal){
200        _timeout = newVal;
201    }
202
203    /**
204     * If popup not open send a setup param request to try and catch nodes awaiting number allocation
205     * when an all node respond message is sent.
206     * @param m Outgoing CanMessage
207     */
208    @Override
209    public void message(CanMessage m) { // outgoing cbus message
210        if ( m.extendedOrRtr() ) {
211            return;
212        }
213        if (CbusMessage.getOpcode(m) == CbusConstants.CBUS_QNN && !NODE_NUM_DIALOGUE_OPEN ) {
214            send.nodeRequestParamSetup();
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}