001package jmri.jmrix.loconet;
002
003import java.awt.event.ActionEvent;
004import java.util.ArrayList;
005import java.util.List;
006
007import javax.annotation.Nonnull;
008
009import jmri.*;
010import jmri.beans.PropertyChangeSupport;
011import jmri.jmrix.ConnectionConfig;
012import jmri.jmrix.ConnectionConfigManager;
013import jmri.jmrix.loconet.hexfile.HexFileFrame;
014import jmri.jmrix.loconet.lnsvf1.Lnsv1MessageContents;
015import jmri.jmrix.loconet.lnsvf2.Lnsv2MessageContents;
016import jmri.jmrix.loconet.uhlenbrock.LncvMessageContents;
017
018/**
019 * Provide an Ops Mode Programmer via a wrapper that works with the LocoNet
020 * SlotManager object.
021 * Specific handling for message formats:
022 * <ul>
023 * <li>LOCONETOPSBOARD</li>
024 * <li>LOCONETSV1MODE</li>
025 * <li>LOCONETSV2MODE</li>
026 * <li>LOCONETLNCVMODE</li>
027 * <li>LOCONETBDOPSWMODE</li>
028 * <li>LOCONETBD7OPSWMODE</li>
029 * <li>LOCONETCSOPSWMODE</li>
030 * </ul>
031 * as defined in {@link LnProgrammerManager}
032 *
033 * Note that running a simulated LocoNet connection, {@link HexFileFrame#configure()} will substitute the
034 * {@link jmri.progdebugger.ProgDebugger} for the {@link jmri.jmrix.loconet.LnOpsModeProgrammer},
035 * overriding {@link #readCV(String, ProgListener)} and {@link #writeCV(String, int, ProgListener)}.
036 *
037 * @see jmri.Programmer
038 * @author Bob Jacobsen Copyright (C) 2002
039 * @author B. Milhaupt, Copyright (C) 2018
040 * @author Egbert Broerse, Copyright (C) 2020
041 */
042public class LnOpsModeProgrammer extends PropertyChangeSupport implements AddressedProgrammer, LocoNetListener {
043
044    LocoNetSystemConnectionMemo memo;
045    int mAddress;
046    boolean mLongAddr;
047    ProgListener p;
048    boolean doingWrite;
049    boolean boardOpSwWriteVal;
050    private int artNum;
051    private javax.swing.Timer bdOpSwAccessTimer = null;
052    private javax.swing.Timer sv2AccessTimer = null;
053    private javax.swing.Timer lncvAccessTimer = null;
054    private boolean firstReply;
055
056    private boolean csIsPresent;
057
058    public LnOpsModeProgrammer(LocoNetSystemConnectionMemo memo,
059            int pAddress, boolean pLongAddr) {
060        this.memo = memo;
061        mAddress = pAddress;
062        mLongAddr = pLongAddr;
063        // register to listen
064        memo.getLnTrafficController().addLocoNetListener(~0, this);
065        expectCsB4();
066    }
067    
068    private void expectCsB4() {
069        csIsPresent = true; // assumption...
070
071        if (memo.getSlotManager().getCommandStationType() ==
072                LnCommandStationType.COMMAND_STATION_STANDALONE) {
073            csIsPresent = false;
074            return;
075        }
076        ConnectionConfig[] connection = {null, null, null, null};
077        int i = 0;
078        for (ConnectionConfig conn : InstanceManager.getDefault(ConnectionConfigManager.class)) {
079            if (!conn.getDisabled()) {
080                connection[i] = conn;
081            }
082            break;
083        }
084        
085        if ((csIsPresent) && (connection[0] != null)) {
086            if (connection[0].name().equalsIgnoreCase("LocoNet Simulator")) {
087                csIsPresent = false;
088            }
089        }
090    }
091
092    /**
093     * {@inheritDoc}
094     */
095    @Override
096    public void writeCV(String CV, int val, ProgListener pL) throws ProgrammerException {
097        if (p != null) {
098            log.error("Will try to null an existing programmer!");
099        }
100        p = null;
101        // Check mode
102        LocoNetMessage m;
103        if (getMode().equals(LnProgrammerManager.LOCONETCSOPSWMODE)) {
104            memo.getSlotManager().setMode(LnProgrammerManager.LOCONETCSOPSWMODE);
105            memo.getSlotManager().writeCV(CV, val, pL); // deal with this via service-mode programmer
106        } else if (getMode().equals(LnProgrammerManager.LOCONETBDOPSWMODE)) {
107            /*
108             * CV format is e.g. "113.12" where the first part defines the
109             * typeword for the specific board type and the second is the specific bit number
110             * Known values:
111             * <ul>
112             * <li>0x70 112 - PM4
113             * <li>0x71 113 - BDL16
114             * <li>0x72 114 - SE8
115             * <li>0x73 115 - DS64
116             * </ul>
117             */
118            if (bdOpSwAccessTimer == null) {
119                initializeBdOpsAccessTimer();
120            }
121            p = pL;
122            doingWrite = true;
123            // Board programming mode
124            log.debug("write CV \"{}\" to {} addr:{}", CV, val, mAddress);
125            String[] parts = CV.split("\\.");
126            int typeWord = Integer.parseInt(parts[0]);
127            int state = Integer.parseInt(parts[parts.length>1 ? 1 : 0]);
128
129            // make message
130            m = new LocoNetMessage(6);
131            m.setOpCode(LnConstants.OPC_MULTI_SENSE);
132            int element = 0x72;
133            if ((mAddress & 0x80) != 0) {
134                element |= 1;
135            }
136            m.setElement(1, element);
137            m.setElement(2, (mAddress-1) & 0x7F);
138            m.setElement(3, typeWord);
139            int loc = (state - 1) / 8;
140            int bit = (state - 1) - loc * 8;
141            m.setElement(4, loc * 16 + bit * 2  + (val&0x01));
142
143            // save a copy of the written value low bit for use during reply
144            boardOpSwWriteVal = ((val & 0x01) == 1);
145
146            log.debug("  Message {}", m);
147            memo.getLnTrafficController().sendLocoNetMessage(m);
148            bdOpSwAccessTimer.restart();
149
150        } else if (getMode().equals(LnProgrammerManager.LOCONETBD7OPSWMODE)) {
151            /*
152             * Normal CV format for Digitrax 7th-gen Accy devices
153             */
154            if (bdOpSwAccessTimer == null) {
155                initializeBdOpsAccessTimer();
156            }
157            p = pL;
158            doingWrite = true;
159            // Board programming mode
160            log.debug("write CV \"{}\" to {} addr:{}", CV, val, mAddress);
161
162            // get prefix if any
163            String[] parts = CV.split("\\.");
164            int offset = 0;
165            int cv = 0;
166            switch (parts.length) {
167                case 1: // plain CV number
168                    cv = Integer.parseInt(parts[0])-1;
169                    break;
170                case 2:  //  offset.CV format
171                    offset = Integer.parseInt(parts[0]);
172                    cv = Integer.parseInt(parts[1])-1;
173                    break;
174                default:
175                    log.error("unexpected number of parts in CV {}", CV);
176            }
177
178            int address6th = ((mAddress-1+offset) >> 2) & 0x3F;
179            int upper3 = ~(((mAddress-1+offset) >> 8) & 0x07);
180            int lower2 = (mAddress-1+offset) & 0x03;
181            int address7th = ((upper3 << 4) & 0x70) | (lower2 << 1) | 0x08;
182
183            // make message - send immediate packet with custom content
184            m = new LocoNetMessage(11);
185            m.setOpCode(0xED);
186            m.setElement(1, 0x0B);
187            m.setElement(2, 0x7F);
188            m.setElement(3, 0x54);
189            m.setElement(4, 0x07 | (((val >> 7) & 0x01)<<4));
190            m.setElement(5, address6th);
191            m.setElement(6, address7th);
192            m.setElement(7, 0x6C | ((cv >> 7) & 0x03));
193            m.setElement(8, cv&0x7F);  // CV number
194            m.setElement(9, val&0x7F); // Data
195
196            // save a copy of the written value low bit for use during reply
197            boardOpSwWriteVal = ((val & 0x01) == 1);
198
199            log.debug("  Message {}", m);
200            firstReply = true;
201            memo.getLnTrafficController().sendLocoNetMessage(m);
202            bdOpSwAccessTimer.restart();
203
204        } else if (getMode().equals(LnProgrammerManager.LOCONETSV1MODE)) {
205            // LocoIO family
206            p = pL;
207            doingWrite = true;
208            // SV1 mode
209            log.debug("write CV \"{}\" to {} addr:{}", CV, val, mAddress);
210            // make message
211            int locoIOAddress = mAddress & 0x7F;
212            int locoIOSubAddress = ((mAddress+256)/256) & 0x7F;
213            m = Lnsv1MessageContents.createSv1WriteRequest(locoIOAddress, locoIOSubAddress, decodeCvNum(CV), val);
214            log.debug(" LNSV1 Message {}", m);
215            memo.getLnTrafficController().sendLocoNetMessage(m);
216
217        } else if (getMode().equals(LnProgrammerManager.LOCONETSV2MODE)) {
218            if (sv2AccessTimer == null) {
219                initializeSV2AccessTimer();
220            }
221            p = pL;
222            // SV2 mode
223            log.debug("write CV \"{}\" to {} addr:{}", CV, val, mAddress);
224            // make message
225            m = new LocoNetMessage(16);
226            loadSV2MessageFormat(m, mAddress, decodeCvNum(CV), val);
227            m.setElement(3, 0x01); // 1 byte write
228            log.debug(" LNSV2 Message {}", m);
229            memo.getLnTrafficController().sendLocoNetMessage(m);
230            sv2AccessTimer.restart();
231        } else if (getMode().equals(LnProgrammerManager.LOCONETLNCVMODE)) {
232            if (lncvAccessTimer == null) {
233                initializeLncvAccessTimer();
234            }
235            /*
236             * CV format is e.g. "5033.12" where the first part defines the
237             * article number (type/module class) for the board and the second is the specific bit number.
238             * Modules without their own art. no. use 65535 (broadcast mode).
239             */
240            // LNCV Module programming mode
241            String[] parts = CV.split("\\.");
242            if (parts.length > 1) {
243                artNum = Integer.parseInt(parts[0]); // stored for comparison
244            }
245            int cvNum = Integer.parseInt(parts[parts.length > 1 ? 1 : 0]);
246            p = pL;
247            doingWrite = true;
248            // LNCV mode
249            log.debug("write CV \"{}\" to {} addr:{} (art. {})", cvNum, val, mAddress, artNum);
250            // make message
251            m = LncvMessageContents.createCvWriteRequest(artNum, cvNum, val);
252            // module must be in Programming mode (handled by LNCV tool), note that mAddress is not included in LNCV Write message
253            log.debug(" LNCV Message {}", m);
254            memo.getLnTrafficController().sendLocoNetMessage(m);
255            lncvAccessTimer.restart();
256        } else {
257            // LOCONETOPSBOARD decoder i.e. getMode().equals(LnProgrammerManager.LOCONETOPSBOARD)
258            // and the remaining case of DCC ops mode
259            memo.getSlotManager().setAcceptAnyLACK();
260            memo.getSlotManager().writeCVOpsMode(CV, val, pL, mAddress, mLongAddr);
261        }
262    }
263
264    /**
265     * {@inheritDoc}
266     * @param CV the CV to read, could be a composite string that is split in this method te pass e.g. the module type
267     * @param pL  the listener that will be notified of the read
268     */
269    @Override
270    public void readCV(String CV, ProgListener pL) throws ProgrammerException {
271        if (this.p != null) {
272            log.error("Will try to null an existing programmer!");
273        }
274        this.p = null;
275        // Check mode
276        String[] parts;
277        LocoNetMessage m;
278        if (getMode().equals(LnProgrammerManager.LOCONETCSOPSWMODE)) {
279            memo.getSlotManager().setMode(LnProgrammerManager.LOCONETCSOPSWMODE);
280            memo.getSlotManager().readCV(CV, pL); // deal with this via service-mode programmer
281        } else if (getMode().equals(LnProgrammerManager.LOCONETBDOPSWMODE)) {
282            /*
283             * CV format is e.g. "113.12" where the first part defines the
284             * typeword for the specific board type and the second is the specific bit number
285             * Known values:
286             * <ul>
287             * <li>0x70 112 - PM4
288             * <li>0x71 113 - BDL16
289             * <li>0x72 114 - SE8
290             * <li>0x73 115 - DS64
291             * </ul>
292             */
293            if (bdOpSwAccessTimer == null) {
294                initializeBdOpsAccessTimer();
295            }
296            p = pL;
297            doingWrite = false;
298            // Board programming mode
299            log.debug("read CV \"{}\" addr:{}", CV, mAddress);
300            parts = CV.split("\\.");
301            int typeWord = Integer.parseInt(parts[0]);
302            int state = Integer.parseInt(parts[parts.length>1 ? 1 : 0]);
303
304            // make message
305            m = new LocoNetMessage(6);
306            m.setOpCode(LnConstants.OPC_MULTI_SENSE);
307            int element = 0x62;
308            if ((mAddress & 0x80) != 0) {
309                element |= 1;
310            }
311            m.setElement(1, element);
312            m.setElement(2, (mAddress-1) & 0x7F);
313            m.setElement(3, typeWord);
314            int loc = (state - 1) / 8;
315            int bit = (state - 1) - loc * 8;
316            m.setElement(4, loc * 16 + bit * 2);
317
318            log.debug("  Message {}", m);
319            memo.getLnTrafficController().sendLocoNetMessage(m);
320            bdOpSwAccessTimer.restart();
321
322        } else if (getMode().equals(LnProgrammerManager.LOCONETBD7OPSWMODE)) {
323            /*
324             * Normal CV format
325             */
326            if (bdOpSwAccessTimer == null) {
327                initializeBdOpsAccessTimer();
328            }
329            p = pL;
330            doingWrite = false;
331            // Board programming mode
332            log.debug("read CV \"{}\" addr:{}", CV, mAddress);
333
334            // get prefix if any
335            parts = CV.split("\\.");
336            int offset = 0;
337            int cv = 0;
338            switch (parts.length) {
339                case 1: // plain CV number
340                    cv = Integer.parseInt(parts[0])-1;
341                    break;
342                case 2:  //  offset.CV format
343                    offset = Integer.parseInt(parts[0]);
344                    cv = Integer.parseInt(parts[1])-1;
345                    break;
346                default:
347                    log.error("unexpected number of parts in CV {}", CV);
348            }
349
350            int address6th = ((mAddress-1+offset) >> 2) & 0x3F;
351            int upper3 = ~(((mAddress-1+offset) >> 8) & 0x07);
352            int lower2 = (mAddress-1+offset) & 0x03;
353            int address7th = ((upper3 << 4) & 0x70) | (lower2 << 1) | 0x08;
354
355            // make message - send immediate packet with custom content
356            m = new LocoNetMessage(11);
357            m.setOpCode(0xED);
358            m.setElement(1, 0x0B);
359            m.setElement(2, 0x7F);
360            m.setElement(3, 0x54);
361            m.setElement(4, 0x07);
362            m.setElement(5, address6th);
363            m.setElement(6, address7th);
364            m.setElement(7, 0x64 | ((cv >> 7) & 0x03));
365            m.setElement(8, cv&0x7F);  // CV number
366            m.setElement(9, 0);
367
368            log.debug("  Message {}", m);
369            firstReply = true;
370            memo.getLnTrafficController().sendLocoNetMessage(m);
371            bdOpSwAccessTimer.restart();
372        } else if (getMode().equals(LnProgrammerManager.LOCONETSV1MODE)) {
373            // LocoIO family
374            p = pL;
375            doingWrite = false;
376            // SV1 mode
377            log.debug("read CV \"{}\" addr:{}", CV, mAddress);
378            // make message
379            int locoIOAddress = mAddress & 0xFF;
380            int locoIOSubAddress = ((mAddress+256)/256) & 0x7F;
381            m = Lnsv1MessageContents.createSv1ReadRequest(locoIOAddress, locoIOSubAddress, decodeCvNum(CV));
382            log.debug(" LNSV1 Message {}", m);
383            memo.getLnTrafficController().sendLocoNetMessage(m);
384
385        } else if (getMode().equals(LnProgrammerManager.LOCONETSV2MODE)) {
386            if (sv2AccessTimer == null) {
387                initializeSV2AccessTimer();
388            }
389            p = pL;
390            // SV2 mode
391            log.debug("read CV \"{}\" addr:{}", CV, mAddress);
392            // make message
393            m = new LocoNetMessage(16);
394            loadSV2MessageFormat(m, mAddress, decodeCvNum(CV), 0);
395            m.setElement(3, 0x02); // 1 byte read
396            log.debug(" LNSV2 Message {}", m);
397            memo.getLnTrafficController().sendLocoNetMessage(m);
398            sv2AccessTimer.restart();
399        } else if (getMode().equals(LnProgrammerManager.LOCONETLNCVMODE)) {
400            if (lncvAccessTimer == null) {
401                initializeLncvAccessTimer();
402            }
403            /*
404             * CV format passed by SymbolicProg is formed "5033.12", where the first part defines the
405             * article number (type/module class) for the board and the second is the specific bit number.
406             * Modules without their own art. no. use 65535 (broadcast mode), so cannot use decoder definition.
407             */
408            parts = CV.split("\\.");
409            if (parts.length > 1) {
410                artNum = Integer.parseInt(parts[0]); // stored for comparison
411            }
412            int cvNum = Integer.parseInt(parts[parts.length > 1 ? 1 : 0]);
413            doingWrite = false;
414            // numberformat "113.12" is simply consumed by ProgDebugger (HexFile sim connection)
415            p = pL;
416            // LNCV mode
417            log.debug("read LNCV \"{}\" addr:{}", CV, mAddress);
418            // make message
419            m = LncvMessageContents.createCvReadRequest(artNum, mAddress, cvNum); // module must be in Programming mode (is handled by LNCV tool)
420            log.debug(" LNCV Message {}", m);
421            memo.getLnTrafficController().sendLocoNetMessage(m);
422            lncvAccessTimer.restart();
423        } else if (getMode().equals(LnProgrammerManager.LOCONETOPSBOARD)) {
424            // LOCONETOPSBOARD decoder
425            log.trace("LOCONETOPSBOARD start operation");
426            memo.getSlotManager().setAcceptAnyLACK();
427            memo.getSlotManager().readCVOpsMode(CV, pL, mAddress, mLongAddr);
428        } else {
429            // DCC ops mode
430            memo.getSlotManager().readCVOpsMode(CV, pL, mAddress, mLongAddr);
431        }
432    }
433
434    /**
435     * {@inheritDoc}
436     */
437    @Override
438    public void confirmCV(String CV, int val, ProgListener pL) throws ProgrammerException {
439        if (this.p != null) {
440            log.error("Will try to null an existing programmer!");
441        }
442        p = null;
443        // Check mode
444        if (getMode().equals(LnProgrammerManager.LOCONETCSOPSWMODE)) {
445            memo.getSlotManager().setMode(LnProgrammerManager.LOCONETCSOPSWMODE);
446            memo.getSlotManager().readCV(CV, pL); // deal with this via service-mode programmer
447        } else if (getMode().equals(LnProgrammerManager.LOCONETBDOPSWMODE)) {
448            readCV(CV, pL);
449        } else if (getMode().equals(LnProgrammerManager.LOCONETBD7OPSWMODE)) {
450            readCV(CV, pL);
451        } else if (getMode().equals(LnProgrammerManager.LOCONETSV2MODE)) {
452            // SV2 mode
453            log.warn("confirm CV \"{}\" addr:{} in SV2 mode not implemented", CV, mAddress);
454            notifyProgListenerEnd(pL, 0, ProgListener.UnknownError);
455        } else if (getMode().equals(LnProgrammerManager.LOCONETLNCVMODE)) {
456            // LNCV (Uhlenbrock) mode
457            log.warn("confirm CV \"{}\" addr:{} in LNCV mode not (yet) implemented", CV, mAddress);
458            readCV(CV, pL);
459            //notifyProgListenerEnd(pL, 0, ProgListener.UnknownError);
460        } else if (getMode().equals(LnProgrammerManager.LOCONETOPSBOARD)) {
461            // LOCONETOPSBOARD decoder
462            memo.getSlotManager().setAcceptAnyLACK();
463            memo.getSlotManager().confirmCVOpsMode(CV, val, pL, mAddress, mLongAddr);
464        } else {
465            // DCC ops mode
466            memo.getSlotManager().confirmCVOpsMode(CV, val, pL, mAddress, mLongAddr);
467        }
468    }
469
470    /**
471     * {@inheritDoc}
472     */
473    @Override
474    public void message(LocoNetMessage m) {
475        if (getMode().equals(LnProgrammerManager.LOCONETBDOPSWMODE)) {
476            // are we programming? If not, ignore
477            if (p == null) {
478                log.warn("received board-program reply message with no reply object: {}", m);
479                return;
480            }
481            // check for right type, unit
482            if (m.getOpCode() != LnConstants.OPC_LONG_ACK
483                    || ((m.getElement(1) != 0x00) && (m.getElement(1) != 0x50))) {
484                return;
485            }
486            // got a message that is LONG_ACK reply to an BdOpsSw access
487            bdOpSwAccessTimer.stop();    // kill the timeout timer
488            // LACK with 0x00 or 0x50 in byte 1; assume it's to us
489            if (doingWrite) {
490                int code = ProgListener.OK;
491                int val = (boardOpSwWriteVal ? 1 : 0);
492                ProgListener temp = p;
493                p = null;
494                notifyProgListenerEnd(temp, val, code);
495                return;
496            }
497
498            int val = 0;
499            if ((m.getElement(2) & 0x20) != 0) {
500                val = 1;
501            }
502
503            // successful read if LACK return status is not 0x7F
504            int code = ProgListener.OK;
505            if ((m.getElement(2) == 0x7f)) {
506                code = ProgListener.UnknownError;
507            }
508
509            ProgListener temp = p;
510            p = null;
511            notifyProgListenerEnd(temp, val, code);
512
513        } else if (getMode().equals(LnProgrammerManager.LOCONETBD7OPSWMODE)) {
514            // Deal with Digitrax 7th-gen Accessory board CV accesses
515            // Are we programming? If not, ignore
516            if (p == null) {
517                log.warn("7th-gen Accessory board Ops programmer received reply message with no reply object: {}", m);
518                return;
519            }
520            // check for right type, unit
521            // ignore if not Long_ack
522            if ((m.getOpCode() != LnConstants.OPC_LONG_ACK)){
523                return;
524            }
525            if (!((m.getElement(1) == 0x6E) ||
526                   ( m.getElement(1) == 0x6D))) {
527                // ignore if not Long_ack or either of two appropriate
528                // Long-ack response types
529                log.debug("Ignoring OPC_LONG_ACK with <LOPC> {}, <ACK1> {}.",
530                        Integer.toHexString(m.getElement(1)),
531                        Integer.toHexString(m.getElement(2))
532                        );
533                return;
534            }
535
536            // is a OPC_LONG_ACK og 0x6D or 0x6E.
537
538            if (firstReply && csIsPresent) {
539                firstReply = false;
540                log.debug("Ignoring first OPC_LONG_ACK from 7th gen access. access from cs.");
541                return;
542            }
543
544            // got a message that is LONG_ACK reply to a 7th-gen BdOpsSw access
545            bdOpSwAccessTimer.stop();    // kill the timeout timer
546            int code; // redundant = ProgListener.UnknownError;
547            int val;
548
549            // LACK with 0x6E in byte 1; assume it's to us
550            if (doingWrite
551                    && m.getElement(1) == 0x6D
552                    && (m.getElement(2) == 0x55 || m.getElement(2) == 0x5A)) {
553                code = ProgListener.OK;
554                val = (boardOpSwWriteVal ? 1 : 0);
555            } else {
556                code = ProgListener.OK;
557                val = m.getElement(2) + ((m.getElement(1) & 1) << 7);
558            }
559
560            scheduleReplyForLater(val, code);
561
562        } else if (getMode().equals(LnProgrammerManager.LOCONETSV1MODE)) {
563            // see if reply to LNSV1 or LNSV2 request
564            if ((m.getOpCode() != LnConstants.OPC_PEER_XFER) ||
565                    (m.getElement( 1) != 0x10) ||
566                    (m.getElement( 4) != 0x01) || // format 1
567                    ((m.getElement( 5) & 0x70) != 0x00)) {
568                return;
569            }
570
571            // check for src address (?) moved to 0x50
572            // this might not be the right way to tell....
573            if ((m.getElement(3) & 0x7F) != 0x50) { // to LocoBuffer
574                return;
575            }
576
577            // more checks needed? E.g. addresses?
578
579            // Mode 1 return data comes back in
580            // byte index 12, with the MSB in 0x01 of byte index 10
581            //
582
583            // check pending activity
584            if (p == null) {
585                log.debug("received SV reply message with no reply object: {}", m);
586            } else {
587                log.debug("returning SV programming reply: {}", m);
588                int code = ProgListener.OK;
589                int val;
590                if (doingWrite) {
591                    val = m.getPeerXfrData()[7];
592                } else {
593                    val = m.getPeerXfrData()[5];
594                }
595                ProgListener temp = p;
596                p = null;
597                notifyProgListenerEnd(temp, val, code);
598            }
599        } else if (getMode().equals(LnProgrammerManager.LOCONETSV2MODE)) {
600            // see if reply to LNSV 1 or LNSV2 request
601            if (((m.getOpCode() & 0xFF) != LnConstants.OPC_PEER_XFER) ||
602                    ((m.getElement( 1) & 0xFF) != 0x10) ||
603                    ((m.getElement( 3) != 0x41) && (m.getElement(3) != 0x42)) || // need a "Write One Reply", or a "Read One Reply"
604                    ((m.getElement( 4) & 0xFF) != 0x02) || // format 2)
605                    ((m.getElement( 5) & 0x70) != 0x10) || // need SVX1 high nibble = 1
606                    ((m.getElement(10) & 0x70) != 0x10) // need SVX2 high nibble = 1
607                    ) {
608                return;
609            }
610            // more checks needed? E.g. addresses?
611
612            // return reply
613            if (p == null) {
614                log.error("received SV reply message with no reply object: {}", m);
615            } else {
616                log.debug("returning SV programming reply: {}", m);
617
618                sv2AccessTimer.stop();    // kill the timeout timer
619
620                int code = ProgListener.OK;
621                int val = (m.getElement(11)&0x7F)|(((m.getElement(10)&0x01) != 0x00)? 0x80:0x00);
622
623                ProgListener temp = p;
624                p = null;
625                notifyProgListenerEnd(temp, val, code);
626            }
627        } else if (getMode().equals(LnProgrammerManager.LOCONETLNCVMODE)) {
628            // see if reply to LNCV request
629            // (compare this part to that in LNCV Tool jmri.jmrix.loconet.swing.lncvprog.LncvProgPane.message)
630            // is it a LACK write confirmation response from module?
631            int code;
632            if ((m.getOpCode() == LnConstants.OPC_LONG_ACK) &&
633                    (m.getElement(1) == 0x6D) && doingWrite) { // elem 1 = OPC (matches 0xED), elem 2 = ack1
634                // convert Uhlenbrock LNCV error codes to ProgListener codes, TODO extend that list to match?
635                switch (m.getElement(2)) {
636                    case 0x7f:
637                        code = ProgListener.OK;
638                        break;
639                    case 2:
640                    case 3:
641                        code = ProgListener.NotImplemented;
642                        break;
643                    case 1:
644                    default:
645                        code = ProgListener.UnknownError;
646                }
647                if (lncvAccessTimer != null) {
648                    lncvAccessTimer.stop(); // kill the timeout timer
649                }
650                // LACK with 0x00 or 0x50 in byte 1; assume it's to us.
651                ProgListener temp = p;
652                p = null;
653                notifyProgListenerEnd(temp, 0, code);
654            }
655            if ((LncvMessageContents.extractMessageType(m) == LncvMessageContents.LncvCommand.LNCV_READ_REPLY) ||
656            (LncvMessageContents.extractMessageType(m) == LncvMessageContents.LncvCommand.LNCV_READ_REPLY2)) {
657                // it's an LNCV ReadReply message, decode contents
658                LncvMessageContents contents = new LncvMessageContents(m);
659                int artReturned = contents.getLncvArticleNum();
660                int valReturned = contents.getCvValue();
661                code = ProgListener.OK;
662                // forward write reply
663                if (artReturned != artNum) { // it's not for us?
664                    //code = ProgListener.ConfirmFailed;
665                    log.warn("LNCV read reply received for article {}, expected article {}", artReturned, artNum);
666                }
667                if (lncvAccessTimer != null) {
668                    lncvAccessTimer.stop(); // kill the timeout timer
669                }
670                ProgListener temp = p;
671                p = null;
672                notifyProgListenerEnd(temp, valReturned, code);
673            }
674        }
675    }
676
677    private void scheduleReplyForLater(int val, int code) {
678
679        jmri.util.TimerUtil.scheduleOnLayoutThread(new java.util.TimerTask() {
680            @Override
681            public void run() {
682                log.debug("Passing result from 7g Accy Ops access.");
683                ProgListener tempProgListener = p;
684                p = null;
685                notifyProgListenerEnd(tempProgListener, val, code);
686            }
687        }, 50);
688    }
689
690    int decodeCvNum(String CV) {
691        try {
692            return Integer.parseInt(CV);
693        } catch (java.lang.NumberFormatException e) {
694            return 0;
695        }
696    }
697
698    /** Fill in an SV2 format LocoNet message from parameters provided.
699     * Compare to SV2 message handler in {@link Lnsv2MessageContents#createSv2Message(int, int, int, int, int, int, int, int)}
700     *
701     * @param m         Base LocoNet message to fill
702     * @param mAddress  Destination board address
703     * @param cvAddr    Dest. board CV number
704     * @param data      Value to put into CV
705     */
706    void loadSV2MessageFormat(LocoNetMessage m, int mAddress, int cvAddr, int data) {
707        m.setElement(0, LnConstants.OPC_PEER_XFER);
708        m.setElement(1, 0x10);
709        m.setElement(2, 0x01);
710        // 3 SV_CMD to be filled in later
711        m.setElement(4, 0x02);
712        // 5 will come back to SVX1
713        m.setElement(6, mAddress&0xFF);
714        m.setElement(7, (mAddress>>8)&0xFF);
715        m.setElement(8, cvAddr&0xFF);
716        m.setElement(9, (cvAddr/256)&0xFF);
717
718        // set SVX1
719        int svx1 = 0x10
720                    |((m.getElement(6)&0x80) != 0 ? 0x01 : 0)  // DST_L
721                    |((m.getElement(7)&0x80) != 0 ? 0x02 : 0)  // DST_L
722                    |((m.getElement(8)&0x80) != 0 ? 0x04 : 0)  // DST_L
723                    |((m.getElement(9)&0x80) != 0 ? 0x08 : 0); // SV_ADRH
724        m.setElement(5, svx1);
725        m.setElement(6, m.getElement(6)&0x7F);
726        m.setElement(7, m.getElement(7)&0x7F);
727        m.setElement(8, m.getElement(8)&0x7F);
728        m.setElement(9, m.getElement(9)&0x7F);
729
730        // 10 will come back to SVX2
731        m.setElement(11, data&0xFF);
732        m.setElement(12, (data>>8)&0xFF);
733        m.setElement(13, (data>>16)&0xFF);
734        m.setElement(14, (data>>24)&0xFF);
735
736        // set SVX2
737        int svx2 = 0x10
738                    |((m.getElement(11)&0x80) != 0 ? 0x01 : 0)
739                    |((m.getElement(12)&0x80) != 0 ? 0x02 : 0)
740                    |((m.getElement(13)&0x80) != 0 ? 0x04 : 0)
741                    |((m.getElement(14)&0x80) != 0 ? 0x08 : 0);
742        m.setElement(10, svx2);
743        m.setElement(11, m.getElement(11)&0x7F);
744        m.setElement(12, m.getElement(12)&0x7F);
745        m.setElement(13, m.getElement(13)&0x7F);
746        m.setElement(14, m.getElement(14)&0x7F);
747    }
748
749    // handle mode
750    protected ProgrammingMode mode = ProgrammingMode.OPSBYTEMODE;
751
752    /**
753     * {@inheritDoc}
754     */
755    @Override
756    public final void setMode(ProgrammingMode m) {
757        if (getSupportedModes().contains(m)) {
758            mode = m;
759            firePropertyChange("Mode", mode, m); // NOI18N
760        } else {
761            throw new IllegalArgumentException("Invalid requested mode: " + m); // NOI18N
762        }
763    }
764
765    /**
766     * {@inheritDoc}
767     */
768    @Override
769    public final ProgrammingMode getMode() {
770        return mode;
771    }
772
773    /**
774     * {@inheritDoc}
775     */
776    @Override
777    @Nonnull
778    public List<ProgrammingMode> getSupportedModes() {
779        List<ProgrammingMode> ret = new ArrayList<>(4);
780        ret.add(ProgrammingMode.OPSBYTEMODE);
781        ret.add(LnProgrammerManager.LOCONETBD7OPSWMODE);
782        ret.add(LnProgrammerManager.LOCONETOPSBOARD);
783        ret.add(LnProgrammerManager.LOCONETSV1MODE);
784        ret.add(LnProgrammerManager.LOCONETSV2MODE);
785        ret.add(LnProgrammerManager.LOCONETLNCVMODE);
786        ret.add(LnProgrammerManager.LOCONETBDOPSWMODE);
787        ret.add(LnProgrammerManager.LOCONETCSOPSWMODE);
788        return ret;
789    }
790
791    /**
792     * {@inheritDoc}
793     *
794     * Confirmation mode by programming mode; not that this doesn't
795     * yet know whether BDL168 hardware is present to allow DecoderReply
796     * to function; that should be a preference eventually. See also DCS240...
797     *
798     * @param addr CV address ignored, as there's no variance with this in LocoNet
799     * @return depends on programming mode
800     */
801    @Nonnull
802    @Override
803    public Programmer.WriteConfirmMode getWriteConfirmMode(String addr) {
804        if (getMode().equals(ProgrammingMode.OPSBYTEMODE)) {
805            return WriteConfirmMode.NotVerified;
806        }
807        return WriteConfirmMode.DecoderReply;
808    }
809
810    /**
811     * {@inheritDoc}
812     *
813     * Can this ops-mode programmer read back values? Yes, if transponding
814     * hardware is present and regular ops mode, or if in any other mode.
815     *
816     * @return always true
817     */
818    @Override
819    public boolean getCanRead() {
820        if (getMode().equals(ProgrammingMode.OPSBYTEMODE)) return memo.getSlotManager().getTranspondingAvailable(); // only way can be false
821        return true;
822     }
823
824    /**
825     * {@inheritDoc}
826     */
827    @Override
828    public boolean getCanRead(String addr) {
829        return getCanRead();
830    }
831
832    /**
833     * {@inheritDoc}
834     */
835    @Override
836    public boolean getCanWrite() {
837        return true;
838    }
839
840    /**
841     * {@inheritDoc}
842     */
843    @Override
844    public boolean getCanWrite(String addr) {
845        return getCanWrite() && Integer.parseInt(addr) <= 1024;
846    }
847
848    /**
849     * {@inheritDoc}
850     */
851    @Override
852    @Nonnull
853    public String decodeErrorCode(int i) {
854        return memo.getSlotManager().decodeErrorCode(i);
855    }
856
857    /**
858     * {@inheritDoc}
859     */
860    @Override
861    public boolean getLongAddress() {
862        return mLongAddr;
863    }
864
865    /**
866     * {@inheritDoc}
867     */
868    @Override
869    public int getAddressNumber() {
870        return mAddress;
871    }
872
873    /**
874     * {@inheritDoc}
875     */
876    @Override
877    public String getAddress() {
878        return getAddressNumber() + " " + getLongAddress();
879    }
880
881    void initializeBdOpsAccessTimer() {
882        if (bdOpSwAccessTimer == null) {
883            bdOpSwAccessTimer = new javax.swing.Timer(1000, (ActionEvent e) -> {
884                ProgListener temp = p;
885                p = null;
886                notifyProgListenerEnd(temp, 0, ProgListener.FailedTimeout);
887            });
888        bdOpSwAccessTimer.setInitialDelay(1000);
889        bdOpSwAccessTimer.setRepeats(false);
890        }
891    }
892
893    void initializeSV2AccessTimer() {
894       if (sv2AccessTimer == null) {
895            sv2AccessTimer = new javax.swing.Timer(1000, (ActionEvent e) -> {
896                ProgListener temp = p;
897                p = null;
898                notifyProgListenerEnd(temp, 0, ProgListener.FailedTimeout);
899            });
900        sv2AccessTimer.setInitialDelay(1000);
901        sv2AccessTimer.setRepeats(false);
902        }
903    }
904
905    void initializeLncvAccessTimer() {
906        if (lncvAccessTimer == null) {
907            lncvAccessTimer = new javax.swing.Timer(1000, (ActionEvent e) -> {
908                ProgListener temp = p;
909                p = null;
910                notifyProgListenerEnd(temp, 0, ProgListener.FailedTimeout);
911            });
912            lncvAccessTimer.setInitialDelay(1000);
913            lncvAccessTimer.setRepeats(false);
914        }
915    }
916    
917    @Override
918    public void dispose() {
919        memo.getLnTrafficController().removeLocoNetListener(~0, this);
920    }
921
922    // initialize logging
923    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LnOpsModeProgrammer.class);
924
925}