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