001package jmri.jmrit.symbolicprog;
002
003import java.awt.Color;
004import javax.swing.JLabel;
005import javax.swing.JTextField;
006import jmri.ProgListener;
007import jmri.Programmer;
008import org.slf4j.Logger;
009import org.slf4j.LoggerFactory;
010
011/**
012 * Encapsulate a single CV value and provide programming access to the decoder.
013 * <p>
014 * Since this is a single CV in a single decoder, the Programmer used to get
015 * access is part of the state. This allows us to specify a specific ops-mode
016 * programmer aimed at a particular decoder.
017 * <p>
018 * There are three relevant parameters: Busy, Value, State. Busy == true means
019 * that a read or write operation is going on. When it transitions to "false",
020 * the operation is complete, and the Value and State are stable. During a read
021 * operation, Value changes before State, so you can assume that Value is stable
022 * if notified of a State change.
023 * <p>
024 * CVs track their states, as shown below.<br>
025 * <a href="doc-files/VariableStates.png"><img src="doc-files/VariableStates.png" alt="States for variables and their colors" height="33%" width="33%"></a>
026 *
027 * @author Bob Jacobsen Copyright (C) 2001, 2003, 2004, 2013
028 * @author Howard G. Penny Copyright (C) 2005
029 * @author Andrew Crosland (C) 2021
030 */
031public class CvValue extends AbstractValue implements ProgListener {
032
033    public CvValue(String num, Programmer pProgrammer) {
034        _num = num;
035        mProgrammer = pProgrammer;
036        _tableEntry = new JTextField("0", 3);
037        _defaultColor = _tableEntry.getBackground();
038        _tableEntry.setBackground(ValueState.UNKNOWN.getColor());
039    }
040
041    public CvValue(String num, String cvName, Programmer pProgrammer) {
042        _num = num;
043        _cvName = cvName;
044        if (cvName == null) {
045            log.error("cvName == null in ctor num: {}", num); // NOI18N
046        }
047        mProgrammer = pProgrammer;
048        _tableEntry = new JTextField("0", 3);
049        _defaultColor = _tableEntry.getBackground();
050        _tableEntry.setBackground(ValueState.UNKNOWN.getColor());
051    }
052
053    @Override
054    public String toString() {
055        return "CvValue _num=" + _num + " _cvName=" + _cvName;
056    }
057
058    void setProgrammer(Programmer p) {
059        mProgrammer = p;
060    }
061
062    public String number() {
063        return _num;
064    }
065    private final String _num;
066
067    public String cvName() {
068        return _cvName;
069    }
070    private String _cvName = "";
071
072    private JLabel _status = null;
073
074    private Programmer mProgrammer;
075
076    public int getValue() {
077        return _value;
078    }
079
080    Color getDefaultColor() {
081        return _defaultColor;
082    }
083
084    Color getColor() {
085        return _tableEntry.getBackground();
086    }
087
088    protected void notifyValueChange(int value) {
089        prop.firePropertyChange("Value", null, value);
090    }
091
092
093    /**
094     * Edit a new value into the CV. Fires listeners
095     * <p>
096     * Only use this for external edits, e.g. set from a GUI.
097     * Not for internal uses, as it sets the state to EDITED.
098     * @param value new CV value.
099     */
100    public void setValue(int value) {
101        log.debug("CV {} value changed from {} to {}", number(), _value, value); // NOI18N
102
103        setState(ValueState.EDITED);
104        if (_value != value) {
105            _value = value;
106            _tableEntry.setText("" + value);
107            notifyValueChange(value);
108        }
109    }
110    private int _value = 0;
111
112    /**
113     * Get the decoder value read during compare.
114     *
115     * @return _decoderValue
116     */
117    public int getDecoderValue() {
118        return _decoderValue;
119    }
120
121    private int _decoderValue = 0;
122
123    public ValueState getState() {
124        return _state;
125    }
126
127    /**
128     * Set state value and send notification.Also sets GUI color as needed.
129     * @param state new state, e.g READ, UNKNOWN, SAME.
130     */
131    public void setState(ValueState state) {
132        if (log.isDebugEnabled()) {  // stateToString overhead
133            log.debug("cv {} set state from {} to {}", number(), _state.name(), state.name()); // NOI18N
134        }
135        ValueState oldstate = _state;
136        _state = state;
137        setColor(state.getColor());
138        if (oldstate != state) {
139            prop.firePropertyChange("State", oldstate, state);
140        }
141    }
142
143    private ValueState _state = ValueState.UNKNOWN;
144
145    // read, write operations
146    public boolean isBusy() {
147        return _busy;
148    }
149
150    /**
151     * Set the busy state and send notification. Should be used _only_ if this
152     * is the only thing changing.
153     */
154    private void setBusy(boolean busy) {
155        log.debug("setBusy from {} to {} state {}", _busy, busy, _state); // NOI18N
156
157        boolean oldBusy = _busy;
158        _busy = busy;
159        notifyBusyChange(oldBusy, busy);
160    }
161
162    /**
163     * Notify of changes to the busy state.
164     */
165    private void notifyBusyChange(boolean oldBusy, boolean newBusy) {
166        log.debug("notifyBusyChange from {} to {} current state {}", oldBusy, newBusy, _state); // NOI18N
167
168        if (oldBusy != newBusy) {
169            prop.firePropertyChange("Busy",
170                    oldBusy ? Boolean.TRUE : Boolean.FALSE,
171                    newBusy ? Boolean.TRUE : Boolean.FALSE);
172        }
173    }
174    private boolean _busy = false;
175
176    // color management
177    Color _defaultColor;
178
179    @Override
180    void setColor(Color c) {
181        if (c != null) {
182            _tableEntry.setBackground(c);
183        } else {
184            _tableEntry.setBackground(_defaultColor);
185        }
186        //prop.firePropertyChange("Value", null, null);
187    }
188
189    // object for Table entry
190    JTextField _tableEntry = null;
191
192    JTextField getTableEntry() {
193        return _tableEntry;
194    }
195
196    /**
197     * Set bean keeping track of whether this CV is intended to be read-only.
198     * Does not otherwise affect behaviour! Default is "false".
199     * @param is read-only flag, true or false.
200     */
201    public void setReadOnly(boolean is) {
202        _readOnly = is;
203    }
204
205    private boolean _readOnly = false;
206
207    /**
208     * Retrieve bean keeping track of whether this CV is intended to be
209     * read-only. Does not otherwise affect behaviour! Default is "false".
210     * @return read-only flag.
211     */
212    public boolean getReadOnly() {
213        return _readOnly;
214    }
215
216    /**
217     * Set bean keeping track of whether this CV is intended to be used as
218     * info-only. Does not otherwise affect behaviour! Default is "false".
219     * @param is info-only flag, true or false.
220     */
221    public void setInfoOnly(boolean is) {
222        _infoOnly = is;
223    }
224
225    private boolean _infoOnly = false;
226
227    /**
228     * Retrieve bean keeping track of whether this CV is intended to be used as
229     * info-only. Does not otherwise affect behaviour! Default is "false".
230     * @return write-only flag.
231     */
232    public boolean getInfoOnly() {
233        return _infoOnly;
234    }
235
236    /**
237     * Set bean keeping track of whether this CV is intended to be used as
238     * write-only. Does not otherwise affect behaviour! Default is "false".
239     * @param is write-only flag, true or false.
240     */
241    public void setWriteOnly(boolean is) {
242        _writeOnly = is;
243    }
244
245    private boolean _writeOnly = false;
246
247    /**
248     * Retrieve bean keeping track of whether this CV is intended to be used as
249     * write-only.
250     * <p>
251     * Does not otherwise affect behaviour!
252     * Default is "false".
253     * @return write-only flag.
254     */
255    public boolean getWriteOnly() {
256        return _writeOnly;
257    }
258
259    @Override
260    public void setToRead(boolean state) {
261        if (getInfoOnly() || getWriteOnly()) {
262            state = false;
263        }
264        _toRead = state;
265    }
266
267    @Override
268    public boolean isToRead() {
269        return _toRead;
270    }
271    private boolean _toRead = false;
272
273    @Override
274    public void setToWrite(boolean state) {
275        if (getInfoOnly() || getReadOnly()) {
276            state = false;
277        }
278        _toWrite = state;
279    }
280
281    @Override
282    public boolean isToWrite() {
283        return _toWrite;
284    }
285    private boolean _toWrite = false;
286
287    // read, write support
288    private boolean _reading = false;
289    private boolean _confirm = false;
290
291    public void read(JLabel status) {
292        read(status,0,0,0);
293    }
294    public void read(JLabel status, long number, long total, long cvReadStartTime) {
295        log.debug("read call with Cv number {} and programmer {}", _num, mProgrammer); // NOI18N
296
297        setToRead(false);
298        // get a programmer reference and write
299        _status = status;
300
301        if (status != null) {
302            if (total == 0) {
303                status.setText(
304                        java.text.MessageFormat.format(
305                                Bundle.getMessage("StateReadingCV"),
306                                new Object[]{"" + _num}));
307
308            } else {
309                long remaining = ((System.currentTimeMillis() - cvReadStartTime) / number * (total - number)) / 1000;
310                String units = "seconds";
311                if (remaining > 60) {
312                    remaining = remaining / 60;
313                    units = "minutes";
314                }
315                status.setText(
316                        java.text.MessageFormat.format(
317                                Bundle.getMessage("StateReadingCVincltime"),
318                                new Object[]{"" + _num, number, total, remaining, units}));
319            }
320        }
321
322        if (mProgrammer != null) {
323            setBusy(true);
324            _reading = true;
325            _confirm = false;
326            try {
327                mProgrammer.readCV(_num, this, this.getValue());
328            } catch (Exception e) {
329                if (status != null) {
330                    status.setText(
331                            java.text.MessageFormat.format(
332                                    Bundle.getMessage("StateExceptionDuringRead"),
333                                    new Object[]{e.toString()}));
334                }
335
336                log.warn("Exception during CV read", e); // NOI18N
337                setBusy(false);
338            }
339        } else {
340            if (status != null) {
341                status.setText(Bundle.getMessage("StateNoProgrammer"));
342            }
343            log.error("No programmer available!"); // NOI18N
344        }
345    }
346
347    public void confirm(JLabel status) {
348        log.debug("confirm call with Cv number {}", _num); // NOI18N
349
350        // get a programmer reference and write
351        _status = status;
352
353        if (status != null) {
354            status.setText(
355                    java.text.MessageFormat.format(
356                            Bundle.getMessage("StateConfirmingCV"),
357                            new Object[]{"" + _num}));
358        }
359
360        if (mProgrammer != null) {
361            setBusy(true);
362            _reading = false;
363            _confirm = true;
364            try {
365                mProgrammer.confirmCV(_num, _value, this);
366            } catch (Exception e) {
367                if (status != null) {
368                    status.setText(
369                            java.text.MessageFormat.format(
370                                    Bundle.getMessage("StateExceptionDuringConfirm"),
371                                    new Object[]{e.toString()}));
372                }
373                log.warn("Exception during CV confirm", e); // NOI18N
374                setBusy(false);
375            }
376        } else {
377            if (status != null) {
378                status.setText(Bundle.getMessage("StateNoProgrammer"));
379            }
380            log.error("No programmer available!"); // NOI18N
381        }
382    }
383
384    public void write(JLabel status) {
385        log.debug("write call with Cv number {}", _num); // NOI18N
386
387        setToWrite(false);
388        // get a programmer reference and write
389        _status = status;
390
391        if (status != null) {
392            status.setText(
393                    java.text.MessageFormat.format(
394                            Bundle.getMessage("StateWritingCV"),
395                            new Object[]{"" + _num}));
396        }
397        if (mProgrammer != null) {
398            setBusy(true);
399            _reading = false;
400            _confirm = false;
401            try {
402                setState(ValueState.UNKNOWN);
403                mProgrammer.writeCV(_num, _value, this);
404            } catch (Exception e) {
405                setState(ValueState.UNKNOWN);
406                if (status != null) {
407                    status.setText(
408                            java.text.MessageFormat.format(
409                                    Bundle.getMessage("StateExceptionDuringWrite"),
410                                    new Object[]{e.toString()}));
411                }
412                log.warn("Exception during write CV '{}' to '{}'", _num, _value, e); // NOI18N
413                setBusy(false);
414            }
415        } else {
416            if (status != null) {
417                status.setText(Bundle.getMessage("StateNoProgrammer"));
418            }
419            log.error("No programmer available!"); // NOI18N
420        }
421    }
422
423    @Override
424    public void programmingOpReply(int value, int retval) {
425        if (log.isDebugEnabled()) {
426            log.debug("CV progOpReply for CV {} with retval {} during {}", _num, retval, _reading ? "read sequence" : (_confirm ? "confirm sequence" : "write sequence"));  // NOI18N
427        }
428        if (!_busy) {
429            log.error("opReply when not busy!"); // NOI18N
430        }
431        boolean oldBusy = _busy;
432        if (retval == OK) {
433            if (_status != null) {
434                _status.setText(Bundle.getMessage("StateOK"));
435            }
436            if (_reading) {
437                // set & notify value directly to avoid state going to EDITED
438                _value = value;
439                _tableEntry.setText(Integer.toString(value));
440                notifyValueChange(value);
441                setState(ValueState.READ);
442                log.debug("CV setting not busy on end read"); // NOI18N
443                _busy = false;
444                notifyBusyChange(oldBusy, _busy);
445            } else if (_confirm) {
446                // _value doesn't change, just the state, and save the value read
447                _decoderValue = value;
448                // does the decoder value match the file value
449                if (value == _value) {
450                    setState(ValueState.SAME);
451                } else {
452                    setState(ValueState.DIFFERENT);
453                }
454                _busy = false;
455                notifyBusyChange(oldBusy, _busy);
456            } else {  // writing
457                setState(ValueState.STORED);
458                _busy = false;
459                notifyBusyChange(oldBusy, _busy);
460            }
461        } else {
462            if (_status != null) {
463                _status.setText(
464                        java.text.MessageFormat.format(
465                                Bundle.getMessage("StateProgrammerError"),
466                                new Object[]{mProgrammer.decodeErrorCode(retval)}));
467            }
468
469            // delay setting not Busy to ensure that the message appears to the user
470            javax.swing.Timer timer = new javax.swing.Timer(1000, new java.awt.event.ActionListener() {
471                @Override
472                public void actionPerformed(java.awt.event.ActionEvent e) {
473                    errorTimeout();
474                }
475            });
476            timer.setInitialDelay(1000);
477            timer.setRepeats(false);
478            timer.start();
479        }
480
481        log.debug("CV progOpReply end of handling CV {}", _num); // NOI18N
482    }
483
484    void errorTimeout() {
485        setState(ValueState.UNKNOWN);
486        log.debug("CV setting not busy on error reply"); // NOI18N
487        _busy = false;
488        notifyBusyChange(true, _busy);
489    }
490
491    // clean up connections when done
492    public void dispose() {
493        log.debug("dispose"); // NOI18N
494    }
495
496    // initialize logging
497    private static final Logger log = LoggerFactory.getLogger(CvValue.class);
498
499}