001package jmri.implementation;
002
003import jmri.ProgListener;
004import jmri.Programmer;
005import jmri.jmrix.AbstractProgrammerFacade;
006import org.slf4j.Logger;
007import org.slf4j.LoggerFactory;
008
009/**
010 * Programmer facade for accessing CVs that require one or more "index CVs" 
011 * to have specific values before doing the final read or write operation.
012 * <p>
013 * Currently supports direct access to CVs (the usual style), operations where
014 * one index CV (called PI, for primary index) must have a specific value first,
015 * and operations where two index CVs (called PI and SI, for secondary index)
016 * must have a specific value first. 
017 * <p>
018 * Accepts two different address formats so that the CV addresses can be 
019 * written in the same style as the decoder manufacturer's documentation:
020 * <ul>
021 * <li>If cvFirst is true:
022 * <ul>
023 *   <li> 123 Do read or write directly to CV 123; this allows unindexed CVs to go through
024 *   <li> 123.11 Writes 11 to PI, the index CV, then does the final read or write to CV 123
025 *   <li> 123.11.12 Writes 11 to the first index CV, then 12 to the second index CV, 
026 *                    then does the final read or write to CV 123
027 * </ul>
028 * <li>If cvFirst is false:
029 * <ul>
030 *   <li> 123 Do read or write directly to CV 123; this allows unindexed CVs to go through
031 *   <li> 11.123 Writes 11 to the first index CV, then does the final read or write to CV 123
032 *   <li> 11.12.123 Writes 11 to the first index CV, then 12 to the second index CV, 
033 *              then does the final read or write to CV 123
034 * </ul>
035 * </ul>
036 * QSI decoders generally use the 1st format, and ESU LokSound decoders the second.
037 * <p>
038 * In some circumstances, the PI and SI values should be set to specific values
039 * <u>after</u> the operation is otherwise complete.  The syntax for this
040 * uses a semicolon:
041 * <ul>
042 *  <li>11.12.123;0.1 After the read or write operation has accessed the main CV,
043 *          write 0 to the PI and 1 to the SI.
044 * </ul>
045 * <p>
046 * The specific CV numbers for PI and SI are provided when constructing the object.
047 * They can be read from a decoder definition file 
048 * by e.g. {@link jmri.implementation.ProgrammerFacadeSelector}.
049 * <p>
050 * Alternately the PI and/or SI CV numbers can be set by using a "nn=nn" syntax when specifying
051 * PI and/or SI.  For example, using a cvFirst false syntax, "101=12.80" sets CV101 to 12 before
052 * accessing CV 80, regardless of the PI value configured into the facade.
053 * <p>
054 * If skipDupIndexWrite is true, sequential operations with the same PI and SI values
055 * (and only immediately sequential operations with both PI and SI unchanged) will
056 * skip writing of the PI and SI CVs.  This might not work for some decoders, hence is
057 * configurable. See the logic in {@link jmri.implementation.ProgrammerFacadeSelector}
058 * for how the decoder file contents and default (preferences) interact.
059 * <p>
060 * State Diagram for read and write operations (click to magnify):
061 * <br>
062 * <a href="doc-files/MultiIndexProgrammerFacade-State-Diagram.png"><img src="doc-files/MultiIndexProgrammerFacade-State-Diagram.png" alt="UML State diagram" height="50%" width="50%"></a>
063 *
064 * @see jmri.implementation.ProgrammerFacadeSelector
065 *
066 * @author Bob Jacobsen Copyright (C) 2013
067 * @author Andrew Crosland Copyright (C) 2021
068 */
069 
070/*
071 * @startuml jmri/implementation/doc-files/MultiIndexProgrammerFacade-State-Diagram.png
072 * [*] --> NOTPROGRAMMING 
073 * NOTPROGRAMMING --> PROGRAMMING: readCV() & & PI==-1\n(read CV)
074 * NOTPROGRAMMING --> FINISHREAD: readCV() & PI!=-1\n(write PI)
075 * NOTPROGRAMMING --> PROGRAMMING: writeCV() & single CV\n(write CV)
076 * NOTPROGRAMMING --> FINISHWRITE: writeCV() & PI write needed\n(write PI)
077 *
078 * FINISHREAD --> FINISHREAD: OK reply & SI!=-1\n(write SI)
079 * FINISHREAD --> PROGRAMMING: OK reply & SI==-1\n(read CV)
080 * FINISHWRITE --> FINISHWRITE: OK reply & SI!=-1\n(write SI)
081 * FINISHWRITE --> PROGRAMMING: OK reply & SI==-1\n(write CV)
082 * PROGRAMMING --> NOTPROGRAMMING: OK reply received and no after-index\n(return status and value)
083 *
084 * FINISHREAD --> NOTPROGRAMMING : Error reply received
085 * FINISHWRITE --> NOTPROGRAMMING : Error reply received
086 * PROGRAMMING --> NOTPROGRAMMING : Error reply received
087 *
088 * PROGRAMMING --> AFTERWRITEFIRST : if PI, SI are to be reset,\n write PI value
089 * 
090 * AFTERWRITEFIRST --> NOTPROGRAMMING : Error reply received
091 * AFTERWRITEFIRST --> AFTERWRITESECOND : write SI value
092 *
093 * AFTERWRITESECOND --> NOTPROGRAMMING : Error reply received
094 * AFTERWRITESECOND --> NOTPROGRAMMING : OK reply received\n(return status and value)
095 * @enduml
096*/
097
098public class MultiIndexProgrammerFacade extends AbstractProgrammerFacade implements ProgListener {
099
100    /**
101     * @param prog              the programmer to which this facade is attached
102     * @param indexPI           CV to which the first value is to be written for
103     *                          NN.NN and NN.NN.NN forms
104     * @param indexSI           CV to which the second value is to be written
105     *                          for NN.NN.NN forms
106     * @param cvFirst           true if first value is parsed CV to be
107     *                          written; false if second value is to be written
108     * @param skipDupIndexWrite true if heuristics can be used to skip PI and SI
109     *                          writes; false requires them to be written each
110     *                          time.
111     */
112    public MultiIndexProgrammerFacade(Programmer prog, String indexPI, String indexSI, boolean cvFirst, boolean skipDupIndexWrite) {
113        super(prog);
114        this.defaultIndexPI = indexPI;
115        this.defaultIndexSI = indexSI;
116        this.cvFirst = cvFirst;
117        this.skipDupIndexWrite = skipDupIndexWrite;
118    }
119
120    String defaultIndexPI;
121    String defaultIndexSI;
122
123    String indexPI;
124    String indexSI;
125    boolean cvFirst;
126    boolean skipDupIndexWrite;
127
128    long maxDelay = 1000;  // max mSec since last successful end-of-operation for skipDupIndexWrite; longer delay writes anyway
129
130    // members for handling the programmer interface
131    int _val; // remember the value being read/written for confirmative reply
132    String _cv; // remember the cv number being read/written
133    int valuePI;  //  value to write to PI before current operation or -1
134    int valueSI;  //  value to write to SI before current operation or -1
135    int valuePIafter;  //  value to write to PI after current operation or -1
136    int valueSIafter;  //  value to write to SI after current operation or -1
137    int _startVal;  // Current CV value hint
138
139    // remember last operation for skipDupIndexWrite
140    int lastValuePI = -1;  // value written in last operation
141    int lastValueSI = -1;  // value written in last operation
142    long lastOpTime = -1;  // time of last complete
143
144    // take the CV string and configure the actions to take
145    void parseCV(String cv) {
146        valuePI = -1;
147        valueSI = -1;
148        valuePIafter = -1;
149        valueSIafter = -1;
150        if (cv.contains(".")) {
151            String[] parts = cv.split(";");
152            String[] splits = parts[0].split("\\.");
153            if (cvFirst) {
154                switch (splits.length) {
155                    case 2:
156                        if (hasAlternateAddress(splits[1])) {
157                            valuePI = getAlternateValue(splits[1]);
158                            indexPI = getAlternateAddress(splits[1]);
159                        } else {
160                            valuePI = Integer.parseInt(splits[1]);
161                            indexPI = defaultIndexPI;
162                        }
163                        _cv = splits[0];
164                        break;
165                    case 3:
166                        if (hasAlternateAddress(splits[1])) {
167                            valuePI = getAlternateValue(splits[1]);
168                            indexPI = getAlternateAddress(splits[1]);
169                        } else {
170                            valuePI = Integer.parseInt(splits[1]);
171                            indexPI = defaultIndexPI;
172                        }
173                        if (hasAlternateAddress(splits[2])) {
174                            valueSI = getAlternateValue(splits[2]);
175                            indexSI = getAlternateAddress(splits[2]);
176                        } else {
177                            valueSI = Integer.parseInt(splits[2]);
178                            indexSI = defaultIndexSI;
179                        }
180                        _cv = splits[0];
181                        break;
182                    default:
183                        log.error("Too many parts in CV name {}; taking 1st two", cv);
184                        valuePI = Integer.parseInt(splits[1]);
185                        valueSI = Integer.parseInt(splits[2]);
186                        _cv = splits[0];
187                        break;
188                }
189            } else {
190                // not cvFirst case
191                switch (splits.length) {
192                    case 2:
193                        if (hasAlternateAddress(splits[0])) {
194                            valuePI = getAlternateValue(splits[0]);
195                            indexPI = getAlternateAddress(splits[0]);
196                        } else {
197                            valuePI = Integer.parseInt(splits[0]);
198                            indexPI = defaultIndexPI;
199                        }
200                        _cv = splits[1];
201                        break;
202                    case 3:
203                        if (hasAlternateAddress(splits[0])) {
204                            valuePI = getAlternateValue(splits[0]);
205                            indexPI = getAlternateAddress(splits[0]);
206                        } else {
207                            valuePI = Integer.parseInt(splits[0]);
208                            indexPI = defaultIndexPI;
209                        }
210                        if (hasAlternateAddress(splits[1])) {
211                            valueSI = getAlternateValue(splits[1]);
212                            indexSI = getAlternateAddress(splits[1]);
213                        } else {
214                            valueSI = Integer.parseInt(splits[1]);
215                            indexSI = defaultIndexSI;
216                        }
217                        _cv = splits[2];
218                        break;
219                    default:
220                        log.error("Too many parts in CV name {}; taking 1st two", cv);
221                        valuePI = Integer.parseInt(splits[0]);
222                        valueSI = Integer.parseInt(splits[1]);
223                        _cv = splits[2];
224                        break;
225                }
226                // now handle anything after a semicolon
227            }
228            if (parts.length == 2) {
229                String[] afters = parts[1].split("\\.");
230                valuePIafter = Integer.parseInt(afters[0]);
231                valueSIafter = Integer.parseInt(afters[1]);
232            }
233        } else {
234            // no "." in CV number argument; accept as number, don't parts for semicolon
235            _cv = cv;
236        }
237        
238        
239    }
240
241    boolean hasAlternateAddress(String cv) {
242        return cv.contains("=");
243    }
244    
245    String getAlternateAddress(String cv) {
246        return cv.split("=")[0];
247    }
248    
249    int getAlternateValue(String cv) {
250        return Integer.parseInt(cv.split("=")[1]);
251    }
252    
253    /**
254     * Check if the last-written PI and SI values can still be counted on.
255     *
256     * @return true if last-written values are reliable; false otherwise
257     */
258    boolean useCachePiSi() {
259        return skipDupIndexWrite
260                && (lastValuePI == valuePI)
261                && (lastValueSI == valueSI)
262                && ((System.currentTimeMillis() - lastOpTime) < maxDelay);
263    }
264
265    // programming interface
266    @Override
267    synchronized public void writeCV(String CV, int val, jmri.ProgListener p) throws jmri.ProgrammerException {
268        _val = val;
269        useProgrammer(p);
270        parseCV(CV);
271        if (valuePI == -1) {
272            lastValuePI = -1;  // next indexed operation needs to write PI, SI
273            lastValueSI = -1;
274
275            // non-indexed operation
276            state = ProgState.PROGRAMMING;
277            prog.writeCV(_cv, val, this);
278        } else if (useCachePiSi()) {
279            // indexed operation with set values is same as non-indexed operation
280            state = ProgState.PROGRAMMING;
281            prog.writeCV(_cv, val, this);
282        } else {
283            lastValuePI = valuePI;  // after check in 'if' statement
284            lastValueSI = valueSI;
285
286            // write index first
287            state = ProgState.FINISHWRITE;
288            prog.writeCV(indexPI, valuePI, this);
289        }
290    }
291
292    @Override
293    synchronized public void readCV(String CV, jmri.ProgListener p) throws jmri.ProgrammerException {
294        readCV(CV, p, 0);
295    }
296
297    @Override
298    synchronized public void readCV(String CV, jmri.ProgListener p, int startVal) throws jmri.ProgrammerException {
299        useProgrammer(p);
300        parseCV(CV);
301        _startVal = startVal;
302        if (valuePI == -1) {
303            lastValuePI = -1;  // next indexed operation needs to write PI, SI
304            lastValueSI = -1;
305
306            state = ProgState.PROGRAMMING;
307            prog.readCV(_cv, this, _startVal);
308        } else if (useCachePiSi()) {
309            // indexed operation with set values is same as non-indexed operation
310            state = ProgState.PROGRAMMING;
311            prog.readCV(_cv, this, _startVal);
312        } else {
313            lastValuePI = valuePI;  // after check in 'if' statement
314            lastValueSI = valueSI;
315
316            // write index first
317            state = ProgState.FINISHREAD;
318            prog.writeCV(indexPI, valuePI, this);
319        }
320    }
321
322    @Override
323    synchronized public void confirmCV(String CV, int val, jmri.ProgListener p) throws jmri.ProgrammerException {
324        _val = val;
325        useProgrammer(p);
326        parseCV(CV);
327        if (valuePI == -1) {
328            lastValuePI = -1;  // next indexed operation needs to write PI, SI
329            lastValueSI = -1;
330
331            // non-indexed operation
332            state = ProgState.PROGRAMMING;
333            prog.confirmCV(_cv, val, this);
334        } else if (useCachePiSi()) {
335            // indexed operation with set values is same as non-indexed operation
336            state = ProgState.PROGRAMMING;
337            prog.confirmCV(_cv, val, this);
338        } else {
339            lastValuePI = valuePI;  // after check in 'if' statement
340            lastValueSI = valueSI;
341
342            // write index first
343            state = ProgState.FINISHCONFIRM;
344            prog.writeCV(indexPI, valuePI, this);
345        }
346    }
347
348    private jmri.ProgListener _usingProgrammer = null;
349
350    // internal method to remember who's using the programmer
351    protected void useProgrammer(jmri.ProgListener p) throws jmri.ProgrammerException {
352        // test for only one!
353        if (_usingProgrammer != null && _usingProgrammer != p) {
354            log.info("programmer already in use by {}", _usingProgrammer);
355            throw new jmri.ProgrammerException("programmer in use");
356        } else {
357            _usingProgrammer = p;
358        }
359    }
360
361    /**
362     * State machine for MultiIndexProgrammerFacade  (click to magnify):
363     * <a href="doc-files/MultiIndexProgrammerFacade-State-Diagram.png"><img src="doc-files/MultiIndexProgrammerFacade-State-Diagram.png" alt="UML State diagram" height="50%" width="50%"></a>
364     */
365    enum ProgState {
366        /** Waiting for response to (final) read or write operation, final reply next */
367        PROGRAMMING,
368        /** Waiting for response to first or second index write before a final read operation */
369        FINISHREAD,
370        /** Waiting for response to first or second index write before a final write operation */
371        FINISHWRITE,
372        /** Waiting for response to first or second index write before a final confirm operation */
373        FINISHCONFIRM,
374        /** Just wrote PI after the read or write has been done */
375        AFTERWRITEFIRST,
376        /** Just wrote SI after the read or write has been done */
377        AFTERWRITESECOND,
378        /** No current operation */
379        NOTPROGRAMMING
380    }
381    ProgState state = ProgState.NOTPROGRAMMING;
382
383    int storedReturnValue = -1;
384    
385    // get notified of the final result
386    // Note this assumes that there's only one phase to the operation
387    @Override
388    public void programmingOpReply(int value, int status) {
389        log.debug("notifyProgListenerEnd value {} status {} ", value, status);
390
391        if (status != OK) {
392            // clear memory of last PI, SI written
393            lastValuePI = -1;
394            lastValueSI = -1;
395            lastOpTime = -1;
396
397            // pass abort up
398            log.debug("Reset and pass abort up");
399            jmri.ProgListener temp = _usingProgrammer;
400            _usingProgrammer = null; // done
401            state = ProgState.NOTPROGRAMMING;
402            temp.programmingOpReply(value, status);
403            return;
404        }
405
406        if (_usingProgrammer == null) {
407            log.error("No listener to notify, reset and ignore");
408            state = ProgState.NOTPROGRAMMING;
409            return;
410        }
411
412        jmri.ProgListener tempProgListener; 
413        
414        switch (state) {
415            case PROGRAMMING:
416                // check for whether after operation id needed
417                if (valuePIafter != -1) {
418                    // save the programming value
419                    storedReturnValue = value;
420                    // write primary index
421                    try {
422                        lastValuePI = valuePIafter;
423                        lastValueSI = valueSIafter;
424
425                        valuePIafter = -1;
426                        state = ProgState.AFTERWRITEFIRST;
427                        prog.writeCV(indexPI, lastValuePI, this); 
428                    } catch (jmri.ProgrammerException e) {
429                        log.error("Exception doing write PI after operation", e);
430                    }   
431                    break;         
432                }
433                // No write after, this is the end of the operation
434                // the programmingOpReply handler might send an immediate reply, so
435                // clear the current listener _first_
436                tempProgListener = _usingProgrammer;
437                _usingProgrammer = null; // done
438                state = ProgState.NOTPROGRAMMING;
439                lastOpTime = System.currentTimeMillis();
440                tempProgListener.programmingOpReply(value, status);
441                break;
442            case AFTERWRITEFIRST:
443                if (valueSIafter == -1) {
444                    log.error("Does not yet handle writing only one after index value");
445                } else {
446                    // need to write 2nd after-operation CV
447                    try {
448                        int tempSI = valueSIafter;
449                        valueSIafter = -1;
450                        state = ProgState.AFTERWRITESECOND;
451                        prog.writeCV(indexSI, tempSI, this);
452                    } catch (jmri.ProgrammerException e) {
453                        log.error("Exception doing write SI after operation", e);
454                    }
455                }
456                
457                break;
458
459            case AFTERWRITESECOND:
460                // 2nd after write done, this is the end of the operation
461                // the programmingOpReply handler might send an immediate reply, so
462                // clear the current listener _first_
463                tempProgListener = _usingProgrammer;
464                _usingProgrammer = null; // done
465                state = ProgState.NOTPROGRAMMING;
466                lastOpTime = System.currentTimeMillis();
467                tempProgListener.programmingOpReply(storedReturnValue, status);
468                break;
469
470
471            case FINISHREAD:
472                if (valueSI == -1) {
473                    try {
474                        state = ProgState.PROGRAMMING;
475                        prog.readCV(_cv, this, _startVal);
476                    } catch (jmri.ProgrammerException e) {
477                        log.error("Exception doing final read", e);
478                    }
479                } else {
480                    try {
481                        int tempSI = valueSI;
482                        valueSI = -1;
483                        state = ProgState.FINISHREAD;
484                        prog.writeCV(indexSI, tempSI, this);
485                    } catch (jmri.ProgrammerException e) {
486                        log.error("Exception doing write SI for read", e);
487                    }
488                }
489                break;
490            case FINISHWRITE:
491                if (valueSI == -1) {
492                    try {
493                        state = ProgState.PROGRAMMING;
494                        prog.writeCV(_cv, _val, this);
495                    } catch (jmri.ProgrammerException e) {
496                        log.error("Exception doing final write", e);
497                    }
498                } else {
499                    try {
500                        int tempSI = valueSI;
501                        valueSI = -1;
502                        state = ProgState.FINISHWRITE;
503                        prog.writeCV(indexSI, tempSI, this);
504                    } catch (jmri.ProgrammerException e) {
505                        log.error("Exception doing write SI for write", e);
506                    }
507                }
508                break;
509            case FINISHCONFIRM:
510                if (valueSI == -1) {
511                    try {
512                        state = ProgState.PROGRAMMING;
513                        prog.confirmCV(_cv, _val, this);
514                    } catch (jmri.ProgrammerException e) {
515                        log.error("Exception doing final confirm", e);
516                    }
517                } else {
518                    try {
519                        int tempSI = valueSI;
520                        valueSI = -1;
521                        state = ProgState.FINISHCONFIRM;
522                        prog.writeCV(indexSI, tempSI, this);
523                    } catch (jmri.ProgrammerException e) {
524                        log.error("Exception doing write SI for write", e);
525                    }
526                }
527                break;
528            default:
529                log.error("Unexpected state on reply: {}", state);
530                // clean up as much as possible
531                _usingProgrammer = null;
532                state = ProgState.NOTPROGRAMMING;
533                lastValuePI = -1;
534                lastValueSI = -1;
535                lastOpTime = -1;
536
537        }
538    }
539
540    private final static Logger log = LoggerFactory.getLogger(MultiIndexProgrammerFacade.class);
541
542}