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