001package jmri.jmrix.loconet;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004import javax.annotation.CheckForNull;
005import jmri.DccLocoAddress;
006import jmri.DccThrottle;
007import jmri.LocoAddress;
008import jmri.SpeedStepMode;
009import jmri.jmrix.AbstractThrottle;
010import org.slf4j.Logger;
011import org.slf4j.LoggerFactory;
012import jmri.ThrottleListener;
013
014/**
015 * An implementation of DccThrottle via AbstractThrottle with code specific to a
016 * LocoNet connection.
017 * <p>
018 * Speed in the Throttle interfaces and AbstractThrottle is a float, but in
019 * LocoNet is an int with values from 0 to 127.
020 *
021 * @author Glen Oberhauser, Bob Jacobsen Copyright (C) 2003, 2004
022 * @author Stephen Williams Copyright (C) 2008
023 * @author B. Milhaupt, Copyright (C) 2018
024 */
025public class LocoNetThrottle extends AbstractThrottle implements SlotListener {
026
027    protected LocoNetSlot slot;
028    protected LocoNetInterface network;
029    protected LnThrottleManager throttleManager;
030    protected int address;
031
032    // members to record the last known spd/dirf/snd bytes AS READ FROM THE LAYOUT!!
033    protected int layout_spd;
034    protected int layout_dirf;
035    protected int layout_snd;
036    protected int layout_stat1 = 0;
037
038    // with extended slots the slots may not have been updated by the echo
039    // before the next message needs sending.So we must save and send what
040    // we believe to be the correct speed and direction.
041    // remember in expanded mode 2 throttle cannot be in control of a loco
042
043    protected int new_spd;
044    protected long new_spd_lastupdated;
045    protected boolean new_isFwd;
046    protected long new_isFwd_lastupdated;
047
048    // slot status to be warned if slot released or dispatched
049    protected int slotStatus;
050    protected boolean isDisposing = false;
051
052    /**
053     * Constructor
054     *
055     * @param memo connection details
056     * @param slot The LocoNetSlot this throttle will talk on.
057     */
058    public LocoNetThrottle(LocoNetSystemConnectionMemo memo, LocoNetSlot slot) {
059        super(memo, 69); // supports up to F68
060        this.slot = slot;
061        slot.setIsInitialized(false);
062        network = memo.getLnTrafficController();
063        throttleManager = (LnThrottleManager)memo.getThrottleManager();
064
065        // save last known layout state for spd/dirf/snd so we can
066        // avoid race condition if another LocoNet process queries
067        // our slot while we are in the act of changing it.
068        layout_spd = slot.speed();
069        layout_dirf = slot.dirf();
070        layout_snd = slot.snd();
071
072        // cache settings
073        synchronized(this) {
074            this.speedSetting = floatSpeed(slot.speed());
075        }
076        for (int i = 0; i < 29; i++) {
077            super.updateFunction(i,slot.isFunction(i));
078        }
079
080        // for LocoNet throttles, the default is f2 momentary (for the horn)
081        // all other functions are continuos (as set in AbstractThrottle).
082        super.updateFunctionMomentary(2, true);
083
084        this.address = slot.locoAddr();
085        this.isForward = slot.isForward();
086        this.slotStatus = slot.slotStatus();
087
088        switch (slot.decoderType()) {
089            case LnConstants.DEC_MODE_128:
090            case LnConstants.DEC_MODE_128A:
091                setSpeedStepMode(SpeedStepMode.NMRA_DCC_128);
092                break;
093            case LnConstants.DEC_MODE_28:
094            case LnConstants.DEC_MODE_28A:
095            case LnConstants.DEC_MODE_28TRI:
096                setSpeedStepMode(SpeedStepMode.NMRA_DCC_28);
097                break;
098            case LnConstants.DEC_MODE_14:
099                setSpeedStepMode(SpeedStepMode.NMRA_DCC_14);
100                break;
101            default:
102                log.warn("Unhandled decoder type: {}", slot.decoderType());
103                break;
104        }
105
106        // listen for changes
107        slot.addSlotListener(this);
108
109        network.sendLocoNetMessage(slot.writeNullMove());
110
111        // start periodically sending the speed, to keep this
112        // attached
113        startRefresh();
114        log.debug("constructed a new throttle using slot {} for loco address {}", slot.getSlot(), slot.locoAddr());
115    }
116
117    /**
118     * Convert a LocoNet speed integer to a float speed value
119     *
120     * @param lSpeed LocoNet style speed value
121     * @return speed as float 0-&gt;1.0, or -1.0 to indicate E-Stop
122     */
123    protected float floatSpeed(int lSpeed) {
124        log.debug("speed (int) is {}", lSpeed);
125        if (lSpeed == 0) {
126            return 0.f;
127        } else if (lSpeed == 1) {
128            return -1.f;   // estop
129        }
130        if (getSpeedStepMode() == SpeedStepMode.NMRA_DCC_28) {
131            if (lSpeed <= 15) //Value less than 15 is in the stop/estop range bracket
132            {
133                return 0.f;
134            }
135            return (((lSpeed - 12) / 4f) / 28.f);
136        } else if (getSpeedStepMode() == SpeedStepMode.NMRA_DCC_14) {
137            if (lSpeed <= 15) //Value less than 15 is in the stop/estop range bracket
138            {
139                return 0.f;
140            }
141            return ((lSpeed - 8) / 8f) / 14.f;
142        } else {
143            return ((lSpeed - 1) / 126.f);
144        }
145    }
146
147    /**
148     * Computes the integer speed value from a float.
149     * <p>
150     * Values of less than 0 indicate Emergency Stop.
151     * <p>
152     * Value of 0.0 indicates stop.
153     * <p>
154     * Values between 0.0+ and 1.0 imply speed step values between 2 and the
155     * maximum value allowed for the loco's speed step mode.
156     *
157     * @param fSpeed is the floating-point speed value to be converted
158     * @return an integer which represents the speed step value
159     */
160    @Override
161    protected int intSpeed(float fSpeed) {
162        log.debug("intSpeed speed is {}", fSpeed);
163        int speed = super.intSpeed(fSpeed);
164        if (speed <= 1) {
165            return speed; // return idle and emergency stop
166        }
167        switch (this.getSpeedStepMode()) {
168            case NMRA_DCC_28:
169            case MOTOROLA_28:
170                return (int) ((fSpeed * 28) * 4) + 12;
171            case NMRA_DCC_14:
172                return (int) ((fSpeed * 14) * 8) + 8;
173            case NMRA_DCC_128:
174                return speed;
175            default:
176                log.warn("Unhandled speed step: {}", this.getSpeedStepMode());
177                break;
178        }
179        return speed;
180    }
181
182    /**
183     * Constants to represent Function Groups.
184     * <p>
185     * The are the same groupings for both normal Functions and Momentary.
186     */
187    private static final int[] EXP_FUNCTION_GROUPS = new int[]{
188            1, 1, 1, 1, 1, 1, 1, /** 0-6 */
189            2, 2, 2, 2, 2, 2, 2, /** 7 - 13 */
190            3, 3, 3, 3, 3, 3, 3, /** 14 -20 */
191            4, 4, 4, 4, 4, 4, 4, 4, /** 21 - 28 */
192            5, 5, 5, 5, 5, 5, 5, 5, 5, 5, // 29 - 69
193            5, 5, 5, 5, 5, 5, 5, 5, 5, 5, // 29 - 69
194            5, 5, 5, 5, 5, 5, 5, 5, 5, 5, // 29 - 69
195            5, 5, 5, 5, 5, 5, 5, 5, 5, 5, // 29 - 69
196            5, 5, 5, 5, 5, 5, 5, 5, 5, 5  // 29 - 69
197    };
198
199    /**
200     * Send whole (DCC) Function Group for a particular function number.
201     * @param functionNum Function Number
202     * @param momentary False to send normal function status, true to send momentary.
203     */
204    @Override
205    protected void sendFunctionGroup(int functionNum, boolean momentary){
206        if (slot.getProtocol() != LnConstants.LOCONETPROTOCOL_TWO) {
207            super.sendFunctionGroup(functionNum, momentary);
208            return;
209        }
210        switch (EXP_FUNCTION_GROUPS[functionNum]) {
211            case 1:
212                if (momentary) sendMomentaryFunctionGroup1(); else sendExpFunctionGroup1();
213                break;
214            case 2:
215                if (momentary) sendMomentaryFunctionGroup2(); else sendExpFunctionGroup2();
216                break;
217            case 3:
218                if (momentary) sendMomentaryFunctionGroup3(); else sendExpFunctionGroup3();
219                break;
220            case 4:
221                if (momentary) sendMomentaryFunctionGroup4(); else sendExpFunctionGroup4();
222                break;
223            case 5:
224                // send as regular function operations
225                super.sendFunctionGroup(functionNum, momentary);
226                break;
227            default:
228                break;
229        }
230    }
231
232    /**
233     * Send the LocoNet message to set the state of locomotive direction and
234     * functions F0, F1, F2, F3, F4
235     * Unfortunately this is used by all throttles to send direction changes, but the expanded slots dont use this
236     * for direction changes, they use speed... And we don't know if the caller wants to send functions or direction.
237     */
238    @Override
239    protected void sendFunctionGroup1() {
240        int new_dirf = ((getIsForward() ? 0 : LnConstants.DIRF_DIR)
241                | (getFunction(0) ? LnConstants.DIRF_F0 : 0)
242                | (getFunction(1) ? LnConstants.DIRF_F1 : 0)
243                | (getFunction(2) ? LnConstants.DIRF_F2 : 0)
244                | (getFunction(3) ? LnConstants.DIRF_F3 : 0)
245                | (getFunction(4) ? LnConstants.DIRF_F4 : 0));
246        log.debug("sendFunctionGroup1 sending {} to LocoNet slot {}", new_dirf, slot.getSlot());
247        LocoNetMessage msg = new LocoNetMessage(4);
248        msg.setOpCode(LnConstants.OPC_LOCO_DIRF);
249        msg.setElement(1, slot.getSlot());
250        msg.setElement(2, new_dirf);
251        network.sendLocoNetMessage(msg);
252    }
253
254    /**
255     * Send the LocoNet message to set the state of functions F5, F6, F7, F8
256     */
257    @Override
258    protected void sendFunctionGroup2() {
259        int new_snd = ((getFunction(8) ? LnConstants.SND_F8 : 0)
260                | (getFunction(7) ? LnConstants.SND_F7 : 0)
261                | (getFunction(6) ? LnConstants.SND_F6 : 0)
262                | (getFunction(5) ? LnConstants.SND_F5 : 0));
263        log.debug("sendFunctionGroup2 sending {} to LocoNet slot {}", new_snd, slot.getSlot());
264        LocoNetMessage msg = new LocoNetMessage(4);
265        msg.setOpCode(LnConstants.OPC_LOCO_SND);
266        msg.setElement(1, slot.getSlot());
267        msg.setElement(2, new_snd);
268        network.sendLocoNetMessage(msg);
269    }
270
271    /**
272     * Sends Function Group 3 values - F9 thru F12, using an "OPC_IMM_PACKET" LocoNet
273     * Message.
274     */
275    @Override
276    protected void sendFunctionGroup3() {
277        // LocoNet practice is to send F9-F12 as a DCC packet
278        byte[] result = jmri.NmraPacket.function9Through12Packet(address, (address >= 128),
279                getFunction(9), getFunction(10), getFunction(11), getFunction(12));
280
281        log.debug("sendFunctionGroup3 sending {} to LocoNet slot {}", result, slot.getSlot());
282        adapterMemo.get(jmri.CommandStation.class).sendPacket(result, 4); // repeat = 4
283    }
284
285    /**
286     * Sends Function Group 4 values - F13 thru F20, using an "OPC_IMM_PACKET" LocoNet
287     * Message.
288     */
289    @Override
290    protected void sendFunctionGroup4() {
291        // LocoNet practice is to send F13-F20 as a DCC packet
292        byte[] result = jmri.NmraPacket.function13Through20Packet(address, (address >= 128),
293                getFunction(13), getFunction(14), getFunction(15), getFunction(16),
294                getFunction(17), getFunction(18), getFunction(19), getFunction(20));
295
296        log.debug("sendFunctionGroup4 sending {} to LocoNet slot {}", result, slot.getSlot());
297        adapterMemo.get(jmri.CommandStation.class).sendPacket(result, 4); // repeat = 4
298    }
299
300    /**
301     * Sends Function Group 5 values - F21 thru F28, using an "OPC_IMM_PACKET" LocoNet
302     * Message.
303     */
304    @Override
305    protected void sendFunctionGroup5() {
306        // LocoNet practice is to send F21-F28 as a DCC packet
307        byte[] result = jmri.NmraPacket.function21Through28Packet(address, (address >= 128),
308                getFunction(21), getFunction(22), getFunction(23), getFunction(24),
309                getFunction(25), getFunction(26), getFunction(27), getFunction(28));
310
311        log.debug("sendFunctionGroup5 sending {} to LocoNet slot {}", result, slot.getSlot());
312        adapterMemo.get(jmri.CommandStation.class).sendPacket(result, 4); // repeat = 4
313    }
314
315    /**
316     * Sends Function Group 6 values - F29 thru F36, using an "OPC_IMM_PACKET" LocoNet
317     * Message.
318     */
319    @Override
320    protected void sendFunctionGroup6() {
321        // LocoNet practice is to send as a DCC packet
322        int i = 29;
323        byte[] result = jmri.NmraPacket.function29Through36Packet(address, (address >= 128),
324                getFunction(i), getFunction(i+1), getFunction(i+2), getFunction(i+3),
325                getFunction(i+4), getFunction(i+5), getFunction(i+6), getFunction(i+7));
326
327        log.debug("sendFunctionGroup6 sending {} to LocoNet slot {}", result, slot.getSlot());
328        adapterMemo.get(jmri.CommandStation.class).sendPacket(result, 4); // repeat = 4
329    }
330
331    /**
332     * Sends Function Group 7 values - F37 thru F44, using an "OPC_IMM_PACKET" LocoNet
333     * Message.
334     */
335    @Override
336    protected void sendFunctionGroup7() {
337        // LocoNet practice is to send as a DCC packet
338        int i = 37;
339        byte[] result = jmri.NmraPacket.function37Through44Packet(address, (address >= 128),
340                getFunction(i), getFunction(i+1), getFunction(i+2), getFunction(i+3),
341                getFunction(i+4), getFunction(i+5), getFunction(i+6), getFunction(i+7));
342
343        log.debug("sendFunctionGroup7 sending {} to LocoNet slot {}", result, slot.getSlot());
344        adapterMemo.get(jmri.CommandStation.class).sendPacket(result, 4); // repeat = 4
345    }
346
347    /**
348     * Sends Function Group 8 values - F45 thru F52, using an "OPC_IMM_PACKET" LocoNet
349     * Message.
350     */
351    @Override
352    protected void sendFunctionGroup8() {
353        // LocoNet practice is to send as a DCC packet
354        int i = 45;
355        byte[] result = jmri.NmraPacket.function45Through52Packet(address, (address >= 128),
356                getFunction(i), getFunction(i+1), getFunction(i+2), getFunction(i+3),
357                getFunction(i+4), getFunction(i+5), getFunction(i+6), getFunction(i+7));
358
359        log.debug("sendFunctionGroup8 sending {} to LocoNet slot {}", result, slot.getSlot());
360        adapterMemo.get(jmri.CommandStation.class).sendPacket(result, 4); // repeat = 4
361    }
362
363    /**
364     * Sends Function Group 9 values - F53 thru F60, using an "OPC_IMM_PACKET" LocoNet
365     * Message.
366     */
367    @Override
368    protected void sendFunctionGroup9() {
369        // LocoNet practice is to send as a DCC packet
370        int i = 53;
371        byte[] result = jmri.NmraPacket.function53Through60Packet(address, (address >= 128),
372                getFunction(i), getFunction(i+1), getFunction(i+2), getFunction(i+3),
373                getFunction(i+4), getFunction(i+5), getFunction(i+6), getFunction(i+7));
374
375        log.debug("sendFunctionGroup9 sending {} to LocoNet slot {}", result, slot.getSlot());
376        adapterMemo.get(jmri.CommandStation.class).sendPacket(result, 4); // repeat = 4
377    }
378
379    /**
380     * Sends Function Group 10 values - F61 thru F68, using an "OPC_IMM_PACKET" LocoNet
381     * Message.
382     */
383    @Override
384    protected void sendFunctionGroup10() {
385        // LocoNet practice is to send as a DCC packet
386        int i = 61;
387        byte[] result = jmri.NmraPacket.function61Through68Packet(address, (address >= 128),
388                getFunction(i), getFunction(i+1), getFunction(i+2), getFunction(i+3),
389                getFunction(i+4), getFunction(i+5), getFunction(i+6), getFunction(i+7));
390
391        log.debug("sendFunctionGroup10 sending {} to LocoNet slot {}", result, slot.getSlot());
392        adapterMemo.get(jmri.CommandStation.class).sendPacket(result, 4); // repeat = 4
393    }
394
395    /**
396     * Send the Expanded LocoNet message to set the state of locomotive direction and
397     * functions F0, F1, F2, F3, F4, F5, F6
398     */
399    protected void sendExpFunctionGroup1() {
400            int new_F0F6 = ((getFunction(5) ? 0b00100000 : 0) | (getFunction(6) ? 0b01000000 : 0)
401                | (getFunction(0) ? LnConstants.DIRF_F0 : 0)
402                | (getFunction(1) ? LnConstants.DIRF_F1 : 0)
403                | (getFunction(2) ? LnConstants.DIRF_F2 : 0)
404                | (getFunction(3) ? LnConstants.DIRF_F3 : 0)
405                | (getFunction(4) ? LnConstants.DIRF_F4 : 0));
406            LocoNetMessage msg = new LocoNetMessage(6);
407            msg.setOpCode(LnConstants.OPC_EXP_SEND_FUNCTION_OR_SPEED_AND_DIR);
408            msg.setElement(1, (slot.getSlot() / 128) | LnConstants.OPC_EXP_SEND_FUNCTION_GROUP_F0F6 );
409            msg.setElement(2,slot.getSlot() & 0b01111111);
410            msg.setElement(3,slot.id() & 0x7F);
411            msg.setElement(4, new_F0F6);
412            network.sendLocoNetMessage(msg);
413    }
414
415    /**
416     * Send the Expanded LocoNet message to set the state of functions F7, F8, F8, F9, F10, F11, F12, F13
417     */
418    protected void sendExpFunctionGroup2() {
419            int new_F7F13 = ((getFunction(7) ? 0b00000001 : 0) | (getFunction(8) ? 0b00000010 : 0)
420                    | (getFunction(9)  ? 0b00000100 : 0)
421                    | (getFunction(10) ? 0b00001000 : 0)
422                    | (getFunction(11) ? 0b00010000 : 0)
423                    | (getFunction(12) ? 0b00100000 : 0)
424                    | (getFunction(13) ? 0b01000000 : 0));
425                LocoNetMessage msg = new LocoNetMessage(6);
426                msg.setOpCode(LnConstants.OPC_EXP_SEND_FUNCTION_OR_SPEED_AND_DIR);
427                msg.setElement(1, (slot.getSlot() / 128) | LnConstants.OPC_EXP_SEND_FUNCTION_GROUP_F7F13 );
428                msg.setElement(2,slot.getSlot() & 0b01111111);
429                msg.setElement(3,slot.id() & 0x7F);
430                msg.setElement(4, new_F7F13);
431                network.sendLocoNetMessage(msg);
432    }
433
434    /**
435     * Sends expanded loconet message F14 thru F20
436     * Message.
437     */
438    protected void sendExpFunctionGroup3() {
439        int new_F14F20 = ((getFunction(14) ? 0b00000001 : 0) | (getFunction(15) ? 0b00000010 : 0)
440                | (getFunction(16)  ? 0b00000100 : 0)
441                | (getFunction(17) ? 0b00001000 : 0)
442                | (getFunction(18) ? 0b00010000 : 0)
443                | (getFunction(19) ? 0b00100000 : 0)
444                | (getFunction(20) ? 0b01000000 : 0));
445            LocoNetMessage msg = new LocoNetMessage(6);
446            msg.setOpCode(LnConstants.OPC_EXP_SEND_FUNCTION_OR_SPEED_AND_DIR);
447            msg.setElement(1, (slot.getSlot() / 128) | LnConstants.OPC_EXP_SEND_FUNCTION_GROUP_F14F20 );
448            msg.setElement(2,slot.getSlot() & 0b01111111);
449            msg.setElement(3,slot.id() & 0x7F);
450            msg.setElement(4, new_F14F20);
451            network.sendLocoNetMessage(msg);
452    }
453
454    /**
455     * Sends Expanded loconet message F21 thru F28 Message.
456     */
457    protected void sendExpFunctionGroup4() {
458        int new_F2128 = ((getFunction(21) ? 0b00000001 : 0) |
459                (getFunction(22) ? 0b00000010 : 0) |
460                (getFunction(23) ? 0b00000100 : 0) |
461                (getFunction(24) ? 0b00001000 : 0) |
462                (getFunction(25) ? 0b00010000 : 0) |
463                (getFunction(26) ? 0b00100000 : 0) |
464                (getFunction(27) ? 0b01000000 : 0));
465        LocoNetMessage msg = new LocoNetMessage(6);
466        msg.setOpCode(LnConstants.OPC_EXP_SEND_FUNCTION_OR_SPEED_AND_DIR);
467        if (!getFunction(28)) {
468            msg.setElement(1, (slot.getSlot() / 128) | LnConstants.OPC_EXP_SEND_FUNCTION_GROUP_F21F28_F28OFF);
469        } else {
470            msg.setElement(1, (slot.getSlot() / 128) | LnConstants.OPC_EXP_SEND_FUNCTION_GROUP_F21F28_F28ON);
471        }
472        msg.setElement(2, slot.getSlot() & 0b01111111);
473        msg.setElement(3, slot.id() & 0x7F);
474        msg.setElement(4, new_F2128);
475        network.sendLocoNetMessage(msg);
476    }
477
478    /**
479     * Send the expanded slot command for speed and direction on change of speed
480     * Note we send our stored values as slot is updated via an echo
481     * and may not have been updated yet when sending rapid commands
482     * @param speed the speed to set
483     */
484    protected void sendExpSpeedAndDirection(int speed) {
485        boolean isFwd;
486        if (slot.getLastUpdateTime() <  new_isFwd_lastupdated) {
487            isFwd = new_isFwd;
488        } else {
489            isFwd = slot.isForward();
490        }
491        // save last speed update for change of direction;
492        new_spd = speed;
493        new_spd_lastupdated = System.currentTimeMillis();
494        LocoNetMessage msg = new LocoNetMessage(6);
495        msg.setOpCode(LnConstants.OPC_EXP_SEND_FUNCTION_OR_SPEED_AND_DIR);
496        msg.setElement(1, ((slot.getSlot() / 128) & 0x03) | (isFwd ? 0x00 : 0x08));
497        msg.setElement(2, slot.getSlot() & 0x7f);
498        msg.setElement(3, (slot.id() & 0x7f));
499        msg.setElement(4, speed);
500        network.sendLocoNetMessage(msg);
501    }
502
503    /**
504     * Send the expanded slot command for speed and direction on change of direction
505     * Note we send our stored speed if slot has not yet been updated by the echo
506     * @param isFwd new direction
507     */
508    protected void sendExpSpeedAndDirection(boolean isFwd) {
509        int speed;
510        if (slot.getLastUpdateTime() <  new_spd_lastupdated) {
511            speed = new_spd;
512        } else {
513            speed = slot.speed();
514        }
515        // save last speed update for change of direction;
516        new_isFwd = isFwd;
517        new_isFwd_lastupdated = System.currentTimeMillis();
518        LocoNetMessage msg = new LocoNetMessage(6);
519        msg.setOpCode(LnConstants.OPC_EXP_SEND_FUNCTION_OR_SPEED_AND_DIR);
520        msg.setElement(1, ((slot.getSlot() / 128) & 0x03) | (isFwd ? 0x00 : 0x08));
521        msg.setElement(2, slot.getSlot() & 0x7f);
522        msg.setElement(3, (slot.id() & 0x7f));
523        msg.setElement(4, speed);
524        network.sendLocoNetMessage(msg);
525    }
526
527    /**
528     * Send a LocoNet message to set the loco speed speed.
529     *
530     * @param speed Number from 0 to 1; less than zero is "emergency stop"
531     */
532    @Override
533    public void setSpeedSetting(float speed) {
534        setSpeedSetting(speed, false, false);
535    }
536
537    /**
538     * Set the Speed, ensuring that a LocoNet message is sent to update the slot
539     * even if the new speed is effectively the same as the current speed. Note: this
540     * can cause an increase in LocoNet traffic.
541     *
542     * @param speed Number from 0 to 1; less than zero is emergency stop
543     */
544    @Override
545    public void setSpeedSettingAgain(float speed) {
546        setSpeedSetting(speed, true, true);
547    }
548
549    /**
550     * Set the speed. No LocoNet message is sent if the new speed would
551     * result in a 'duplicate' - ie. a speed setting no different to the one the slot
552     * currently has - unless the boolean paramters indicate it should be.
553     *
554     * @param speed Number from 0 to 1; less than zero is emergency stop
555     * @param allowDuplicates boolean - if true, send a LocoNet message no matter what
556     * @param allowDuplicatesOnStop boolean - if true, send a LocoNet message if the new speed is
557     *                              'idle' or 'emergency stop', even if that matches the
558     *                              existing speed.
559     *
560     */
561    @SuppressFBWarnings(value = "FE_FLOATING_POINT_EQUALITY") // OK to compare floating point, notify on any change
562    @Override
563    public void setSpeedSetting(float speed, boolean allowDuplicates, boolean allowDuplicatesOnStop) {
564        log.debug("setSpeedSetting: called with speed {} for LocoNet slot {} allowDup {} allowDupOnStop {}",
565                    speed, slot.getSlot(), allowDuplicates, allowDuplicatesOnStop);
566        if (LnConstants.CONSIST_MID == slot.consistStatus()
567                || LnConstants.CONSIST_SUB == slot.consistStatus()) {
568            // Digitrax slots use the same memory location to store the
569            // speed AND the slot to which a locomotive is consisted.
570            // if the locomotive is either a CONSIST_MID or a CONSIST_SUB,
571            // we need to ignore the request to change the speed
572            log.debug("Attempt to change speed on locomotive {} which is a {}", getLocoAddress(), LnConstants.CONSIST_STAT(slot.consistStatus()));
573            return;
574        }
575        float oldSpeed;
576        synchronized(this) {
577            oldSpeed = this.speedSetting;
578            this.speedSetting = speed;
579            if (speed < 0) {
580                this.speedSetting = -1.f;
581            }
582        }
583
584        new_spd = intSpeed(speed);
585
586        // decide whether to send a new LocoNet message
587        boolean sendLoconetMessage = false;
588        if (new_spd != layout_spd ) {
589            // the new speed is different - send a message
590            sendLoconetMessage = true;
591        } else if (allowDuplicates) {
592            // calling method wants a new message sent regardless
593            sendLoconetMessage = true;
594        } else if (allowDuplicatesOnStop && new_spd <= 1) {
595            // calling method wants a new message sent if the speed is idle or estop, which it is
596            sendLoconetMessage = true;
597        }
598
599        if (sendLoconetMessage) {
600            log.debug("setSpeedSetting: sending speed {} to LocoNet slot {}", speed, slot.getSlot());
601            if (slot.getProtocol() != LnConstants.LOCONETPROTOCOL_TWO) {
602                LocoNetMessage msg = new LocoNetMessage(4);
603                msg.setOpCode(LnConstants.OPC_LOCO_SPD);
604                msg.setElement(1, slot.getSlot());
605                log.debug("setSpeedSetting: float speed: {} LocoNet speed: {}", speed, new_spd);
606                msg.setElement(2, new_spd);
607                network.sendLocoNetMessage(msg);
608            } else {
609                sendExpSpeedAndDirection(new_spd);
610            }
611
612            // reset timeout - but only if something sent on net
613            if (mRefreshTimer != null) {
614                mRefreshTimer.stop();
615                mRefreshTimer.setRepeats(true);     // refresh until stopped by dispose
616                mRefreshTimer.start();
617                log.debug("Initially starting refresh timer for slot {} address {}", slot.getSlot(), slot.locoAddr());
618            }
619        } else {
620            log.debug("setSpeedSetting: not sending LocoNet speed message to slot {}, new({})==old({})", slot.getSlot(), new_spd, layout_spd);
621        }
622        synchronized(this) {
623            firePropertyChange(SPEEDSETTING, oldSpeed, this.speedSetting);
624        }
625        log.debug("about to invoke record({})", speed);
626        record(speed);
627    }
628
629    /**
630     * Send a LocoNet message containing the specified direction of travel.
631     *
632     * LocoNet actually puts forward and backward in the same message as the
633     * first function group.
634     *
635     * @param forward is true for forward movement, else false
636     */
637    @Override
638    public void setIsForward(boolean forward) {
639        boolean old = isForward;
640        isForward = forward;
641        log.debug("setIsForward to {}, old value {}", isForward, old);
642        if (slot.getProtocol() != LnConstants.LOCONETPROTOCOL_TWO) {
643            sendFunctionGroup1();
644        } else {
645            sendExpSpeedAndDirection(forward);
646        }
647        firePropertyChange(ISFORWARD, old, this.isForward);
648    }
649
650    /**
651     * Get the LocoNetSlot which is used for controlling the loco assoicated
652     * with this throttle.
653     *
654     * @return the LocoNetSlot
655     */
656    @CheckForNull
657    public LocoNetSlot getLocoNetSlot() {
658        if (slot == null) return slot;
659        log.debug("getLocoNetSlot is returning slot {}", slot.getSlot());
660        return slot;
661    }
662
663    @Override
664    public String toString() {
665        return getLocoAddress().toString();
666    }
667
668    /**
669     * Dispose the LocoNetThrottle when finished with this object.
670     *
671     * After this is executed, further use of this Throttle object will
672     * result in a JmriException.
673     */
674    @Override
675    public void throttleDispose() {
676        if (isDisposing) return;
677        log.debug("throttleDispose - disposing of throttle (and setting slot = null)");
678        isDisposing = true;
679
680        // Release throttle connections
681        if (slot != null) {
682            if (slot.slotStatus() == LnConstants.LOCO_IN_USE  ) {
683                // Digitrax throttles do not set the slot speed to zero, so do
684                // not do so here.
685
686                // Make the slot common, after a little wait
687                log.debug("dispatchThrottle is dispatching slot {}", slot);
688                network.sendLocoNetMessage(slot.releaseSlot());
689            }
690            // Can remove the slot listener at any time; any further messages
691            // aren't needed.
692            slot.removeSlotListener(this);
693            // Stop the throttle speed refresh timer
694            if (mRefreshTimer != null) {
695                mRefreshTimer.stop();
696                log.debug("Stopped refresh timer for slot {} address {} as part of throttleDispose", slot.getSlot(), slot.locoAddr());
697            mRefreshTimer = null;
698            }
699
700            slot = null;
701            network = null;
702
703            finishRecord();
704            isDisposing = false;
705        }
706    }
707
708    javax.swing.Timer mRefreshTimer = null;
709
710    /**
711     * Start the "refresh" timer.  The "refresh" timer determines
712     * when to send a new LocoNet message to "refresh" the slot's speed
713     * setting, so that the slot does not get "purged".
714     *
715     */
716    protected void startRefresh() {
717        mRefreshTimer = new javax.swing.Timer(50000, e -> timeout());
718        mRefreshTimer.setRepeats(true);     // refresh until stopped by dispose
719        mRefreshTimer.start();
720        log.debug("Starting refresh timer for slot {} address {}", slot.getSlot(), slot.locoAddr());
721    }
722
723    /**
724     * Internal routine to resend the speed on a timeout
725     */
726    protected synchronized void timeout() {
727        if (slot != null) {
728            log.debug("refresh timer timed-out on slot {}", slot.getSlot());
729            // clear the last known layout_spd so that we will actually send the
730            // message.
731            layout_spd = -1;
732            setSpeedSetting(speedSetting);
733        }
734        else {
735            log.debug("refresh timer time-out on a null slot");
736        }
737    }
738
739    /**
740     * Get notified when underlying slot acquisition process fails.  Slot acquisition
741     * failure is handled by @link LnThrottleManager, so no code is required here.
742     *
743     * @param addr Locomotive address
744     * @param s reason the acquisition failed
745     */
746    public void notifyRefused(int addr, String s) {
747        // don't do anything here; is handled by LnThrottleManager.
748    }
749
750
751    /**
752     * Get notified when underlying slot information changes
753     *
754     * @param pSlot the slot which was changed
755     */
756    @SuppressFBWarnings(value = "FE_FLOATING_POINT_EQUALITY") // OK to compare floating point, notify on any change
757    @Override
758    public void notifyChangedSlot(LocoNetSlot pSlot) {
759        log.debug("notifyChangedSlot executing for slot {}, slotStatus {}", slot.getSlot(), Integer.toHexString(slot.slotStatus()));
760        if (slot != pSlot) {
761            log.error("notified of change in different slot");
762        }
763
764        if(!slot.getIsInitilized() && slot.slotStatus() == LnConstants.LOCO_IN_USE){
765           log.debug("Attempting to update slot with this JMRI instance's throttle id ({})", throttleManager.getThrottleID());
766           network.sendLocoNetMessage(slot.writeThrottleID(throttleManager.getThrottleID()));
767           // finally we are done...
768           slot.setIsInitialized(true);
769           throttleManager.notifyComplete(this, slot);
770        }
771
772        // Save current layout state of spd/dirf/snd so we won't run amok
773        // toggling values if another LocoNet entity accesses the slot while
774        // our most recent change request is still in-flight.
775        layout_spd = slot.speed();
776        layout_dirf = slot.dirf();
777        layout_snd = slot.snd();
778
779        // handle change in each state
780        synchronized(this) {
781            if (this.speedSetting != floatSpeed(slot.speed())) {
782                float old = this.speedSetting;
783                this.speedSetting = floatSpeed(slot.speed());
784                log.debug("notifyChangedSlot: old speed: {} new speed: {}", old, this.speedSetting); // NOI18N
785                firePropertyChange(SPEEDSETTING, old, this.speedSetting);
786            }
787        }
788        firePropertyChange(ISFORWARD, this.isForward, this.isForward = slot.isForward());
789
790        // Slot status
791        if (slotStatus != slot.slotStatus()) {
792            int newStat = slot.slotStatus();
793            log.debug("Slot status changed from {} to {}", LnConstants.LOCO_STAT(slotStatus), LnConstants.LOCO_STAT(newStat)); // NOI18N
794            // PropertyChangeListeners notification: ThrottleConnected from True to False when disconnected
795            firePropertyChange("ThrottleConnected", (slotStatus & LnConstants.LOCOSTAT_MASK) == LnConstants.LOCO_IN_USE, // NOI18N
796                    !((slotStatus & LnConstants.LOCOSTAT_MASK) == LnConstants.LOCO_IN_USE));
797            slotStatus = newStat;
798        }
799
800        // It is possible that the slot status change we are being notified of
801        // is the slot being set to status COMMON. In which case the slot just
802        // got set to null. No point in continuing. In fact to do so causes a NPE.
803        if (slot == null) {
804            return;
805        }
806
807        switch (slot.decoderType()) {
808            case LnConstants.DEC_MODE_128:
809            case LnConstants.DEC_MODE_128A:
810                if(SpeedStepMode.NMRA_DCC_128 != getSpeedStepMode()) {
811                   setSpeedStepMode(SpeedStepMode.NMRA_DCC_128);
812                }
813                break;
814            case LnConstants.DEC_MODE_28:
815            case LnConstants.DEC_MODE_28A:
816            case LnConstants.DEC_MODE_28TRI:
817                if(SpeedStepMode.NMRA_DCC_28 != getSpeedStepMode()) {
818                   setSpeedStepMode(SpeedStepMode.NMRA_DCC_28);
819                }
820                break;
821            case LnConstants.DEC_MODE_14:
822                if(SpeedStepMode.NMRA_DCC_14 != getSpeedStepMode()) {
823                   setSpeedStepMode(SpeedStepMode.NMRA_DCC_14);
824                }
825                break;
826            default:
827                log.warn("Unhandled decoder type: {}", slot.decoderType());
828                break;
829        }
830
831        // Functions
832        updateFunctions();
833
834        log.debug("notifyChangedSlot ends");
835    }
836
837    /**
838     * update the F0-F29 functions.
839     * Invoked by notifyChangedSlot(), this nominally updates from the slot.
840     */
841    protected void updateFunctions() {
842        for (int i = 0; i < 29; i++) {
843            log.debug("updateFunction({}, {})", i, slot.isFunction(i));
844            if (i==20 && log.isTraceEnabled()) log.trace("Tracing back F20", new Exception("traceback"));
845            updateFunction(i,slot.isFunction(i));
846        }
847    }
848
849    /**
850     * Set the speed step value and the related speedIncrement value.
851     *
852     * @param Mode the current speed step mode - default should be 128
853     *             speed step mode in most cases
854     */
855    @Override
856    public void setSpeedStepMode(SpeedStepMode Mode) {
857        int status = slot.slotStatus();
858        log.debug("Speed Step Mode Change to Mode: {} Current mode is: {}", Mode, this.speedStepMode); // NOI18N
859        log.debug("Current Slot Mode: {}", LnConstants.DEC_MODE(status)); // NOI18N
860        firePropertyChange(SPEEDSTEPS, this.speedStepMode, this.speedStepMode = Mode);
861        if (Mode == SpeedStepMode.NMRA_DCC_14) {
862            log.debug("14 speed step change"); // NOI18N
863            status = status & ((~LnConstants.DEC_MODE_MASK)
864                    | LnConstants.STAT1_SL_SPDEX)
865                    | LnConstants.DEC_MODE_14;
866        } else if (Mode == SpeedStepMode.MOTOROLA_28) {
867            log.debug("28-Tristate speed step change");
868            status = status & ((~LnConstants.DEC_MODE_MASK)
869                    | LnConstants.STAT1_SL_SPDEX)
870                    | LnConstants.DEC_MODE_28TRI;
871        } else if (Mode == SpeedStepMode.NMRA_DCC_28) {
872            log.debug("28 speed step change");
873            status = status & ((~LnConstants.DEC_MODE_MASK)
874                    | LnConstants.STAT1_SL_SPDEX);
875            // | LnConstants.DEC_MODE_28;      // DEC_MODE_28 has a zero value, here for documentation
876            // it unfortunately shows a INT_VACUOUS_BIT_OPERATION in SpotBugs
877            // and I don't want to annote that around this entire long method
878        } else { // default to 128 speed step mode
879            log.debug("128 speed step change");
880            status = status & ((~LnConstants.DEC_MODE_MASK)
881                    | LnConstants.STAT1_SL_SPDEX)
882                    | LnConstants.DEC_MODE_128;
883        }
884        log.debug("New Slot Mode: {}", LnConstants.DEC_MODE(status));
885        if (slot.getIsInitilized() )
886            // check that the throttle is completely initialized.
887        {
888            network.sendLocoNetMessage(slot.writeMode(status));
889        }
890    }
891
892    /**
893     * Get the address controlled by this throttle. If the throttle is controlling.
894     *
895     * @return a LocoAddress for the address controlled by this throttle
896     */
897    @Override
898    public LocoAddress getLocoAddress() {
899        if (slot != null) {
900            if ((slot.slotStatus() == LnConstants.LOCO_IN_USE) ||
901                (slot.slotStatus() == LnConstants.LOCO_COMMON)) {
902                log.debug("getLocoAddress replying address {} for slot {}", address, slot.getSlot());
903                return new DccLocoAddress(address, LnThrottleManager.isLongAddress(address));
904            }
905        }
906        log.debug("getLocoAddress replying address {} for slot not in-use or for sub-consisted slot or for null slot", address);
907        return new DccLocoAddress(address, LnThrottleManager.isLongAddress(address));
908    }
909
910    /**
911     * "Dispatch" a LocoNet throttle by setting the slot as "common" then performing
912     * a slot move to slot 0.
913     * <p>
914     * The throttle being dispatched no longer has control of the loco, but other
915     * throttles may continue to control the loco.
916     *
917     * @param t throttle being dispatched
918     * @param l throttle listener to remove
919     */
920    public void dispatchThrottle(DccThrottle t, ThrottleListener l) {
921        log.debug("dispatchThrottle - throttle {}", t.getLocoAddress());
922        // set status to common & dispatch slot
923        // needs to be done one after another with no delay.
924        if (t instanceof LocoNetThrottle){
925            LocoNetThrottle lnt = (LocoNetThrottle) t;
926            LocoNetSlot tSlot = lnt.getLocoNetSlot();
927            if (tSlot != null) {
928                if (tSlot.slotStatus() != LnConstants.LOCO_COMMON) {
929                    network.sendLocoNetMessage(tSlot.writeStatus(LnConstants.LOCO_COMMON));
930                    log.debug("dispatchThrottle is dispatching slot {}", tSlot);
931                        network.sendLocoNetMessage(tSlot.dispatchSlot());
932                }
933            }
934        }
935    }
936
937    // initialize logging
938    private final static Logger log = LoggerFactory.getLogger(LocoNetThrottle.class);
939
940}