001package jmri.jmrix.can.cbus.swing.modules.base; 002 003import static jmri.jmrix.can.cbus.node.CbusNodeNVTableDataModel.NV_SELECT_COLUMN; 004 005import java.awt.GridBagConstraints; 006import java.awt.GridBagLayout; 007import java.awt.event.ActionEvent; 008 009import javax.swing.*; 010import javax.swing.border.*; 011import javax.swing.event.TableModelEvent; 012 013import org.slf4j.Logger; 014import org.slf4j.LoggerFactory; 015 016import jmri.jmrix.can.cbus.node.CbusNode; 017import jmri.jmrix.can.cbus.node.CbusNodeNVTableDataModel; 018import jmri.jmrix.can.cbus.swing.modules.*; 019 020/** 021 * Node Variable edit frame for a basic 8 channel servo module. 022 * 023 * NVs can be written in "real time" as the user interacts with the GUI. 024 * This allows the servo positions to be observed during setup. 025 * CBUS Servo modules behave differently in that they need to be in learn mode 026 * to write NVs. 027 * The NVs will be stored by the module when it is taken out of learn mode. 028 * The entry/exit to/from learn mode is handled by the call to CbusSend.nVSET 029 * 030 * @author Andrew Crosland Copyright (C) 2021 031 */ 032public class Servo8BaseEditNVPane extends AbstractEditNVPane { 033 034 // Number of outputs 035 public static final int OUTPUTS = 8; 036 037 // Startup action 038 public static final int ACTION_OFF = 3; 039 public static final int ACTION_SAVED = 1; 040 public static final int ACTION_NONE = 0; 041 042 private ServoPane[] servo = new ServoPane[OUTPUTS+1]; 043 044 private final UpdateNV onPosUpdateFn = new UpdateOnPos(); 045 private final UpdateNV offPosUpdateFn = new UpdateOffPos(); 046 private final UpdateNV onSpdUpdateFn = new UpdateOnSpd(); 047 private final UpdateNV offSpdUpdateFn = new UpdateOffSpd(); 048 private final UpdateNV startupUpdateFn = new UpdateStartup(); 049 050 protected JButton save; 051 052 protected Servo8BaseEditNVPane(CbusNodeNVTableDataModel dataModel, CbusNode node) { 053 super(dataModel, node); 054 } 055 056 /** {@inheritDoc} */ 057 @Override 058 public AbstractEditNVPane getContent() { 059 060 JPanel gridPane = new JPanel(new GridBagLayout()); 061 GridBagConstraints c = new GridBagConstraints(); 062 c.fill = GridBagConstraints.HORIZONTAL; 063 c.weightx = 1; 064 c.weighty = 1; 065 c.gridx = 0; 066 c.gridy = 0; 067 068 // Two columns for the outputs 069 for (int y = 0; y < OUTPUTS/2; y++) { 070 c.gridx = 0; 071 for (int x = 0; x < 2; x++) { 072 int index = y*2 + x + 1; // NVs indexed from 1 073 servo[index] = new ServoPane(index); 074 gridPane.add(servo[index], c); 075 c.gridx++; 076 } 077 c.gridy++; 078 } 079 080 JScrollPane scroll = new JScrollPane(gridPane); 081 add(scroll); 082 083 return this; 084 } 085 086 /** {@inheritDoc} */ 087 @Override 088 public void tableChanged(TableModelEvent e) { 089// log.debug("servo gui table changed"); 090 if (e.getType() == TableModelEvent.UPDATE) { 091 int row = e.getFirstRow(); 092 int nv = row + 1; 093 int sv = (nv - Servo8BasePaneProvider.OUT1_ON)/4 + 1; // Outout channel number for NV 5 - 36 094// int value = getSelectValue(nv); 095 int value; 096 try { 097 value = (int)_dataModel.getValueAt(row, NV_SELECT_COLUMN); 098 } catch (NullPointerException ex) { 099 // NVs are not available yet, e.g. during resync 100 // CBUS servo modules support "live update" od servo settings. 101 // We do not want to update sliders, etc., before the NV Array is available as doing so 102 // will trigger calls to the update Fns which will send NV writes with incorrect values. 103 // 104 return; 105 } 106 log.debug("servo gui table changed NV: {} Value: {}", nv, value); 107 if (nv == Servo8BasePaneProvider.CUTOFF) { 108 //log.debug("Update cutoff to {}", value); 109 for (int i = 1; i <= OUTPUTS; i++) { 110 servo[i].cutoff.setSelected((value & (1<<(i-1))) > 0); 111 } 112 } else if ((nv == Servo8BasePaneProvider.STARTUP_POS) || (nv == Servo8BasePaneProvider.STARTUP_MOVE)) { 113 //log.debug("Update startup action {}", value); 114 for (int i = 1; i <= OUTPUTS; i++) { 115 servo[i].action.setButtons(); 116 } 117 } else if (nv == Servo8BasePaneProvider.SEQUENCE) { 118 //log.debug("Update sequential to {}", value); 119 for (int i = 1; i <= OUTPUTS; i++) { 120 servo[i].seq.setSelected((value & (1<<(i-1))) > 0); 121 } 122 } else if (nv > Servo8BasePaneProvider.OUT8_OFF_SPD) { 123 // Not used (we don't display the "last posn" NV37 124 //log.debug("Update non-displayed NV {}", nv); 125 } else if (nv > 0) { 126 // Four NVs per output 127 if (((nv - Servo8BasePaneProvider.OUT1_ON) % 4) == 0) { 128 // ON position 129 //log.debug("Update ON pos NV {} output {} to {}", nv, sv, value); 130 servo[sv].onPosSlider.setValue(value); 131 } else if (((nv - Servo8BasePaneProvider.OUT1_OFF) % 4) == 0) { 132 // OFF position 133 //log.debug("Update OFF pos NV {} output {} to {}", nv, sv, value); 134 servo[sv].offPosSlider.setValue(value); 135 } else if (((nv - Servo8BasePaneProvider.OUT1_ON_SPD) % 4) == 0) { 136 // ON speed, this will trigger the spinner change listener to call updateOnSpd 137// log.debug("Update ON spd NV {} output {} to {}", nv, sv, value); 138 servo[sv].onSpdSpinner.setValue(value & 7); 139 } else { 140 // OFF speed, this will trigger the spinner change listener to call updateOffSpd 141// log.debug("Update OFF spd NV {} output {} to {}", nv, sv, value); 142 servo[sv].offSpdSpinner.setValue(value & 7); 143 } 144 } else { 145 // row was -1, do nothing 146 } 147 } 148 } 149 150 /** 151 * Update the NV controlling the ON position 152 * 153 * index is the output number 1 - 8 154 */ 155 protected class UpdateOnPos implements UpdateNV { 156 157 /** {@inheritDoc} */ 158 @Override 159 public void setNewVal(int index) { 160 int pos = servo[index].onPosSlider.getValue(); 161 // Four NVs per output 162 int nv_index = (index - 1)*4 + Servo8BasePaneProvider.OUT1_ON; 163 //log.debug("UpdateOnPos() index {} nv {} pos {}", index, nv_index, pos); 164 _dataModel.setValueAt(pos, nv_index - 1, CbusNodeNVTableDataModel.NV_SELECT_COLUMN); 165 if (_node.getliveUpdate()) { 166 // Send to module immediately in live update mode 167 _node.send.nVSET(_node.getNodeNumber(), nv_index, pos); 168 } 169 } 170 } 171 172 /** 173 * Update the NV controlling the OFF position 174 * 175 * index is the output number 1 - 8 176 */ 177 protected class UpdateOffPos implements UpdateNV { 178 179 /** {@inheritDoc} */ 180 @Override 181 public void setNewVal(int index) { 182 int pos = servo[index].offPosSlider.getValue(); 183 // Four NVs per output 184 int nv_index = (index - 1)*4 + Servo8BasePaneProvider.OUT1_OFF; 185 //log.debug("UpdateOffPos() index {} nv {} pos {}", index, nv_index, pos); 186 _dataModel.setValueAt(pos, nv_index - 1, CbusNodeNVTableDataModel.NV_SELECT_COLUMN); 187 if (_node.getliveUpdate()) { 188 // Send to module immediately in live update mode 189 _node.send.nVSET(_node.getNodeNumber(), nv_index, pos); 190 } 191 } 192 } 193 194 /** 195 * Update the NV controlling the ON speed 196 * 197 * index is the output number 1 - 8 198 */ 199 protected class UpdateOnSpd implements UpdateNV { 200 201 /** {@inheritDoc} */ 202 @Override 203 public void setNewVal(int index) { 204 int spd = servo[index].onSpdSpinner.getIntegerValue(); 205 // Four NVs per output 206 int nv_index = (index - 1)*4 + Servo8BasePaneProvider.OUT1_ON_SPD; 207 //log.debug("UpdateOnSpeed() index {} nv {} spd {}", index, nv_index, spd); 208 // Note that changing the data model will result in tableChanged() being called 209 _dataModel.setValueAt(spd, nv_index - 1, CbusNodeNVTableDataModel.NV_SELECT_COLUMN); 210 if (_node.getliveUpdate()) { 211 // Send to module immediately in live update mode 212 _node.send.nVSET(_node.getNodeNumber(), nv_index, spd); 213 } 214 } 215 } 216 217 /** 218 * Update the NV controlling the OFF speed 219 * 220 * index is the output number 1 - 8 221 */ 222 protected class UpdateOffSpd implements UpdateNV { 223 224 /** {@inheritDoc} */ 225 @Override 226 public void setNewVal(int index) { 227 int spd = servo[index].offSpdSpinner.getIntegerValue(); 228 // Four NVs per output 229 int nv_index = (index - 1)*4 + Servo8BasePaneProvider.OUT1_OFF_SPD; 230 //log.debug("UpdateOffSpeed index {} nv {} spd {}", index, nv_index, spd); 231 // Note that changing the data model will result in tableChanged() being called 232 _dataModel.setValueAt(spd, nv_index - 1, CbusNodeNVTableDataModel.NV_SELECT_COLUMN); 233 if (_node.getliveUpdate()) { 234 // Send to module immediately in live update mode 235 _node.send.nVSET(_node.getNodeNumber(), nv_index, spd); 236 } 237 } 238 } 239 240 /** 241 * Update the NVs controlling the startup action 242 */ 243 protected class UpdateStartup implements UpdateNV { 244 245 @Override 246 public void setNewVal(int index) { 247 int newPos = getSelectValue8(Servo8BasePaneProvider.STARTUP_POS) & (~(1<<(index-1))); 248 int newMove = getSelectValue8(Servo8BasePaneProvider.STARTUP_MOVE) & (~(1<<(index-1))); 249 250 // Startup action is in NV2 and NV3, 1 bit per output 251 if (servo[index].action.off.isSelected()) { 252 // 11 253 newPos |= (1<<(index-1)); 254 newMove |= (1<<(index-1)); 255 } else if (servo[index].action.saved.isSelected()) { 256 // 01 257 newMove |= (1<<(index-1)); 258 } 259 260 _dataModel.setValueAt(newPos, Servo8BasePaneProvider.STARTUP_POS - 1, CbusNodeNVTableDataModel.NV_SELECT_COLUMN); 261 _dataModel.setValueAt(newMove, Servo8BasePaneProvider.STARTUP_MOVE - 1, CbusNodeNVTableDataModel.NV_SELECT_COLUMN); 262 if (_node.getliveUpdate()) { 263 // Send to module immediately in live update mode 264 _node.send.nVSET(_node.getNodeNumber(), Servo8BasePaneProvider.STARTUP_POS, newPos); 265 _node.send.nVSET(_node.getNodeNumber(), Servo8BasePaneProvider.STARTUP_MOVE, newMove); 266 } 267 } 268 } 269 270 /** 271 * Construct pane to allow configuration of the module outputs 272 */ 273 private class ServoPane extends JPanel { 274 275 int _index; 276 277 protected JButton testOn; 278 protected JButton testOff; 279 protected JCheckBox cutoff; 280 protected JCheckBox seq; 281 protected TitledSlider onPosSlider; 282 protected TitledSlider offPosSlider; 283 protected TitledSpinner onSpdSpinner; 284 protected TitledSpinner offSpdSpinner; 285 protected StartupActionPane action; 286 287 public ServoPane(int index) { 288 super(); 289 _index = index; 290 JPanel gridPane = new JPanel(new GridBagLayout()); 291 GridBagConstraints c = new GridBagConstraints(); 292 c.fill = GridBagConstraints.HORIZONTAL; 293 c.weightx = 1; 294 c.weighty = 1; 295 296 Border border = BorderFactory.createEtchedBorder(EtchedBorder.LOWERED); 297 TitledBorder title = BorderFactory.createTitledBorder(border, Bundle.getMessage("OutputX", _index)); 298 setBorder(title); 299 300 testOn = new JButton(Bundle.getMessage("TestOn")); 301 testOff = new JButton(Bundle.getMessage("TestOff")); 302 cutoff = new JCheckBox(Bundle.getMessage("Cutoff")); 303 seq = new JCheckBox(Bundle.getMessage("SequentialOp")); 304 305 testOn.setToolTipText(Bundle.getMessage("TestOnTt")); 306 testOff.setToolTipText(Bundle.getMessage("TestOffTt")); 307 cutoff.setToolTipText(Bundle.getMessage("CutoffTt")); 308 seq.setToolTipText(Bundle.getMessage("SequentialOpTt")); 309 310 testOn.addActionListener((ActionEvent e) -> { 311 testActionListener(e); 312 }); 313 testOff.addActionListener((ActionEvent e) -> { 314 testActionListener(e); 315 }); 316 cutoff.addActionListener((ActionEvent e) -> { 317 cutoffActionListener(); 318 }); 319 seq.addActionListener((ActionEvent e) -> { 320 seqActionListener(); 321 }); 322 323 onPosSlider = new TitledSlider(Bundle.getMessage("OnPos"), _index, onPosUpdateFn); 324 onPosSlider.setToolTip(Bundle.getMessage("OnPosTt")); 325 onPosSlider.init(0, 255, 127); 326 327 offPosSlider = new TitledSlider(Bundle.getMessage("OffPos"), _index, offPosUpdateFn); 328 offPosSlider.setToolTip(Bundle.getMessage("OffPosTt")); 329 offPosSlider.init(0, 255, 127); 330 331 onSpdSpinner = new TitledSpinner(Bundle.getMessage("OnSpd"), _index, onSpdUpdateFn); 332 onSpdSpinner.setToolTip(Bundle.getMessage("OnSpdTt")); 333 onSpdSpinner.init(0, 0, 7, 1); 334 335 offSpdSpinner = new TitledSpinner(Bundle.getMessage("OffSpd"), _index, offSpdUpdateFn); 336 offSpdSpinner.setToolTip(Bundle.getMessage("OffSpdTt")); 337 offSpdSpinner.init(0, 0, 7, 1); 338 339 c.gridx = 0; 340 c.gridy = 0; 341 c.gridwidth = 3; 342 c.weighty = 1; 343 gridPane.add(onPosSlider, c); 344 c.gridy++; 345 gridPane.add(offPosSlider, c); 346 c.gridy++; 347 c.gridwidth = 1; 348 gridPane.add(testOn, c); 349 c.gridx++; 350 gridPane.add(testOff, c); 351 c.gridx++; 352 gridPane.add(cutoff, c); 353 354 c.gridx = 3; 355 c.gridy = 0; 356 gridPane.add(onSpdSpinner, c); 357 c.gridy++; 358 gridPane.add(offSpdSpinner, c); 359 c.gridy++; 360 gridPane.add(seq, c); 361 362 c.gridx = 4; 363 c.gridy = 0; 364 c.gridheight = 3; 365 action = new StartupActionPane(_index); 366 gridPane.add(action, c); 367 368 add(gridPane); 369 } 370 371 /** 372 * Callback for test buttons. 373 * 374 * Writes output number to NV37, adding 128 for ON event 375 * @param e ActionEvent 376 */ 377 protected void testActionListener(ActionEvent e) { 378 int val; 379 for (int i = 1; i <= OUTPUTS; i++) { 380 val = 0; 381 if (e.getSource() == servo[i].testOn) { 382 log.debug("Servo {} test ON", i); 383 val = 128 + i; 384 } else if (e.getSource() == servo[i].testOff) { 385 log.debug("Servo {} test OFF", i); 386 val = i; 387 } 388 if (val > 0) { 389 // Send to module immediately 390 _node.send.nVSET(_node.getNodeNumber(), Servo8BasePaneProvider.LAST, val); 391 } 392 } 393 } 394 395 /** 396 * Callback for cut off buttons. 397 */ 398 protected void cutoffActionListener() { 399 int newCutoff = 0; 400 for (int i = OUTPUTS; i > 0; i--) { 401 newCutoff = (newCutoff << 1) + ((servo[i].cutoff.isSelected()) ? 1 : 0); 402 } 403 log.debug("Cutoff Action now {}", newCutoff); 404 _dataModel.setValueAt(newCutoff, Servo8BasePaneProvider.CUTOFF - 1, CbusNodeNVTableDataModel.NV_SELECT_COLUMN); 405 if (_node.getliveUpdate()) { 406 // Send to module immediately in live update mode 407 _node.send.nVSET(_node.getNodeNumber(), Servo8BasePaneProvider.CUTOFF, newCutoff); 408 } 409 } 410 411 /** 412 * Callback for sequential move button. 413 */ 414 protected void seqActionListener() { 415 int newSeq = 0; 416 for (int i = OUTPUTS; i > 0; i--) { 417 newSeq = (newSeq << 1) + ((servo[i].seq.isSelected()) ? 1 : 0); 418 } 419 log.debug("Sequential Action now {}", newSeq); 420 _dataModel.setValueAt(newSeq, Servo8BasePaneProvider.SEQUENCE - 1, CbusNodeNVTableDataModel.NV_SELECT_COLUMN); 421 if (_node.getliveUpdate()) { 422 // Send to module immediately in live update mode 423 _node.send.nVSET(_node.getNodeNumber(), Servo8BasePaneProvider.SEQUENCE, newSeq); 424 } 425 } 426 } 427 428 /** 429 * Construct pane to allow configuration of the output startup action 430 */ 431 private class StartupActionPane extends JPanel { 432 433 int _index; 434 435 JRadioButton off; 436 JRadioButton none; 437 JRadioButton saved; 438 439 public StartupActionPane(int index) { 440 super(); 441 _index = index; 442 JPanel gridPane = new JPanel(new GridBagLayout()); 443 GridBagConstraints c = new GridBagConstraints(); 444 c.fill = GridBagConstraints.HORIZONTAL; 445 c.weightx = 1; 446 c.weighty = 1; 447 c.gridx = 0; 448 c.gridy = 0; 449 450 Border border = BorderFactory.createEtchedBorder(EtchedBorder.LOWERED); 451 TitledBorder title = BorderFactory.createTitledBorder(border, Bundle.getMessage("StartupAction")); 452 setBorder(title); 453 454 off = new JRadioButton(Bundle.getMessage("Off")); 455 off.setToolTipText(Bundle.getMessage("OffTt")); 456 none = new JRadioButton(Bundle.getMessage("None")); 457 none.setToolTipText(Bundle.getMessage("NoneTt")); 458 saved = new JRadioButton(Bundle.getMessage("SavedAction")); 459 saved.setToolTipText(Bundle.getMessage("SavedActionTt")); 460 461 off.addActionListener((ActionEvent e) -> { 462 startupActionListener(); 463 }); 464 none.addActionListener((ActionEvent e) -> { 465 startupActionListener(); 466 }); 467 saved.addActionListener((ActionEvent e) -> { 468 startupActionListener(); 469 }); 470 471 ButtonGroup buttons = new ButtonGroup(); 472 buttons.add(off); 473 buttons.add(none); 474 buttons.add(saved); 475 setButtons(); 476 // Startup action is in NV2 and NV3, 1 bit per output 477 if ((getSelectValue8(Servo8BasePaneProvider.STARTUP_POS) & (1<<(_index-1)))>0) { 478 // 1x 479 off.setSelected(true); 480 } else if ((getSelectValue8(Servo8BasePaneProvider.STARTUP_MOVE) & (1<<(_index-1)))>0) { 481 // 01 482 saved.setSelected(true); 483 } else { 484 // 00 485 none.setSelected(true); 486 } 487 488 gridPane.add(off, c); 489 c.gridy++; 490 gridPane.add(none, c); 491 c.gridy++; 492 gridPane.add(saved, c); 493 494 add(gridPane); 495 } 496 497 /** 498 * Set startup action button states 499 */ 500 public void setButtons() { 501 // Startup action is in NV2 and NV3, 1 bit per output 502 if ((getSelectValue8(Servo8BasePaneProvider.STARTUP_POS) & (1<<(_index-1)))>0) { 503 // 1x 504 off.setSelected(true); 505 } else if ((getSelectValue8(Servo8BasePaneProvider.STARTUP_MOVE) & (1<<(_index-1)))>0) { 506 // 01 507 saved.setSelected(true); 508 } else { 509 // 00 510 none.setSelected(true); 511 } 512 } 513 514 /** 515 * Call the callback to update from radio button selection state. 516 */ 517 protected void startupActionListener() { 518 startupUpdateFn.setNewVal(_index); 519 } 520 } 521 522 private final static Logger log = LoggerFactory.getLogger(Servo8BaseEditNVPane.class); 523 524}