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}