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 * The specific CV numbers for PI and SI are provided when constructing the object.
039 * They can be read from a decoder definition file by e.g. {@link jmri.implementation.ProgrammerFacadeSelector}.
040 * <p>
041 * Alternately the PI and/or SI CV numbers can be set by using a "nn=nn" syntax when specifying
042 * PI and/or SI.  For example, using a cvFirst false syntax, "101=12.80" sets CV101 to 12 before
043 * accessing CV 80, regardless of the PI value configured into the facade.
044 * <p>
045 * If skipDupIndexWrite is true, sequential operations with the same PI and SI values
046 * (and only immediately sequential operations with both PI and SI unchanged) will
047 * skip writing of the PI and SI CVs.  This might not work for some decoders, hence is
048 * configurable. See the logic in {@link jmri.implementation.ProgrammerFacadeSelector}
049 * for how the decoder file contents and default (preferences) interact.
050 * <p>
051 * State Diagram for read and write operations (click to magnify):
052 * <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>
053 *
054 * @see jmri.implementation.ProgrammerFacadeSelector
055 *
056 * @author Bob Jacobsen Copyright (C) 2013
057 * @author Andrew Crosland Copyright (C) 2021
058 */
059 
060/*
061 * @startuml jmri/implementation/doc-files/MultiIndexProgrammerFacade-State-Diagram.png
062 * [*] --> NOTPROGRAMMING 
063 * NOTPROGRAMMING --> PROGRAMMING: readCV() & & PI==-1\n(read CV)
064 * NOTPROGRAMMING --> FINISHREAD: readCV() & PI!=-1\n(write PI)
065 * NOTPROGRAMMING --> PROGRAMMING: writeCV() & single CV\n(write CV)
066 * NOTPROGRAMMING --> FINISHWRITE: writeCV() & PI write needed\n(write PI)
067 * FINISHREAD --> FINISHREAD: OK reply & SI!=-1\n(write SI)
068 * FINISHREAD --> PROGRAMMING: OK reply & SI==-1\n(read CV)
069 * FINISHWRITE --> FINISHWRITE: OK reply & SI!=-1\n(write SI)
070 * FINISHWRITE --> PROGRAMMING: OK reply & SI==-1\n(write CV)
071 * PROGRAMMING --> NOTPROGRAMMING: OK reply received\n(return status and value)
072 * FINISHREAD --> NOTPROGRAMMING : Error reply received
073 * FINISHWRITE --> NOTPROGRAMMING : Error reply received
074 * PROGRAMMING --> NOTPROGRAMMING : Error reply received
075 * @enduml
076*/
077
078public class MultiIndexProgrammerFacade extends AbstractProgrammerFacade implements ProgListener {
079
080    /**
081     * @param prog              the programmer to which this facade is attached
082     * @param indexPI           CV to which the first value is to be written for
083     *                          NN.NN and NN.NN.NN forms
084     * @param indexSI           CV to which the second value is to be written
085     *                          for NN.NN.NN forms
086     * @param cvFirst           true if first value in parsed CV is to be
087     *                          written; false if second value is to be written
088     * @param skipDupIndexWrite true if heuristics can be used to skip PI and SI
089     *                          writes; false requires them to be written each
090     *                          time.
091     */
092    public MultiIndexProgrammerFacade(Programmer prog, String indexPI, String indexSI, boolean cvFirst, boolean skipDupIndexWrite) {
093        super(prog);
094        this.defaultIndexPI = indexPI;
095        this.defaultIndexSI = indexSI;
096        this.cvFirst = cvFirst;
097        this.skipDupIndexWrite = skipDupIndexWrite;
098    }
099
100    String defaultIndexPI;
101    String defaultIndexSI;
102
103    String indexPI;
104    String indexSI;
105    boolean cvFirst;
106    boolean skipDupIndexWrite;
107
108    long maxDelay = 1000;  // max mSec since last successful end-of-operation for skipDupIndexWrite; longer delay writes anyway
109
110    // members for handling the programmer interface
111    int _val; // remember the value being read/written for confirmative reply
112    String _cv; // remember the cv number being read/written
113    int valuePI;  //  value to write to PI in current operation or -1
114    int valueSI;  //  value to write to SI in current operation or -1
115    int _startVal;  // Current CV value hint
116
117    // remember last operation for skipDupIndexWrite
118    int lastValuePI = -1;  // value written in last operation
119    int lastValueSI = -1;  // value written in last operation
120    long lastOpTime = -1;  // time of last complete
121
122    // take the CV string and configure the actions to take
123    void parseCV(String cv) {
124        valuePI = -1;
125        valueSI = -1;
126        if (cv.contains(".")) {
127            if (cvFirst) {
128                String[] splits = cv.split("\\.");
129                switch (splits.length) {
130                    case 2:
131                        if (hasAlternateAddress(splits[1])) {
132                            valuePI = getAlternateValue(splits[1]);
133                            indexPI = getAlternateAddress(splits[1]);
134                        } else {
135                            valuePI = Integer.parseInt(splits[1]);
136                            indexPI = defaultIndexPI;
137                        }
138                        _cv = splits[0];
139                        break;
140                    case 3:
141                        if (hasAlternateAddress(splits[1])) {
142                            valuePI = getAlternateValue(splits[1]);
143                            indexPI = getAlternateAddress(splits[1]);
144                        } else {
145                            valuePI = Integer.parseInt(splits[1]);
146                            indexPI = defaultIndexPI;
147                        }
148                        if (hasAlternateAddress(splits[2])) {
149                            valueSI = getAlternateValue(splits[2]);
150                            indexSI = getAlternateAddress(splits[2]);
151                        } else {
152                            valueSI = Integer.parseInt(splits[2]);
153                            indexSI = defaultIndexSI;
154                        }
155                        _cv = splits[0];
156                        break;
157                    default:
158                        log.error("Too many parts in CV name; taking 1st two {}", cv);
159                        valuePI = Integer.parseInt(splits[1]);
160                        valueSI = Integer.parseInt(splits[2]);
161                        _cv = splits[0];
162                        break;
163                }
164            } else {
165                String[] splits = cv.split("\\.");
166                switch (splits.length) {
167                    case 2:
168                        if (hasAlternateAddress(splits[0])) {
169                            valuePI = getAlternateValue(splits[0]);
170                            indexPI = getAlternateAddress(splits[0]);
171                        } else {
172                            valuePI = Integer.parseInt(splits[0]);
173                            indexPI = defaultIndexPI;
174                        }
175                        _cv = splits[1];
176                        break;
177                    case 3:
178                        if (hasAlternateAddress(splits[0])) {
179                            valuePI = getAlternateValue(splits[0]);
180                            indexPI = getAlternateAddress(splits[0]);
181                        } else {
182                            valuePI = Integer.parseInt(splits[0]);
183                            indexPI = defaultIndexPI;
184                        }
185                        if (hasAlternateAddress(splits[1])) {
186                            valueSI = getAlternateValue(splits[1]);
187                            indexSI = getAlternateAddress(splits[1]);
188                        } else {
189                            valueSI = Integer.parseInt(splits[1]);
190                            indexSI = defaultIndexSI;
191                        }
192                        _cv = splits[2];
193                        break;
194                    default:
195                        log.error("Too many parts in CV name; taking 1st two {}", cv);
196                        valuePI = Integer.parseInt(splits[0]);
197                        valueSI = Integer.parseInt(splits[1]);
198                        _cv = splits[2];
199                        break;
200                }
201            }
202        } else {
203            _cv = cv;
204        }
205    }
206
207    boolean hasAlternateAddress(String cv) {
208        return cv.contains("=");
209    }
210    
211    String getAlternateAddress(String cv) {
212        return cv.split("=")[0];
213    }
214    
215    int getAlternateValue(String cv) {
216        return Integer.parseInt(cv.split("=")[1]);
217    }
218    
219    /**
220     * Check if the last-written PI and SI values can still be counted on.
221     *
222     * @return true if last-written values are reliable; false otherwise
223     */
224    boolean useCachePiSi() {
225        return skipDupIndexWrite
226                && (lastValuePI == valuePI)
227                && (lastValueSI == valueSI)
228                && ((System.currentTimeMillis() - lastOpTime) < maxDelay);
229    }
230
231    // programming interface
232    @Override
233    synchronized public void writeCV(String CV, int val, jmri.ProgListener p) throws jmri.ProgrammerException {
234        _val = val;
235        useProgrammer(p);
236        parseCV(CV);
237        if (valuePI == -1) {
238            lastValuePI = -1;  // next indexed operation needs to write PI, SI
239            lastValueSI = -1;
240
241            // non-indexed operation
242            state = ProgState.PROGRAMMING;
243            prog.writeCV(_cv, val, this);
244        } else if (useCachePiSi()) {
245            // indexed operation with set values is same as non-indexed operation
246            state = ProgState.PROGRAMMING;
247            prog.writeCV(_cv, val, this);
248        } else {
249            lastValuePI = valuePI;  // after check in 'if' statement
250            lastValueSI = valueSI;
251
252            // write index first
253            state = ProgState.FINISHWRITE;
254            prog.writeCV(indexPI, valuePI, this);
255        }
256    }
257
258    @Override
259    synchronized public void readCV(String CV, jmri.ProgListener p) throws jmri.ProgrammerException {
260        readCV(CV, p, 0);
261    }
262
263    @Override
264    synchronized public void readCV(String CV, jmri.ProgListener p, int startVal) throws jmri.ProgrammerException {
265        useProgrammer(p);
266        parseCV(CV);
267        _startVal = startVal;
268        if (valuePI == -1) {
269            lastValuePI = -1;  // next indexed operation needs to write PI, SI
270            lastValueSI = -1;
271
272            state = ProgState.PROGRAMMING;
273            prog.readCV(_cv, this, _startVal);
274        } else if (useCachePiSi()) {
275            // indexed operation with set values is same as non-indexed operation
276            state = ProgState.PROGRAMMING;
277            prog.readCV(_cv, this, _startVal);
278        } else {
279            lastValuePI = valuePI;  // after check in 'if' statement
280            lastValueSI = valueSI;
281
282            // write index first
283            state = ProgState.FINISHREAD;
284            prog.writeCV(indexPI, valuePI, this);
285        }
286    }
287
288    @Override
289    synchronized public void confirmCV(String CV, int val, jmri.ProgListener p) throws jmri.ProgrammerException {
290        _val = val;
291        useProgrammer(p);
292        parseCV(CV);
293        if (valuePI == -1) {
294            lastValuePI = -1;  // next indexed operation needs to write PI, SI
295            lastValueSI = -1;
296
297            // non-indexed operation
298            state = ProgState.PROGRAMMING;
299            prog.confirmCV(_cv, val, this);
300        } else if (useCachePiSi()) {
301            // indexed operation with set values is same as non-indexed operation
302            state = ProgState.PROGRAMMING;
303            prog.confirmCV(_cv, val, this);
304        } else {
305            lastValuePI = valuePI;  // after check in 'if' statement
306            lastValueSI = valueSI;
307
308            // write index first
309            state = ProgState.FINISHCONFIRM;
310            prog.writeCV(indexPI, valuePI, this);
311        }
312    }
313
314    private jmri.ProgListener _usingProgrammer = null;
315
316    // internal method to remember who's using the programmer
317    protected void useProgrammer(jmri.ProgListener p) throws jmri.ProgrammerException {
318        // test for only one!
319        if (_usingProgrammer != null && _usingProgrammer != p) {
320            log.info("programmer already in use by {}", _usingProgrammer);
321            throw new jmri.ProgrammerException("programmer in use");
322        } else {
323            _usingProgrammer = p;
324        }
325    }
326
327    /**
328     * State machine for MultiIndexProgrammerFacade  (click to magnify):
329     * <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>
330     */
331    enum ProgState {
332        /** Waiting for response to (final) read or write operation, final reply next */
333        PROGRAMMING,
334        /** Waiting for response to first or second index write before a final read operation */
335        FINISHREAD,
336        /** Waiting for response to first or second index write before a final write operation */
337        FINISHWRITE,
338        /** Waiting for response to first or second index write before a final confirm operation */
339        FINISHCONFIRM,
340        /** No current operation */
341        NOTPROGRAMMING
342    }
343    ProgState state = ProgState.NOTPROGRAMMING;
344
345    // get notified of the final result
346    // Note this assumes that there's only one phase to the operation
347    @Override
348    public void programmingOpReply(int value, int status) {
349        log.debug("notifyProgListenerEnd value {} status {} ", value, status);
350
351        if (status != OK) {
352            // clear memory of last PI, SI written
353            lastValuePI = -1;
354            lastValueSI = -1;
355            lastOpTime = -1;
356
357            // pass abort up
358            log.debug("Reset and pass abort up");
359            jmri.ProgListener temp = _usingProgrammer;
360            _usingProgrammer = null; // done
361            state = ProgState.NOTPROGRAMMING;
362            temp.programmingOpReply(value, status);
363            return;
364        }
365
366        if (_usingProgrammer == null) {
367            log.error("No listener to notify, reset and ignore");
368            state = ProgState.NOTPROGRAMMING;
369            return;
370        }
371
372        switch (state) {
373            case PROGRAMMING:
374                // the programmingOpReply handler might send an immediate reply, so
375                // clear the current listener _first_
376                jmri.ProgListener temp = _usingProgrammer;
377                _usingProgrammer = null; // done
378                state = ProgState.NOTPROGRAMMING;
379                lastOpTime = System.currentTimeMillis();
380                temp.programmingOpReply(value, status);
381                break;
382            case FINISHREAD:
383                if (valueSI == -1) {
384                    try {
385                        state = ProgState.PROGRAMMING;
386                        prog.readCV(_cv, this, _startVal);
387                    } catch (jmri.ProgrammerException e) {
388                        log.error("Exception doing final read", e);
389                    }
390                } else {
391                    try {
392                        int tempSI = valueSI;
393                        valueSI = -1;
394                        state = ProgState.FINISHREAD;
395                        prog.writeCV(indexSI, tempSI, this);
396                    } catch (jmri.ProgrammerException e) {
397                        log.error("Exception doing write SI for read", e);
398                    }
399                }
400                break;
401            case FINISHWRITE:
402                if (valueSI == -1) {
403                    try {
404                        state = ProgState.PROGRAMMING;
405                        prog.writeCV(_cv, _val, this);
406                    } catch (jmri.ProgrammerException e) {
407                        log.error("Exception doing final write", e);
408                    }
409                } else {
410                    try {
411                        int tempSI = valueSI;
412                        valueSI = -1;
413                        state = ProgState.FINISHWRITE;
414                        prog.writeCV(indexSI, tempSI, this);
415                    } catch (jmri.ProgrammerException e) {
416                        log.error("Exception doing write SI for write", e);
417                    }
418                }
419                break;
420            case FINISHCONFIRM:
421                if (valueSI == -1) {
422                    try {
423                        state = ProgState.PROGRAMMING;
424                        prog.confirmCV(_cv, _val, this);
425                    } catch (jmri.ProgrammerException e) {
426                        log.error("Exception doing final confirm", e);
427                    }
428                } else {
429                    try {
430                        int tempSI = valueSI;
431                        valueSI = -1;
432                        state = ProgState.FINISHCONFIRM;
433                        prog.writeCV(indexSI, tempSI, this);
434                    } catch (jmri.ProgrammerException e) {
435                        log.error("Exception doing write SI for write", e);
436                    }
437                }
438                break;
439            default:
440                log.error("Unexpected state on reply: {}", state);
441                // clean up as much as possible
442                _usingProgrammer = null;
443                state = ProgState.NOTPROGRAMMING;
444                lastValuePI = -1;
445                lastValueSI = -1;
446                lastOpTime = -1;
447
448        }
449    }
450
451    private final static Logger log = LoggerFactory.getLogger(MultiIndexProgrammerFacade.class);
452
453}