001package jmri.jmrix.loconet.locoio;
002
003import java.beans.PropertyChangeEvent;
004import java.beans.PropertyChangeListener;
005import jmri.beans.PropertyChangeSupport;
006import jmri.jmrix.loconet.LnConstants;
007import jmri.jmrix.loconet.LnTrafficController;
008import jmri.jmrix.loconet.LocoNetListener;
009import jmri.jmrix.loconet.LocoNetMessage;
010import org.slf4j.Logger;
011import org.slf4j.LoggerFactory;
012
013/**
014 * Data associated with a LocoIO device.
015 * 
016 * @author John Plocher, January 28, 2007
017 */
018public class LocoIOData extends PropertyChangeSupport
019        implements LocoNetListener, PropertyChangeListener {
020
021    private int sv0;
022    private int unitAddress;
023    private int unitSubAddress;
024    private LnTrafficController tc;
025
026    /**
027     * Define the number of rows in the table, which is also the number of
028     * "channels" in a single LocoIO unit.
029     */
030    private int _numRows = 16;
031    /**
032     * LocoBuffer always has address 0x01 0x50.
033     */
034    private static final int LocoBufferAddress = 0x0150;
035    private String locoBufferVersion = Bundle.getMessage("StateUnknown");
036    private String locoIOVersion = Bundle.getMessage("StateUnknown");
037    private String status = Bundle.getMessage("StateUnknown");  // LocoIO activity status
038    /**
039     * Per-port SV data.
040     */
041    private LocoIOMode[] lim = new LocoIOMode[_numRows];
042    private int[] addr = new int[_numRows];
043    private int[] sv = new int[_numRows];
044    private int[] v1 = new int[_numRows];
045    private int[] v2 = new int[_numRows];
046    private int[] readState = new int[_numRows];
047    private int[] writeState = new int[_numRows];
048
049    /**
050     * Record whether this pin is looking to capture a value from the LocoNet.
051     */
052    private boolean[] capture = new boolean[_numRows];
053    private String[] mode = new String[_numRows];
054
055    private LocoIOModeList validmodes;
056
057    /**
058     * Create a new instance of LocoIOData.
059     * @param unitAddr unit address.
060     * @param unitSubAddr unit SubAddress.
061     * @param tc system connection traffic controller.
062     */
063    public LocoIOData(int unitAddr, int unitSubAddr, LnTrafficController tc) {
064        timeoutcounter = 0;
065        unitAddress = unitAddr;
066        unitSubAddress = unitSubAddr;
067        validmodes = new LocoIOModeList();
068
069        for (int i = 0; i < _numRows; i++) {
070            setMode(i, "<none>"); // NOI18N
071            lim[i] = null;
072            setAddr(i, 0);
073            setSV(i, 0);
074            setV1(i, 0);
075            setV2(i, 0);
076            readState[i] = NONE;
077            writeState[i] = NONE;
078            capture[i] = false;
079        }
080        // addPropertyChangeListener(this);
081        this.tc = tc;
082        // for now, we're always listening to LocoNet
083        if (tc != null) {
084            tc.addLocoNetListener(~0, this);
085        } else {
086            log.error("No LocoNet interface available"); // NOI18N
087        }
088    }
089
090    @Override
091    public void propertyChange(PropertyChangeEvent evt) {
092        log.info("LocoIOData: {} := {} from {}", // NOI18N
093                evt.getPropertyName(), evt.getNewValue(), evt.getSource());
094    }
095
096    /**
097     * Address and SubAddress of this device.
098     * <p>
099     * High byte of the Address is fixed to 0x01
100     * <br>
101     * Low byte Address must be in the range of 0x01 .. 0x4F, 0x51 .. 0x7F
102     * <br>
103     * (0x50 is reserved for the LocoBuffer)
104     * <br>
105     * The subAddress is in the range of 0x01 .. 0x7E
106     * <br>
107     * (0x7F is reserved)
108     * @param unit unit address.
109     * @param unitSub unit subAddress.
110     */
111    public synchronized void setUnitAddress(int unit, int unitSub) {
112        setUnitAddress(unit);
113        setUnitSubAddress(unitSub);
114    }
115
116    public synchronized void setUnitAddress(int unit) {
117        firePropertyChange("UnitAddress", unitAddress, 0x0100 | (unit & 0x07F)); // NOI18N
118        unitAddress = 0x0100 | (unit & 0x07F);  // protect against high bits set
119    }
120
121    public synchronized void setUnitSubAddress(int unitSub) {
122        firePropertyChange("UnitSubAddress", unitSubAddress, unitSub & 0x07F); // NOI18N
123        unitSubAddress = unitSub & 0x07F;
124    }
125
126    public synchronized int getUnitAddress() {
127        return unitAddress & 0x07F;
128    }
129
130    public synchronized int getUnitSubAddress() {
131        return unitSubAddress & 0x07F;
132    }
133
134    /**
135     * No LocoIO Board level configuration.
136     * <pre>
137     * Bit 0: 0 = default, 1 = Port Refresh
138     * Bit 1: 0 = Fixed code PBs, 1 = Alternated code PBs
139     * Bit 2: 0 = default - Not used
140     * Bit 3: 0 = default, 1 = Ports 5-12 are Servo Ports
141     * Bit 4-7: Blink Rate
142     *
143     * Add/support the additional config options for HDL boards -
144     * Work has moved to xml decoder definition Public_Domain_HDL_LocoIO, is included there since 4.21.2
145     * </pre>
146     * @param portRefresh port refresh value, bit 0.
147     * @param altCodePBs alternated code PBs, bit 1.
148     * @param isServo servo port, bit 3.
149     * @param blinkRate blink rate, bits 4-7.
150     */
151    public void setUnitConfig(int portRefresh, int altCodePBs, int isServo, int blinkRate) {
152        int newsv0 = ((portRefresh & 0x01)) |   // bit 0
153                ((altCodePBs & 0x01) << 0x01) | // bit 1
154                // bit 2 is left at zero
155                // later LocoServo boards store 2 pos/4 pos type in bits 2-3, see Public_Domain_HDL_LocoIO
156                ((isServo & 0x01) << 0x03) |    // bit 3
157                ((blinkRate & 0x0F) << 0x04);   // bits 4-7
158        firePropertyChange("UnitConfig", sv0, newsv0); // NOI18N
159        sv0 = newsv0;
160    }
161
162    public int getUnitConfig() {
163        return sv0 & 0xFF;
164    }
165
166    public void setLBVersion(String version) {
167        locoBufferVersion = version;
168        firePropertyChange("LBVersionChange", "", locoBufferVersion); // NOI18N
169    }
170
171    public String getLBVersion() {
172        return locoBufferVersion;
173    }
174
175    public void setLIOVersion(String version) {
176        locoIOVersion = version;
177        firePropertyChange("LIOVersionChange", "", locoIOVersion); // NOI18N
178    }
179
180    public String getLIOVersion() {
181        return locoBufferVersion;
182    }
183
184    public void setStatus(String msg) {
185        status = msg;
186        firePropertyChange("StatusChange", "", status); // NOI18N
187    }
188
189    public String getStatus() {
190        return status;
191    }
192
193    public void setSV(int channel, int value) {
194        sv[channel] = value & 0xFF;
195        firePropertyChange("PortChange", -1, channel); // NOI18N
196    }
197
198    public int getSV(int channel) {
199        return sv[channel] & 0xFF;
200    }
201
202    public void setV1(int channel, LocoIOMode l, int address) {
203        setV1(channel, validmodes.addressToValue1(l, getAddr(channel)));
204    }
205
206    public void setV1(int channel, int value) {
207        v1[channel] = value & 0xFF;
208        firePropertyChange("PortChange", -1, channel); // NOI18N
209    }
210
211    public int getV1(int channel) {
212        return v1[channel] & 0xFF;
213    }
214
215    public void setV2(int channel, LocoIOMode l, int address) {
216        setV2(channel, validmodes.addressToValue2(l, getAddr(channel)));
217    }
218
219    public void setV2(int channel, int value) {
220        v2[channel] = value & 0xFF;
221        firePropertyChange("PortChange", -1, channel); // NOI18N
222    }
223
224    public int getV2(int channel) {
225        return v2[channel] & 0xFF;
226    }
227
228    /**
229     * Set new value in addr field (for the address info used in each LocoIO channel).
230     *
231     * @param channel integer value of the addresses in use for this row
232     *                (0 = invalid)
233     * @param value channel value.
234     */
235    public void setAddr(int channel, int value) {
236        addr[channel] = value & 0x7FF;
237        firePropertyChange("PortChange", -1, channel); // NOI18N
238    }
239
240    public int getAddr(int channel) {
241        return addr[channel] & 0x7FF;
242    }
243
244    public void setMode(int channel, String m) {
245        mode[channel] = m;
246        firePropertyChange("PortChange", -1, channel); // NOI18N
247    }
248
249    public String getMode(int channel) {
250        return mode[channel];
251    }
252
253    public void setLIM(int channel, String s) {
254        if (validmodes != null) {
255            setLIM(channel, validmodes.getLocoIOModeFor(s));
256        }
257    }
258
259    public void setLIM(int channel) {
260        if (validmodes != null) {
261            setLIM(channel, validmodes.getLocoIOModeFor(getSV(channel), getV1(channel), getV2(channel)));
262        }
263    }
264
265    public void setLIM(int channel, LocoIOMode m) {
266        lim[channel] = m;
267        firePropertyChange("PortChange", -1, channel); // NOI18N
268    }
269
270    public LocoIOMode getLIM(int channel) {
271        return lim[channel];
272    }
273
274    public void readValues(int channel) {
275        readState[channel] = READ;
276        issueNextOperation();
277    }
278
279    public void captureValues(int channel) {
280        capture[channel] = true;
281    }
282
283    public void writeValues(int channel) {
284        writeState[channel] = WRITE;
285        issueNextOperation();
286    }
287
288    /**
289     * Start reading all rows back.
290     */
291    public void readAll() {
292        for (int row = 0; row < _numRows; row++) {
293            readState[row] = READ;
294        }
295        issueNextOperation();
296    }
297
298    /**
299     * Start writing all rows out.
300     */
301    public void writeAll() {
302        for (int row = 0; row < _numRows; row++) {
303            writeState[row] = WRITE;
304        }
305        issueNextOperation();
306    }
307
308    public LocoIOModeList getLocoIOModeList() {
309        return validmodes;
310    }
311
312    /**
313     * Code for read activity needed.
314     * See states NONE, READMODE, READINGMODE, READVALUE1,
315     * READINGVALUE1, READVALUE2, READINGVALUE2
316     */
317    // int[] needRead = new int[_numRows];
318    protected final int NONE = 0;
319    protected final int READVALUE1 = 1;
320    protected final int READINGVALUE1 = 2;
321    protected final int READVALUE2 = 3;
322    protected final int READINGVALUE2 = 4;
323    protected final int READMODE = 5;
324    protected final int READINGMODE = 6;
325
326    protected final int READ = READVALUE1;  // starting state
327
328    /**
329     * Code for write activity needed. See states NONE, WRITEMODE, WRITINGMODE,
330     * WRITEVALUE1, WRITINGVALUE1, WRITEVALUE2, WRITINGVALUE2
331     */
332    // int[] needWrite = new int[_numRows];
333    protected final int WRITEVALUE1 = 11;
334    protected final int WRITINGVALUE1 = 12;
335    protected final int WRITEVALUE2 = 13;
336    protected final int WRITINGVALUE2 = 14;
337    protected final int WRITEMODE = 15;
338    protected final int WRITINGMODE = 16;
339
340    protected final int WRITE = WRITEVALUE1;  // starting state
341
342    private int lastOpCv = -1;
343    private boolean reading = false;  // false means write in progress
344    //private boolean writing = false;
345
346    protected int highPart(int value) { // generally value 1
347        return value / 256;
348    }
349
350    protected int lowPart(int value) { // generally value 2
351        return value - 256 * highPart(value);
352    }
353
354    private String dotme(int val) {
355        int dit;
356        int x = val;
357        StringBuffer ret = new StringBuffer();
358        if (val == 0) {
359            return "0"; // NOI18N
360        }
361        while (x != 0) {
362            dit = x % 10;
363            ret.insert(0, Integer.toString(dit));
364            x = x / 10;
365            if (x != 0) {
366                ret.insert(0, ".");
367            }
368        }
369        return ret.toString();
370    }
371
372    /**
373     * Listen to the LocoNet. We're listening for incoming OPC_PEER_XFR
374     * messages, which might be part of a read or write sequence. We're also
375     * _sometimes_ listening for commands as part of a "capture" operation.
376     * <p>
377     * The incoming LocoNet OPC_PEER_XFR messages don't retain any information
378     * about the CV number or whether it was a read or write operation. We store
379     * the data regardless of whether it was read or write, but we need to
380     * remember the cv number in the lastOpCv member.
381     *
382     * @param m Incoming message
383     */
384    @Override
385    public synchronized void message(LocoNetMessage m) {
386        // sort out the opCode
387        int opCode = m.getOpCode();
388        switch (opCode) {
389            case LnConstants.OPC_PEER_XFER:
390                // could be read or write operation
391                // check that src_low_address is our unit, and
392                // dst is "our" LocoBufferAddress
393                int src = m.getElement(2);
394                int dst = m.getElement(3) + m.getElement(4) * 256;
395                int[] packet = m.getPeerXfrData();
396
397                if (src == lowPart(LocoBufferAddress)) {
398                    String lbv = ((packet[2] != 0) ? dotme(packet[2]) : "1.0");
399                    setLBVersion(lbv);
400                }
401
402                if (dst == LocoBufferAddress && src == lowPart(unitAddress) && (packet[4] == unitSubAddress)) {
403                    // yes, we assume this is a reply to us
404                    stopTimer();
405                    replyReceived(); // advance state
406
407                    String fw = ((packet[2] != 0) ? dotme(packet[2]) : "1.3.2"); // NOI18N
408                    setLIOVersion(fw);
409                    if (packet[0] == LocoIO.LOCOIO_SV_READ || reading) {  // read command
410                        // get data and store
411                        if (lastOpCv >= 0 && lastOpCv <= 50) {
412
413                            // there are two formats of the return packet...
414                            int data = (packet[2] != 0) ? packet[5] : packet[7];
415                            int channel = (lastOpCv / 3) - 1;
416                            if (channel < 0) {
417                                log.warn("... channel is less than zero!!!"); // NOI18N
418                                channel = 0;
419                            }
420                            int type = lastOpCv - (channel * 3 + 3);
421                            // type = 0 for cv, 1 for value1, 2 for value2
422                            // We can't update the mode until we have all three values
423                            // Sequence (from state machine below) is V2, V1, Mode
424                            log.debug("... updating port {} SV{}({}) = 0x{}", channel, type, type == 1 ? "value1" // NOI18N
425                                    : type == 2 ? "value2" // NOI18N
426                                    : type == 0 ? "mode" // NOI18N
427                                    : "unknown", Integer.toHexString(data));
428                            if (type == 2) {            // v2
429                                setV2(channel, data);
430                                setMode(channel, "<none>"); // NOI18N
431                            } else if (type == 1) {     // v1
432                                setV1(channel, data);
433                                setMode(channel, "<none>"); // NOI18N
434                            } else if (type == 0) {     // cv
435                                setSV(channel, data);
436                                // Now that we have all the pieces, recalculate mode
437                                LocoIOMode lim = validmodes.getLocoIOModeFor(getSV(channel), getV1(channel), getV2(channel));
438                                if (lim == null) {
439                                    setMode(channel, "<none>"); // NOI18N
440                                    setAddr(channel, 0);
441                                    log.debug("Could not find mode!"); // NOI18N
442                                } else {
443                                    setMode(channel, lim.getFullMode());
444                                    setAddr(channel, validmodes.valuesToAddress(lim.getOpCode(), getSV(channel), getV1(channel), getV2(channel)));
445                                }
446                                log.debug("... decoded address (cv={} v1={} v2={}) is {}(0x{})",
447                                        Integer.toHexString(getSV(channel)), Integer.toHexString(getV1(channel)),
448                                        Integer.toHexString(getV2(channel)), getAddr(channel),
449                                        Integer.toHexString(getAddr(channel))); // NOI18N
450                            } else {
451                                log.warn("OPC_PEER_XFR: Type ({}) is not {0,1,2} for channel {}", type, channel); // NOI18N
452                            }
453                        // } else {
454                            // log.error("last CV recorded is invalid: {}", lastOpCv);
455                        }
456                    }  // end of read processing
457
458                    // check for anything else to do
459                    issueNextOperation();
460                    return;
461                } else {
462                    return;
463                }
464            case LnConstants.OPC_INPUT_REP: // Block Sensors and other general sensor codes
465                log.debug("{} received", LnConstants.OPC_NAME(opCode)); // NOI18N
466                // these might require capture
467                for (int i = 0; i < _numRows; i++) {
468                    if (capture[i]) {
469                        log.debug("row set for capture: {}", i); // NOI18N
470                        // This is a capture request, get address bytes
471                        int val1 = m.getElement(1);
472                        int val2 = m.getElement(2);
473                        // calculate address from val's, save result, mark as done
474                        // INPUT_REP's use val2's OPC_SW_REQ_DIR bit as LSB...'
475                        setAddr(i, ((val2 & 0x0F) << 5) * 256 + ((val1 & 0x7f) << 1)
476                                | (((val2 & LnConstants.OPC_SW_REQ_DIR) == LnConstants.OPC_SW_REQ_DIR) ? 0x01 : 0x00));
477                        capture[i] = false;
478                    }
479                }
480                return;
481
482            case LnConstants.OPC_SW_REQ:    // Turnout SWITCH Request
483                log.debug("{} received", LnConstants.OPC_NAME(opCode)); // NOI18N
484                // these might require capture
485                for (int i = 0; i < _numRows; i++) {
486                    if (capture[i]) {
487                        log.debug("row set for capture: {}", i); // NOI18N
488                        // This is a capture request, get address bytes
489                        int val1 = m.getElement(1);
490                        int val2 = m.getElement(2);
491                        // calculate address from val's, save result, mark as done
492                        int addr = LocoIO.SENSOR_ADR(val1, val2);
493                        setAddr(i, addr);
494                        capture[i] = false;
495                    }
496                }
497                return;
498            default:    // we ignore all other LocoNet messages
499            log.debug("{} received (ignored)", LnConstants.OPC_NAME(opCode));
500        }
501    }
502
503    /**
504     * A valid reply has been received, so the read/write worked, and the state
505     * should be advanced.
506     */
507    protected synchronized void replyReceived() {
508        timeoutcounter = 0;
509        // READ operations state machine
510        switch (readState[currentPin]) {
511            case NONE:
512                break;   // try the write operations
513            case READVALUE1:
514            case READINGVALUE1:
515                readState[currentPin] = READVALUE2;
516                return;
517            case READVALUE2:
518            case READINGVALUE2:
519                readState[currentPin] = READMODE;
520                return;
521            case READMODE:
522            case READINGMODE:
523                readState[currentPin] = NONE;
524                return;
525            default:
526                log.error("Pin {} unexpected read state, can't advance {}", currentPin, readState[currentPin]); // NOI18N
527                readState[currentPin] = NONE;
528                return;
529        }
530        // WRITE operations state machine
531        switch (writeState[currentPin]) {
532            case NONE:
533                return;
534            case WRITEVALUE1:
535            case WRITINGVALUE1:
536                writeState[currentPin] = WRITEVALUE2;
537                return;
538            case WRITEVALUE2:
539            case WRITINGVALUE2:
540                writeState[currentPin] = WRITEMODE;
541                return;
542            case WRITEMODE:
543            case WRITINGMODE:
544                writeState[currentPin] = NONE;
545                return;
546            default:
547                log.error("Pin {} unexpected write state, can't advance {}", currentPin, writeState[currentPin]); // NOI18N
548                writeState[currentPin] = NONE;
549                return;
550        }
551    }
552
553    private int currentPin = 0;
554
555    /**
556     * Look through the table to find the next thing that needs to be read.
557     */
558    protected synchronized void issueNextOperation() {
559        // stop the timer while we figure this out
560        stopTimer();
561        // find the first item that needs to be read
562        for (int i = 0; i < _numRows; i++) {
563            currentPin = i;
564            if (readState[i] != NONE) {
565                // yes, needs read. Find what kind
566                log.debug("iNO: readState[{}] = {}", i, readState[i]);
567                switch (readState[i]) {
568                    case READVALUE1:
569                    case READINGVALUE1:
570                        // set new state, send read, then done
571                        readState[i] = READINGVALUE1;
572                        lastOpCv = i * 3 + 4;
573                        setStatus(Bundle.getMessage("StatusReading", lastOpCv, i + 1, 1)); // number port like table
574                        sendReadCommand(unitAddress, unitSubAddress, lastOpCv);
575                        return;
576                    case READVALUE2:
577                    case READINGVALUE2:
578                        // set new state, send read, then done
579                        readState[i] = READINGVALUE2;
580                        lastOpCv = i * 3 + 5;
581                        setStatus(Bundle.getMessage("StatusReading", lastOpCv, i + 1, 2));
582                        sendReadCommand(unitAddress, unitSubAddress, lastOpCv);
583                        return;
584                    case READMODE:
585                    case READINGMODE:
586                        // set new state, send read, then done
587                        readState[i] = READINGMODE;
588                        lastOpCv = i * 3 + 3;
589                        setStatus(Bundle.getMessage("StatusReadMode", lastOpCv, i + 1));
590                        sendReadCommand(unitAddress, unitSubAddress, lastOpCv);
591                        return;
592                    default:
593                        log.error("found an unexpected state: {} on port {}", readState[1], i + 1); // NOI18N
594                        return;
595                }
596            }
597        }
598        // no reads, so continue to check writes
599        for (int i = 0; i < _numRows; i++) {
600            currentPin = i;
601            if (writeState[i] != NONE) {
602                // yes, needs read.  Find what kind
603                log.debug("iNO: writeState[{}] = {}", i, readState[i]);
604                switch (writeState[i]) {
605                    case WRITEVALUE1:
606                    case WRITINGVALUE1:
607                        // set new state, send read, then done
608                        writeState[i] = WRITINGVALUE1;
609                        lastOpCv = i * 3 + 4;
610                        setStatus(Bundle.getMessage("StatusWriting", lastOpCv, i + 1, 1));
611                        sendWriteCommand(unitAddress, unitSubAddress, lastOpCv, getV1(i));
612                        return;
613                    case WRITEVALUE2:
614                    case WRITINGVALUE2:
615                        // set new state, send read, then done
616                        writeState[i] = WRITINGVALUE2;
617                        lastOpCv = i * 3 + 5;
618                        setStatus(Bundle.getMessage("StatusWriting", lastOpCv, i + 1, 2));
619                        sendWriteCommand(unitAddress, unitSubAddress, lastOpCv, getV2(i));
620                        return;
621                    case WRITEMODE:
622                    case WRITINGMODE:
623                        // set new state, send write, then done
624                        writeState[i] = WRITINGMODE;
625                        lastOpCv = i * 3 + 3;
626                        setStatus(Bundle.getMessage("StatusWriteMode", lastOpCv, i + 1));
627                        sendWriteCommand(unitAddress, unitSubAddress, lastOpCv, getSV(i));
628                        return;
629
630                    default:
631                        log.error("found an unexpected state: {} on port {}", writeState[1], i + 1); // NOI18N
632                        return;
633                }
634            }
635        }
636        // nothing of interest found, so just end gracefully
637        log.debug("No operation needed");
638        setStatus(Bundle.getMessage("StatusOK"));
639        lastOpCv = -1;
640        currentPin = 0;
641    }
642
643    /**
644     * Timer Management Protect against communication failures, addressing
645     * mixups and the like.
646     */
647    private static final int TIMEOUT = 2000;    // ms
648    protected javax.swing.Timer timer = null;
649    private int timeoutcounter;
650
651    /**
652     * Internal routine to handle a timeout during read/write by retrying the
653     * same operation.
654     */
655    synchronized protected void timeout() {
656        log.debug("timeout!"); // NOI18N
657        setStatus(Bundle.getMessage("Timeout"));
658        if (timeoutcounter++ == 5) {
659            for (int i = 0; i < _numRows; i++) {
660                readState[i] = NONE;
661                writeState[i] = NONE;
662            }
663            setStatus(Bundle.getMessage("StateAborted"));
664            setLIOVersion(Bundle.getMessage("StateUnknown")); // NOI18N
665            timeoutcounter = 0;
666            stopTimer();
667        } else {
668            issueNextOperation();
669        }
670    }
671
672    /**
673     * Internal routine to start timer to protect the mode-change.
674     */
675    protected void startTimer() {
676        restartTimer(TIMEOUT);
677    }
678
679    /**
680     * Internal routine to stop timer, as all is well.
681     */
682    protected void stopTimer() {
683        if (timer != null) {
684            timer.stop();
685        }
686    }
687
688    /**
689     * Internal routine to handle timer starts and restarts.    
690     * @param delay Milliseconds to wait
691     */
692    protected void restartTimer(int delay) {
693        if (timer == null) {
694            timer = new javax.swing.Timer(delay, new java.awt.event.ActionListener() {
695                @Override
696                public void actionPerformed(java.awt.event.ActionEvent e) {
697                    timeout();
698                }
699            });
700        }
701        timer.stop();
702        timer.setInitialDelay(delay);
703        timer.setRepeats(false);
704        timer.start();
705    }
706
707    /**
708     * Read an SV from a given LocoIO device.
709     * @param locoIOAddress primary board address
710     * @param locoIOSubAddress subaddress within board
711     * @param cv CV number to access
712     */
713    void sendReadCommand(int locoIOAddress, int locoIOSubAddress, int cv) {
714        // remember current op is read
715        reading = true;
716        log.debug("sendReadCommand(to {}/{} SV {}",
717                Integer.toHexString(locoIOAddress), Integer.toHexString(locoIOSubAddress), cv);
718        tc.sendLocoNetMessage(
719                LocoIO.readCV(locoIOAddress, locoIOSubAddress, cv));
720        startTimer();        // and set timeout on reply
721    }
722
723    /**
724     * Write an SV to a given LocoIO device.
725     * @param locoIOAddress primary board address
726     * @param locoIOSubAddress subaddress within board
727     * @param cv CV number to access
728     * @param data value to be written
729     */
730    void sendWriteCommand(int locoIOAddress, int locoIOSubAddress, int cv, int data) {
731        // remember current op is write
732        reading = false;
733
734        tc.sendLocoNetMessage(
735                LocoIO.writeCV(locoIOAddress, locoIOSubAddress, cv, data));
736        startTimer();        // and set timeout on reply
737    }
738
739    public void dispose() {
740        log.debug("dispose"); // NOI18N
741        // disconnect from future events
742        stopTimer();
743        tc.removeLocoNetListener(~0, this);
744
745        // null references, so that they can be gc'd even if this isn't.
746        addr = null;
747        mode = null;
748        sv = null;
749        v1 = null;
750        v2 = null;
751        lim = null;
752    }
753
754    private final static Logger log = LoggerFactory.getLogger(LocoIOData.class);
755
756}