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}