001package jmri.progdebugger;
002
003import java.util.Arrays;
004import java.util.Hashtable;
005import java.util.List;
006import javax.annotation.Nonnull;
007import jmri.AddressedProgrammer;
008import jmri.ProgListener;
009import jmri.Programmer;
010import jmri.ProgrammerException;
011import jmri.ProgrammingMode;
012import jmri.beans.PropertyChangeSupport;
013import jmri.jmrix.loconet.hexfile.HexFileFrame;
014import org.slf4j.Logger;
015import org.slf4j.LoggerFactory;
016
017/**
018 * Debugging implementation of Programmer interface.
019 * <p>
020 * Note that running a simulated LocoNet connection, {@link HexFileFrame#configure()} will substitute the
021 * {@link jmri.progdebugger.ProgDebugger} instead of the {@link jmri.jmrix.loconet.LnOpsModeProgrammer},
022 * overriding {@link #readCV(String, ProgListener)} and {@link #writeCV(String, int, ProgListener)}.
023 * <p>
024 * Remembers writes, and returns the last written value when a read to the same
025 * CV is made.
026 * <p>
027 * Only supports the DCC single-number address space, should be updated to handle
028 * any string address. As a temporary fix we simply discard the first part of any CV name
029 * containing "." and use the rest.
030 * TODO Fully support numberformat "113.12" in ProgDebugger (used in LOCONETLNCVMODE and LOCONETBDOPSWMODE)
031 *
032 * @author Bob Jacobsen Copyright (C) 2001, 2007, 2013
033 */
034public class ProgDebugger extends PropertyChangeSupport implements AddressedProgrammer {
035
036    public ProgDebugger() {
037        mode = ProgrammingMode.PAGEMODE;
038    }
039
040    public ProgDebugger(boolean pLongAddress, int pAddress) {
041        longAddr = pLongAddress;
042        address = pAddress;
043        mode = ProgrammingMode.OPSBITMODE;
044    }
045
046    // write CV is recorded for later use
047    private int _lastWriteVal = -1;
048    private int _lastWriteCv = -1;
049    //private String cvNumPrefix; // TODO use part 0 of composite CVnames for simulated replies?
050
051    public int lastWrite() {
052        return _lastWriteVal;
053    }
054
055    public int lastWriteCv() {
056        return _lastWriteCv;
057    }
058
059    public int nOperations = 0;
060
061    /**
062     * Reset the CV to a value so one can detect if it's been written.
063     * <p>
064     * Does not change the "lastWrite" and "lastWriteCv" results.
065     *
066     * @param cv  the CV to reset
067     * @param val the value
068     */
069    public void resetCv(int cv, int val) {
070        mValues.put(cv, val);
071    }
072
073    /**
074     * Get the CV value directly, without going through the usual indirect
075     * protocol. Used, for example, while testing.
076     * <p>
077     * Does not change the "lastRead" and "lastReadCv" results.
078     *
079     * @param cv the CV to get
080     * @return the value or -1
081     */
082    public int getCvVal(int cv) {
083        // try to get something from hash table
084        Integer saw = (mValues.get(cv));
085        if (saw != null) {
086            return saw;
087        }
088        log.warn("CV {} has no defined value", cv);
089        return -1;
090    }
091
092    /**
093     * See if a CV has been written.
094     *
095     * @param cv the CV to check
096     * @return true if written, false otherwise
097     */
098    public boolean hasBeenWritten(int cv) {
099        Integer saw = (mValues.get(cv));
100        return (saw != null);
101    }
102
103    /**
104     * Clear written status.
105     *
106     * @param cv the CV to clear
107     */
108    public void clearHasBeenWritten(int cv) {
109        mValues.remove(cv);
110    }
111
112    // write CV values are remembered for later reads
113    Hashtable<Integer, Integer> mValues = new Hashtable<>();
114
115    /**
116     * {@inheritDoc}
117     */
118    @Override
119    @Nonnull
120    public String decodeErrorCode(int i) {
121        log.debug("decoderErrorCode {}", i);
122        return "error " + i;
123    }
124
125    /**
126     * {@inheritDoc}
127     */
128    @Override
129    public void writeCV(String CVname, int val, ProgListener p) throws ProgrammerException {
130        final int CV;
131        // Check CVname contents for int parsing
132        if (CVname.contains(".")) {
133            String[] parts = CVname.split("\\.");
134            //cvNumPrefix = parts[0];
135            CVname = parts[1];
136            // in LocoNet LOCONETLNCVMODE and LOCONETBDOPSWMODE the CV value (string) eg. "25.2"
137            // contains a first part for Article ID/Board typeword. cvNumPrefix is discarded during debug/simulation.
138            // See jmri.jmrix.loconet.LnOpsModeProgrammer.writeCV()
139        }
140        CV = Integer.parseInt(CVname);
141        nOperations++;
142        final ProgListener m = p;
143        // log out the request
144        log.info("write CV: {} to: {} mode: {}", CV, val, getMode());
145        _lastWriteVal = val;
146        _lastWriteCv = CV;
147        // save for later retrieval
148        mValues.put(CV, val);
149
150        // return a notification via the queue to ensure end
151        Runnable r = new Runnable() {
152            ProgListener l = m;
153
154            @Override
155            public void run() {
156                log.debug("write CV reply");
157                if (l != null) {
158                    notifyProgListenerEnd(l, val, 0);
159                }
160            }  // 0 is OK status
161        };
162        sendReturn(r);
163    }
164
165    // read CV values
166    // note that the hashTable will be used if the CV has been written
167    private int _nextRead = 123;
168
169    public void nextRead(int r) {
170        _nextRead = r;
171    }
172
173    private int _lastReadCv = -1;
174
175    public int lastReadCv() {
176        return _lastReadCv;
177    }
178
179    boolean confirmOK;  // cached result of last compare
180
181    /**
182     * {@inheritDoc}
183     */
184    @Override
185    public void confirmCV(String CVname, int val, ProgListener p) throws ProgrammerException {
186        final int CV = Integer.parseInt(CVname);
187        final ProgListener m = p;
188
189        nOperations++;
190        // guess by comparing current value in val to has table
191        Integer saw = mValues.get(CV);
192        int result; // what was read
193        if (saw != null) {
194            result = saw;
195            confirmOK = (result == val);
196            log.info("confirm CV: {} mode: {} will return {} pass: {}", CV, getMode(), result, confirmOK);
197        } else {
198            result = -1;
199            confirmOK = false;
200            log.info("confirm CV: {} mode: {} will return -1 pass: false due to no previous value", CV, getMode());
201        }
202        _lastReadCv = CV;
203        // return a notification via the queue to ensure end
204        final int returnResult = result;  // final to allow passing to inner class
205        Runnable r = new Runnable() {
206            ProgListener l = m;
207            int res = returnResult;
208
209            @Override
210            public void run() {
211                log.debug("read CV reply");
212                if (confirmOK) {
213                    notifyProgListenerEnd(l, val, ProgListener.OK);
214                } else {
215                    notifyProgListenerEnd(l, res, ProgListener.ConfirmFailed);
216                }
217            }
218        };
219        sendReturn(r);
220
221    }
222
223    /**
224     * {@inheritDoc}
225     */
226    @Override
227    public void readCV(String CVname, ProgListener p) throws ProgrammerException {
228        final int CV;
229        // Check CVname contents for int parsing
230        if (CVname.contains(".")) {
231            String[] parts = CVname.split("\\.");
232            //cvNumPrefix = parts[0];
233            CVname = parts[1];
234            // in LocoNet LOCONETLNCVMODE and LOCONETBDOPSWMODE the CV value (string) e.g. "113.12"
235            // contains a first part for Article ID/Board typeword. cvNumPrefix is discarded during debug/simulation.
236            // See jmri.jmrix.loconet.LnOpsModeProgrammer.writeCV()
237        }
238        CV = Integer.parseInt(CVname);
239        final ProgListener m = p;
240        _lastReadCv = CV;
241        nOperations++;
242
243        int readValue = _nextRead;
244        // try to get something from hash table
245        Integer saw = mValues.get(CV);
246        if (saw != null) {
247            readValue = saw;
248        }
249
250        log.info("read CV: {} mode: {} will read {}", CV, getMode(), readValue);
251
252        final int returnValue = readValue;
253        // return a notification via the queue to ensure end
254        Runnable r = new Runnable() {
255            int retval = returnValue;
256            ProgListener l = m;
257
258            @Override
259            public void run() {
260                log.debug("read CV reply");
261                notifyProgListenerEnd(l, retval, 0);
262            }  // 0 is OK status
263        };
264        sendReturn(r);
265
266    }
267
268    // handle mode
269    protected ProgrammingMode mode;
270
271    /**
272     * {@inheritDoc}
273     */
274    @Override
275    public final void setMode(ProgrammingMode m) {
276        log.debug("Setting mode from {} to {}", mode, m);
277        if (getSupportedModes().contains(m)) {
278            ProgrammingMode oldMode = mode;
279            mode = m;
280            firePropertyChange("Mode", oldMode, m);
281        } else {
282            throw new IllegalArgumentException("Invalid requested mode: " + m);
283        }
284    }
285
286    /**
287     * {@inheritDoc}
288     */
289    @Override
290    public final ProgrammingMode getMode() {
291        return mode;
292    }
293
294    /**
295     * {@inheritDoc}
296     */
297    @Override
298    @Nonnull
299    public List<ProgrammingMode> getSupportedModes() {
300        if (address >= 0) {
301            // addressed programmer
302            return Arrays.asList(ProgrammingMode.OPSBITMODE,
303                    ProgrammingMode.OPSBYTEMODE);
304        } else {
305            // global programmer
306            return Arrays.asList(ProgrammingMode.PAGEMODE,
307                    ProgrammingMode.DIRECTBITMODE,
308                    ProgrammingMode.DIRECTBYTEMODE,
309                    ProgrammingMode.DIRECTMODE);
310        }
311    }
312    /**
313     * By default, the highest test CV is 256 so that we can test composite
314     * operations
315     */
316    int writeLimit = 256;
317    int readLimit = 256;
318
319    public void setTestReadLimit(int lim) {
320        readLimit = lim;
321    }
322
323    public void setTestWriteLimit(int lim) {
324        writeLimit = lim;
325    }
326
327    @Override
328    public boolean getCanRead() {
329        log.debug("getCanRead() returns true");
330        return true;
331    }
332
333    @Override
334    public boolean getCanRead(String addr) {
335        log.debug("getCanRead({}) returns {}", addr, Integer.parseInt(addr) <= readLimit);
336        return Integer.parseInt(addr) <= readLimit;
337    }
338
339    @Override
340    public boolean getCanWrite() {
341        log.debug("getCanWrite() returns true");
342        return true;
343    }
344
345    @Override
346    public boolean getCanWrite(String addr) {
347        log.debug("getCanWrite({}) returns {}", addr, Integer.parseInt(addr) <= writeLimit);
348        return Integer.parseInt(addr) <= writeLimit;
349    }
350
351    /**
352     * {@inheritDoc}
353     * <p>
354     * By default, say that no verification is done.
355     * @return Always WriteConfirmMode.NotVerified
356     */
357    @Nonnull
358    @Override
359    public Programmer.WriteConfirmMode getWriteConfirmMode(String addr) { return WriteConfirmMode.NotVerified; }
360
361    boolean longAddr = true;
362
363    @Override
364    public boolean getLongAddress() {
365        return true;
366    }
367
368    int address = -1;
369
370    @Override
371    public int getAddressNumber() {
372        return address;
373    }
374
375    @Override
376    public String getAddress() {
377        return "" + getAddressNumber() + " " + getLongAddress();
378    }
379
380    static final boolean IMMEDIATERETURN = false;
381    static final int DELAY = 10;
382
383    /**
384     * Arrange for the return to be invoked on the Swing thread.
385     *
386     * @param run the Runnable
387     */
388    void sendReturn(Runnable run) {
389        if (IMMEDIATERETURN) {
390            javax.swing.SwingUtilities.invokeLater(run);
391        } else {
392            javax.swing.Timer timer = new javax.swing.Timer(DELAY, null);
393            java.awt.event.ActionListener l = new java.awt.event.ActionListener() {
394                javax.swing.Timer myTimer;
395                Runnable runnable;
396
397                java.awt.event.ActionListener init(javax.swing.Timer t, Runnable r) {
398                    this.myTimer = t;
399                    this.runnable = r;
400                    return this;
401                }
402
403                @Override
404                public void actionPerformed(java.awt.event.ActionEvent e) {
405                    this.myTimer.stop();
406                    javax.swing.SwingUtilities.invokeLater(runnable);
407                }
408            }.init(timer, run);
409            timer.addActionListener(l);
410            timer.start();
411        }
412    }
413
414    private final static Logger log = LoggerFactory.getLogger(ProgDebugger.class);
415
416}