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