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    @Override
574    public void setSpeedSetting(float speed, boolean allowDuplicates, boolean allowDuplicatesOnStop) {
575        log.debug("setSpeedSetting: called with speed {} for LocoNet slot {} allowDup {} allowDupOnStop {}",
576                    speed, slot.getSlot(), allowDuplicates, allowDuplicatesOnStop);
577        if (LnConstants.CONSIST_MID == slot.consistStatus()
578                || LnConstants.CONSIST_SUB == slot.consistStatus()) {
579            // Digitrax slots use the same memory location to store the
580            // speed AND the slot to which a locomotive is consisted.
581            // if the locomotive is either a CONSIST_MID or a CONSIST_SUB,
582            // we need to ignore the request to change the speed
583            log.debug("Attempt to change speed on locomotive {} which is a {}", getLocoAddress(), LnConstants.CONSIST_STAT(slot.consistStatus()));
584            return;
585        }
586        float oldSpeed;
587        synchronized(this) {
588            oldSpeed = this.speedSetting;
589            this.speedSetting = speed;
590            if (speed < 0) {
591                this.speedSetting = -1.f;
592            }
593        }
594
595        new_spd = intSpeed(speed);
596
597        // decide whether to send a new LocoNet message
598        boolean sendLoconetMessage = false;
599        if (new_spd != layout_spd ) {
600            // the new speed is different - send a message
601            sendLoconetMessage = true;
602        } else if (allowDuplicates) {
603            // calling method wants a new message sent regardless
604            sendLoconetMessage = true;
605        } else if (allowDuplicatesOnStop && new_spd <= 1) {
606            // calling method wants a new message sent if the speed is idle or estop, which it is
607            sendLoconetMessage = true;
608        }
609
610        if (sendLoconetMessage) {
611            log.debug("setSpeedSetting: sending speed {} to LocoNet slot {}", speed, slot.getSlot());
612            if (slot.getProtocol() != LnConstants.LOCONETPROTOCOL_TWO) {
613                LocoNetMessage msg = new LocoNetMessage(4);
614                msg.setOpCode(LnConstants.OPC_LOCO_SPD);
615                msg.setElement(1, slot.getSlot());
616                log.debug("setSpeedSetting: float speed: {} LocoNet speed: {}", speed, new_spd);
617                msg.setElement(2, new_spd);
618                network.sendLocoNetMessage(msg);
619            } else {
620                sendExpSpeedAndDirection(new_spd);
621            }
622
623            // reset timeout - but only if something sent on net
624            if (mRefreshTimer != null) {
625                mRefreshTimer.stop();
626                mRefreshTimer.setRepeats(true);     // refresh until stopped by dispose
627                mRefreshTimer.start();
628                log.debug("Initially starting refresh timer for slot {} address {}", slot.getSlot(), slot.locoAddr());
629            }
630        } else {
631            log.debug("setSpeedSetting: not sending LocoNet speed message to slot {}, new({})==old({})", slot.getSlot(), new_spd, layout_spd);
632        }
633        synchronized(this) {
634            firePropertyChange(SPEEDSETTING, oldSpeed, this.speedSetting);
635        }
636        log.debug("about to invoke record({})", speed);
637        record(speed);
638    }
639
640    /**
641     * Send a LocoNet message containing the specified direction of travel.
642     *
643     * LocoNet actually puts forward and backward in the same message as the
644     * first function group.
645     *
646     * @param forward is true for forward movement, else false
647     */
648    @Override
649    public void setIsForward(boolean forward) {
650        boolean old = isForward;
651        isForward = forward;
652        log.debug("setIsForward to {}, old value {}", isForward, old);
653        if (slot.getProtocol() != LnConstants.LOCONETPROTOCOL_TWO) {
654            sendFunctionGroup1();
655        } else {
656            sendExpSpeedAndDirection(forward);
657        }
658        firePropertyChange(ISFORWARD, old, this.isForward);
659    }
660
661    /**
662     * Get the LocoNetSlot which is used for controlling the loco assoicated
663     * with this throttle.
664     *
665     * @return the LocoNetSlot
666     */
667    @CheckForNull
668    public LocoNetSlot getLocoNetSlot() {
669        if (slot == null) return slot;
670        log.debug("getLocoNetSlot is returning slot {}", slot.getSlot());
671        return slot;
672    }
673
674    @Override
675    public String toString() {
676        return getLocoAddress().toString();
677    }
678
679    /**
680     * Dispose the LocoNetThrottle when finished with this object.
681     *
682     * After this is executed, further use of this Throttle object will
683     * result in a JmriException.
684     */
685    @Override
686    public void throttleDispose() {
687        if (isDisposing) return;
688        log.debug("throttleDispose - disposing of throttle (and setting slot = null)");
689        isDisposing = true;
690
691        // Release throttle connections
692        if (slot != null) {
693            if (slot.slotStatus() == LnConstants.LOCO_IN_USE  ) {
694                // Digitrax throttles do not set the slot speed to zero, so do
695                // not do so here.
696
697                // Make the slot common, after a little wait
698                log.debug("dispatchThrottle is dispatching slot {}", slot);
699                network.sendLocoNetMessage(slot.releaseSlot());
700            }
701            // Can remove the slot listener at any time; any further messages
702            // aren't needed.
703            slot.removeSlotListener(this);
704            // Stop the throttle speed refresh timer
705            if (mRefreshTimer != null) {
706                mRefreshTimer.stop();
707                log.debug("Stopped refresh timer for slot {} address {} as part of throttleDispose", slot.getSlot(), slot.locoAddr());
708            mRefreshTimer = null;
709            }
710
711            slot = null;
712            network = null;
713
714            finishRecord();
715            isDisposing = false;
716        }
717    }
718
719    javax.swing.Timer mRefreshTimer = null;
720
721    /**
722     * Start the "refresh" timer.  The "refresh" timer determines
723     * when to send a new LocoNet message to "refresh" the slot's speed
724     * setting, so that the slot does not get "purged".
725     *
726     */
727    protected void startRefresh() {
728        mRefreshTimer = new javax.swing.Timer(50000, e -> timeout());
729        mRefreshTimer.setRepeats(true);     // refresh until stopped by dispose
730        mRefreshTimer.start();
731        log.debug("Starting refresh timer for slot {} address {}", slot.getSlot(), slot.locoAddr());
732    }
733
734    /**
735     * Internal routine to resend the speed on a timeout
736     */
737    protected synchronized void timeout() {
738        if (slot != null) {
739            log.debug("refresh timer timed-out on slot {}", slot.getSlot());
740            // clear the last known layout_spd so that we will actually send the
741            // message.
742            layout_spd = -1;
743            setSpeedSetting(speedSetting);
744        }
745        else {
746            log.debug("refresh timer time-out on a null slot");
747        }
748    }
749
750    /**
751     * Get notified when underlying slot acquisition process fails.  Slot acquisition
752     * failure is handled by @link LnThrottleManager, so no code is required here.
753     *
754     * @param addr Locomotive address
755     * @param s reason the acquisition failed
756     */
757    public void notifyRefused(int addr, String s) {
758        // don't do anything here; is handled by LnThrottleManager.
759    }
760
761
762    /**
763     * Get notified when underlying slot information changes
764     *
765     * @param pSlot the slot which was changed
766     */
767    @SuppressFBWarnings(value = "FE_FLOATING_POINT_EQUALITY") // OK to compare floating point, notify on any change
768    @Override
769    public void notifyChangedSlot(LocoNetSlot pSlot) {
770        log.debug("notifyChangedSlot executing for slot {}, slotStatus {}", slot.getSlot(), Integer.toHexString(slot.slotStatus()));
771        if (slot != pSlot) {
772            log.error("notified of change in different slot");
773        }
774
775        if(!slot.getIsInitilized() && slot.slotStatus() == LnConstants.LOCO_IN_USE){
776           log.debug("Attempting to update slot with this JMRI instance's throttle id ({})", throttleManager.getThrottleID());
777           network.sendLocoNetMessage(slot.writeThrottleID(throttleManager.getThrottleID()));
778           // finally we are done...
779           slot.setIsInitialized(true);
780           throttleManager.notifyComplete(this, slot);
781        }
782
783        // Save current layout state of spd/dirf/snd so we won't run amok
784        // toggling values if another LocoNet entity accesses the slot while
785        // our most recent change request is still in-flight.
786        layout_spd = slot.speed();
787        layout_dirf = slot.dirf();
788        layout_snd = slot.snd();
789
790        // handle change in each state
791        synchronized(this) {
792            if (this.speedSetting != floatSpeed(slot.speed())) {
793                float old = this.speedSetting;
794                this.speedSetting = floatSpeed(slot.speed());
795                log.debug("notifyChangedSlot: old speed: {} new speed: {}", old, this.speedSetting); // NOI18N
796                firePropertyChange(SPEEDSETTING, old, this.speedSetting);
797            }
798        }
799        firePropertyChange(ISFORWARD, this.isForward, this.isForward = slot.isForward());
800
801        // Slot status
802        if (slotStatus != slot.slotStatus()) {
803            int newStat = slot.slotStatus();
804            log.debug("Slot status changed from {} to {}", LnConstants.LOCO_STAT(slotStatus), LnConstants.LOCO_STAT(newStat)); // NOI18N
805            // PropertyChangeListeners notification: ThrottleConnected from True to False when disconnected
806            firePropertyChange("ThrottleConnected", (slotStatus & LnConstants.LOCOSTAT_MASK) == LnConstants.LOCO_IN_USE, // NOI18N
807                    !((slotStatus & LnConstants.LOCOSTAT_MASK) == LnConstants.LOCO_IN_USE));
808            slotStatus = newStat;
809        }
810
811        // It is possible that the slot status change we are being notified of
812        // is the slot being set to status COMMON. In which case the slot just
813        // got set to null. No point in continuing. In fact to do so causes a NPE.
814        if (slot == null) {
815            return;
816        }
817
818        switch (slot.decoderType()) {
819            case LnConstants.DEC_MODE_128:
820            case LnConstants.DEC_MODE_128A:
821                if(SpeedStepMode.NMRA_DCC_128 != getSpeedStepMode()) {
822                   setSpeedStepMode(SpeedStepMode.NMRA_DCC_128);
823                }
824                break;
825            case LnConstants.DEC_MODE_28:
826            case LnConstants.DEC_MODE_28A:
827            case LnConstants.DEC_MODE_28TRI:
828                if(SpeedStepMode.NMRA_DCC_28 != getSpeedStepMode()) {
829                   setSpeedStepMode(SpeedStepMode.NMRA_DCC_28);
830                }
831                break;
832            case LnConstants.DEC_MODE_14:
833                if(SpeedStepMode.NMRA_DCC_14 != getSpeedStepMode()) {
834                   setSpeedStepMode(SpeedStepMode.NMRA_DCC_14);
835                }
836                break;
837            default:
838                log.warn("Unhandled decoder type: {}", slot.decoderType());
839                break;
840        }
841
842        // Functions
843        updateFunctions();
844
845        log.debug("notifyChangedSlot ends");
846    }
847
848    /**
849     * update the F0-F29 functions.
850     * Invoked by notifyChangedSlot(), this nominally updates from the slot.
851     */
852    protected void updateFunctions() {
853        for (int i = 0; i < 29; i++) {
854            log.debug("updateFunction({}, {})", i, slot.isFunction(i));
855            if (i==20 && log.isTraceEnabled()) log.trace("Tracing back F20", new Exception("traceback"));
856            updateFunction(i,slot.isFunction(i));
857        }
858    }
859
860    /**
861     * Set the speed step value and the related speedIncrement value.
862     *
863     * @param Mode the current speed step mode - default should be 128
864     *             speed step mode in most cases
865     */
866    @Override
867    public void setSpeedStepMode(SpeedStepMode Mode) {
868        int status = slot.slotStatus();
869        log.debug("Speed Step Mode Change to Mode: {} Current mode is: {}", Mode, this.speedStepMode); // NOI18N
870        log.debug("Current Slot Mode: {}", LnConstants.DEC_MODE(status)); // NOI18N
871        firePropertyChange(SPEEDSTEPS, this.speedStepMode, this.speedStepMode = Mode);
872        if (Mode == SpeedStepMode.NMRA_DCC_14) {
873            log.debug("14 speed step change"); // NOI18N
874            status = status & ((~LnConstants.DEC_MODE_MASK)
875                    | LnConstants.STAT1_SL_SPDEX)
876                    | LnConstants.DEC_MODE_14;
877        } else if (Mode == SpeedStepMode.MOTOROLA_28) {
878            log.debug("28-Tristate speed step change");
879            status = status & ((~LnConstants.DEC_MODE_MASK)
880                    | LnConstants.STAT1_SL_SPDEX)
881                    | LnConstants.DEC_MODE_28TRI;
882        } else if (Mode == SpeedStepMode.NMRA_DCC_28) {
883            log.debug("28 speed step change");
884            status = status & ((~LnConstants.DEC_MODE_MASK)
885                    | LnConstants.STAT1_SL_SPDEX);
886            // | LnConstants.DEC_MODE_28;      // DEC_MODE_28 has a zero value, here for documentation
887            // it unfortunately shows a INT_VACUOUS_BIT_OPERATION in SpotBugs
888            // and I don't want to annote that around this entire long method
889        } else { // default to 128 speed step mode
890            log.debug("128 speed step change");
891            status = status & ((~LnConstants.DEC_MODE_MASK)
892                    | LnConstants.STAT1_SL_SPDEX)
893                    | LnConstants.DEC_MODE_128;
894        }
895        log.debug("New Slot Mode: {}", LnConstants.DEC_MODE(status));
896        if (slot.getIsInitilized() )
897            // check that the throttle is completely initialized.
898        {
899            network.sendLocoNetMessage(slot.writeMode(status));
900        }
901    }
902
903    /**
904     * Get the address controlled by this throttle. If the throttle is controlling.
905     *
906     * @return a LocoAddress for the address controlled by this throttle
907     */
908    @Override
909    public LocoAddress getLocoAddress() {
910        if (slot != null) {
911            if ((slot.slotStatus() == LnConstants.LOCO_IN_USE) ||
912                (slot.slotStatus() == LnConstants.LOCO_COMMON)) {
913                log.debug("getLocoAddress replying address {} for slot {}", address, slot.getSlot());
914                return new DccLocoAddress(address, LnThrottleManager.isLongAddress(address));
915            }
916        }
917        log.debug("getLocoAddress replying address {} for slot not in-use or for sub-consisted slot or for null slot", address);
918        return new DccLocoAddress(address, LnThrottleManager.isLongAddress(address));
919    }
920
921    /**
922     * "Dispatch" a LocoNet throttle by setting the slot as "common" then performing
923     * a slot move to slot 0.
924     * <p>
925     * The throttle being dispatched no longer has control of the loco, but other
926     * throttles may continue to control the loco.
927     *
928     * @param t throttle being dispatched
929     * @param l throttle listener to remove
930     */
931    public void dispatchThrottle(DccThrottle t, ThrottleListener l) {
932        log.debug("dispatchThrottle - throttle {}", t.getLocoAddress());
933        // set status to common & dispatch slot
934        // needs to be done one after another with no delay.
935        if (t instanceof LocoNetThrottle){
936            LocoNetThrottle lnt = (LocoNetThrottle) t;
937            LocoNetSlot tSlot = lnt.getLocoNetSlot();
938            if (tSlot != null) {
939                if (tSlot.slotStatus() != LnConstants.LOCO_COMMON) {
940                    network.sendLocoNetMessage(tSlot.writeStatus(LnConstants.LOCO_COMMON));
941                    log.debug("dispatchThrottle is dispatching slot {}", tSlot);
942                        network.sendLocoNetMessage(tSlot.dispatchSlot());
943                }
944            }
945        }
946    }
947
948    // initialize logging
949    private final static Logger log = LoggerFactory.getLogger(LocoNetThrottle.class);
950
951}