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