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