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}