001package jmri.jmrit.withrottle;
002
003//  WiThrottle
004//
005//
006/**
007 * ThrottleController.java Sends commands to appropriate throttle component.
008 * <p>
009 * Original version sorting codes for received string from client: 'V'elocity
010 * followed by 0 - 126 'X'stop 'F'unction (1-button down, 0-button up) (0-28)
011 * e.g. F14 indicates function 4 button is pressed ` F04 indicates function 4
012 * button is released di'R'ection (0=reverse, 1=forward) 'L'ong address #,
013 * 'S'hort address # e.g. L1234 'r'elease, 'd'ispatch 'C'consist lead address,
014 * e.g. CL1235 'I'dle Idle needs to be called specifically 'Q'uit
015 * <p>
016 * Anything using added codes needs to verify version number for compatibility.
017 * Added in v1.7: 'E'ntry from roster, e.g. ESpiffy Loco 'c'consist lead from
018 * roster ID, e.g. cSpiffy Loco
019 * <p>
020 * Added in v2.0: If sent through MultiThrottle 'M' in DeviceServer, earlier
021 * versions will automatically ignore these. ('M' code did not exist prior to
022 * v2.0, so it will not forward to here) If sent through a 'T' or 'S', need to
023 * verify version number for compatibility. 'f' set a function directly.
024 * 's'peedStepMode - 1-128, 2-28, 4-27, 8-14 re'q'uest information, add the
025 * following: 'V' getSpeedSetting 'R' getIsForward 's' getSpeedStepMode 'm'
026 * getF#Momentary for all functions
027 *
028 *
029 * @author Brett Hoffman Copyright (C) 2009, 2010, 2011
030 * @author Created by Brett Hoffman on: 8/23/09.
031 */
032import java.beans.PropertyChangeEvent;
033import java.beans.PropertyChangeListener;
034import java.util.ArrayList;
035import java.util.List;
036import java.util.LinkedList;
037import java.util.Queue;
038import jmri.DccLocoAddress;
039import jmri.DccThrottle;
040import jmri.InstanceManager;
041import jmri.LocoAddress;
042import jmri.SpeedStepMode;
043import jmri.ThrottleListener;
044import jmri.jmrit.roster.Roster;
045import jmri.jmrit.roster.RosterEntry;
046import org.slf4j.Logger;
047import org.slf4j.LoggerFactory;
048
049public class ThrottleController implements ThrottleListener, PropertyChangeListener {
050
051    DccThrottle throttle;
052    DccThrottle functionThrottle;
053    RosterEntry rosterLoco = null;
054    DccLocoAddress leadAddress;
055    char whichThrottle;
056    float speedMultiplier;
057    protected Queue<Float> lastSentSpeed;
058    protected float newSpeed;
059    boolean isAddressSet;
060    protected ArrayList<ThrottleControllerListener> listeners;
061    protected ArrayList<ControllerInterface> controllerListeners;
062    boolean useLeadLocoF;
063    ConsistFunctionController leadLocoF = null;
064    String locoKey = "";
065
066    final boolean isMomF2 = InstanceManager.getDefault(WiThrottlePreferences.class).isUseMomF2();
067
068    public ThrottleController() {
069        speedMultiplier = 1.0f / 126.0f;
070        lastSentSpeed = new LinkedList<Float>();
071    }
072
073    public ThrottleController(char whichThrottleChar, ThrottleControllerListener tcl, ControllerInterface cl) {
074        this();
075        setWhichThrottle(whichThrottleChar);
076        addThrottleControllerListener(tcl);
077        addControllerListener(cl);
078    }
079
080    public void setWhichThrottle(char c) {
081        whichThrottle = c;
082    }
083
084    public void addThrottleControllerListener(ThrottleControllerListener l) {
085        if (listeners == null) {
086            listeners = new ArrayList<>(1);
087        }
088        if (!listeners.contains(l)) {
089            listeners.add(l);
090        }
091    }
092
093    public void removeThrottleControllerListener(ThrottleControllerListener l) {
094        if (listeners == null) {
095            return;
096        }
097        if (listeners.contains(l)) {
098            listeners.remove(l);
099        }
100    }
101
102    /**
103     * Add a listener to handle: listener.sendPacketToDevice(message);
104     *
105     * @param listener handle of listener to add
106     *
107     */
108    public void addControllerListener(ControllerInterface listener) {
109        if (controllerListeners == null) {
110            controllerListeners = new ArrayList<>(1);
111        }
112        if (!controllerListeners.contains(listener)) {
113            controllerListeners.add(listener);
114        }
115    }
116
117    public void removeControllerListener(ControllerInterface listener) {
118        if (controllerListeners == null) {
119            return;
120        }
121        if (controllerListeners.contains(listener)) {
122            controllerListeners.remove(listener);
123        }
124    }
125
126    /**
127     * Receive notification that an address has been released/dispatched
128     */
129    public void addressRelease() {
130        isAddressSet = false;
131        jmri.InstanceManager.throttleManagerInstance().releaseThrottle(throttle, this);
132        throttle.removePropertyChangeListener(this);
133        throttle = null;
134        rosterLoco = null;
135        sendAddress();
136        clearLeadLoco();
137        for (int i = 0; i < listeners.size(); i++) {
138            ThrottleControllerListener l = listeners.get(i);
139            l.notifyControllerAddressReleased(this);
140            log.debug("Notify TCListener address released: {}", l.getClass());
141        }
142    }
143
144    public void addressDispatch() {
145        isAddressSet = false;
146        jmri.InstanceManager.throttleManagerInstance().dispatchThrottle(throttle, this);
147        throttle.removePropertyChangeListener(this);
148        throttle = null;
149        rosterLoco = null;
150        sendAddress();
151        clearLeadLoco();
152        for (int i = 0; i < listeners.size(); i++) {
153            ThrottleControllerListener l = listeners.get(i);
154            l.notifyControllerAddressReleased(this);
155            log.debug("Notify TCListener address dispatched: {}", l.getClass());
156        }
157    }
158
159    /**
160     * Receive notification that a DccThrottle has been found and is in use.
161     *
162     * @param t The throttle which has been found
163     */
164    @Override
165    public void notifyThrottleFound(DccThrottle t) {
166        if (isAddressSet) {
167            log.debug("Throttle: {} is already set. (Found is: {})", getCurrentAddressString(), t.getLocoAddress());
168            return;
169        }
170        if (t != null) {
171            throttle = t;
172            setFunctionThrottle(throttle); // adds Property Change Listener
173            isAddressSet = true;
174            log.debug("DccThrottle found for: {}", throttle.getLocoAddress());
175        } else {
176            log.error("*throttle is null!*");
177            return;
178        }
179        for (int i = 0; i < listeners.size(); i++) {
180            ThrottleControllerListener l = listeners.get(i);
181            l.notifyControllerAddressFound(this);
182            log.debug("Notify TCListener address found: {}", l.getClass());
183        }
184
185        if (rosterLoco == null) {
186            rosterLoco = findRosterEntry(throttle);
187        }
188
189        syncThrottleFunctions(throttle, rosterLoco);
190
191        sendAddress();
192
193        sendFunctionLabels(rosterLoco);
194
195        sendAllFunctionStates(throttle);
196
197        sendCurrentSpeed(throttle);
198
199        sendCurrentDirection(throttle);
200
201        sendSpeedStepMode(throttle);
202
203    }
204
205    @Override
206    public void notifyFailedThrottleRequest(LocoAddress address, String reason) {
207        log.warn("Throttle request failed for {} because {}.", address, reason);
208        if (!(address instanceof DccLocoAddress)){
209            log.warn("Throttle address {} is not a DccLocoAddress", address);
210            return;
211        }
212        for (ThrottleControllerListener l : listeners) {
213            l.notifyControllerAddressDeclined(this, (DccLocoAddress) address, reason);
214            log.debug("Notify TCListener address declined in-use: {}", l.getClass());
215        }
216    }
217
218    /**
219     * calls notifyFailedThrottleRequest, Steal Required
220     * <p>
221     * {@inheritDoc}
222     */
223    @Override
224    public void notifyDecisionRequired(jmri.LocoAddress address, DecisionType question) {
225        notifyFailedThrottleRequest(address, "Steal Required");
226    }
227
228
229    /*
230     * Current Format:  RPF}|{whichThrottle]\[eventName}|{newValue
231     * This format may be used to send multiple function status, for initial values.
232     *
233     * Event may be from regular throttle or consist throttle, but is handled the same.
234     *
235     * Bound params: SpeedSteps, IsForward, SpeedSetting, F##, F##Momentary
236     */
237    @Override
238    public void propertyChange(PropertyChangeEvent event) {
239        String eventName = event.getPropertyName();
240        log.debug("property change: {}", eventName);
241
242        if (eventName.startsWith("F")) {
243
244            if (eventName.contains("Momentary")) {
245                return;
246            }
247            StringBuilder message = new StringBuilder("RPF}|{");
248            message.append(whichThrottle);
249            message.append("]\\[");
250            message.append(eventName);
251            message.append("}|{");
252            message.append(event.getNewValue());
253
254            for (ControllerInterface listener : controllerListeners) {
255                listener.sendPacketToDevice(message.toString());
256            }
257        }
258
259    }
260
261    public RosterEntry findRosterEntry(DccThrottle t) {
262        RosterEntry re = null;
263        if (t.getLocoAddress() != null) {
264            List<RosterEntry> l = Roster.getDefault().matchingList(null, null, "" + ((DccLocoAddress) t.getLocoAddress()).getNumber(), null, null, null, null);
265            if (l.size() > 0) {
266                log.debug("Roster Loco found: {}", l.get(0).getDccAddress());
267                re = l.get(0);
268            }
269        }
270        return re;
271    }
272
273    public void syncThrottleFunctions(DccThrottle t, RosterEntry re) {
274        if (re != null) {
275            for (int funcNum = 0; funcNum < 29; funcNum++) {
276                t.setFunctionMomentary(funcNum, !(re.getFunctionLockable(funcNum)));
277            }
278        }
279    }
280
281    public void sendFunctionLabels(RosterEntry re) {
282
283        if (re != null) {
284            StringBuilder functionString = new StringBuilder();
285            if (whichThrottle == 'S') {
286                functionString.append("RS29}|{");
287            } else {
288                //  I know, it should have been 'RT' but this was before there were two throttles.
289                functionString.append("RF29}|{");
290            }
291            functionString.append(getCurrentAddressString());
292
293            int i;
294            for (i = 0; i < 29; i++) {
295                functionString.append("]\\[");
296                if ((re.getFunctionLabel(i) != null)) {
297                    functionString.append(re.getFunctionLabel(i));
298                }
299            }
300            for (ControllerInterface listener : controllerListeners) {
301                listener.sendPacketToDevice(functionString.toString());
302            }
303        }
304
305    }
306
307    /**
308     * send all function states, primarily for initial status Current Format:
309     * RPF}|{whichThrottle]\[function}|{state]\[function}|{state...
310     *
311     * @param t throttle to send functions to
312     */
313    public void sendAllFunctionStates(DccThrottle t) {
314
315        log.debug("Sending state of all functions");
316        StringBuilder message = new StringBuilder(buildFStatesHeader());
317
318        for (int cnt = 0; cnt < 29; cnt++) {
319            message.append("]\\[F");
320            message.append(cnt);
321            message.append("}|{");
322            message.append(t.getFunction(cnt) );
323        }
324
325        for (ControllerInterface listener : controllerListeners) {
326            listener.sendPacketToDevice(message.toString());
327        }
328
329    }
330
331    protected String buildFStatesHeader() {
332        return ("RPF}|{" + whichThrottle);
333    }
334
335    synchronized protected void sendCurrentSpeed(DccThrottle t) {
336    }
337
338    protected void sendCurrentDirection(DccThrottle t) {
339    }
340
341    protected void sendSpeedStepMode(DccThrottle t) {
342    }
343
344    protected void sendAllMomentaryStates(DccThrottle t) {
345    }
346
347    /**
348     * Figure out what the received command means, where it has to go, and
349     * translate to a jmri method.
350     *
351     * @param inPackage The package minus its prefix which steered it here.
352     * @return true to keep reading in run loop.
353     */
354    public boolean sort(String inPackage) {
355        if (inPackage.charAt(0) == 'Q') {// If device has Quit.
356            shutdownThrottle();
357            return false;
358        }
359        if (isAddressSet) {
360
361            try {
362                switch (inPackage.charAt(0)) {
363                    case 'V': // Velocity
364                        setSpeed(Integer.parseInt(inPackage.substring(1)));
365
366                        break;
367
368                    case 'X':
369                        eStop();
370
371                        break;
372
373                    case 'F': // Function
374
375                        handleFunction(inPackage);
376
377                        break;
378
379                    case 'f': //v>=2.0 Force function
380
381                        forceFunction(inPackage.substring(1));
382
383                        break;
384
385                    case 'R': // Direction
386                        setDirection(!inPackage.endsWith("0")); // 0 sets to reverse, all others forward
387                        break;
388
389                    case 'r': // Release
390                        addressRelease();
391                        break;
392
393                    case 'd': // Dispatch
394                        addressDispatch();
395                        break;
396
397                    case 'L': // Set a Long address.
398                        addressRelease();
399                        int addr = Integer.parseInt(inPackage.substring(1));
400                        setAddress(addr, true);
401                        break;
402
403                    case 'S': // Set a Short address.
404                        addressRelease();
405                        addr = Integer.parseInt(inPackage.substring(1));
406                        setAddress(addr, false);
407                        break;
408
409                    case 'E':       //v>=1.7    Address from RosterEntry
410                        addressRelease();
411                        requestEntryFromID(inPackage.substring(1));
412                        break;
413
414                    case 'C':
415                        setLocoForConsistFunctions(inPackage.substring(1));
416
417                        break;
418
419                    case 'c':       //v>=1.7      Consist Lead from RosterEntry
420                        setRosterLocoForConsistFunctions(inPackage.substring(1));
421                        break;
422
423                    case 'I':
424                        idle();
425                        break;
426
427                    case 's':       //v>=2.0
428                        handleSpeedStepMode(decodeSpeedStepMode(inPackage.substring(1)));
429                        break;
430
431                    case 'm':       //v>=2.0
432                        handleMomentary(inPackage.substring(1));
433                        break;
434
435                    case 'q':       //v>=2.0
436                        handleRequest(inPackage.substring(1));
437                        break;
438                    default:
439                        log.warn("Unhandled code: {}", inPackage.charAt(0));
440                        break;
441                }
442            } catch (NullPointerException e) {
443                log.warn("No throttle frame to receive: {}", inPackage);
444                return false;
445            }
446            try {    //  Some layout connections cannot handle rapid inputs
447                Thread.sleep(20);
448            } catch (java.lang.InterruptedException ex) {
449            }
450        } else {  //  Address not set
451            switch (inPackage.charAt(0)) {
452                case 'L': // Set a Long address.
453                    int addr = Integer.parseInt(inPackage.substring(1));
454                    setAddress(addr, true);
455                    break;
456
457                case 'S': // Set a Short address.
458                    addr = Integer.parseInt(inPackage.substring(1));
459                    setAddress(addr, false);
460                    break;
461
462                case 'E':       //v>=1.7      Address from RosterEntry
463                    requestEntryFromID(inPackage.substring(1));
464                    break;
465
466                case 'C':
467                    setLocoForConsistFunctions(inPackage.substring(1));
468
469                    break;
470
471                case 'c':       //v>=1.7      Consist Lead from RosterEntry
472                    setRosterLocoForConsistFunctions(inPackage.substring(1));
473                    break;
474
475                default:
476                    break;
477            }
478        }
479        return true;
480
481    }
482
483    private void clearLeadLoco() {
484        if (useLeadLocoF) {
485            leadLocoF.dispose();
486            functionThrottle.removePropertyChangeListener(this);
487            if (throttle != null) {
488                setFunctionThrottle(throttle);
489            }
490
491            leadLocoF = null;
492            useLeadLocoF = false;
493        }
494    }
495
496    public void setFunctionThrottle(DccThrottle t) {
497        functionThrottle = t;
498        functionThrottle.addPropertyChangeListener(this);
499    }
500
501    public void setLocoForConsistFunctions(String inPackage) {
502        /*
503         *      This is used to control speed and direction on the
504         *      consist address, but have functions mapped to lead.
505         *      Consist address must be set first!
506         */
507
508        leadAddress = new DccLocoAddress(Integer.parseInt(inPackage.substring(1)), (inPackage.charAt(0) != 'S'));
509        log.debug("Setting lead loco address: {}, for consist: {}", leadAddress, getCurrentAddressString());
510        clearLeadLoco();
511        leadLocoF = new ConsistFunctionController(this);
512        useLeadLocoF = leadLocoF.requestThrottle(leadAddress);
513
514        if (!useLeadLocoF) {
515            log.warn("Lead loco address not available.");
516            leadLocoF = null;
517        }
518    }
519
520    public void setRosterLocoForConsistFunctions(String id) {
521        RosterEntry re;
522        List<RosterEntry> l = Roster.getDefault().matchingList(null, null, null, null, null, null, id);
523        if (l.size() > 0) {
524            log.debug("Consist Lead Roster Loco found: {} for ID: {}", l.get(0).getDccAddress(), id);
525            re = l.get(0);
526            clearLeadLoco();
527            leadLocoF = new ConsistFunctionController(this, re);
528            useLeadLocoF = leadLocoF.requestThrottle(re.getDccLocoAddress());
529
530            if (!useLeadLocoF) {
531                log.warn("Lead loco address not available.");
532                leadLocoF = null;
533            }
534        } else {
535            log.debug("No Roster Loco found for: {}", id);
536        }
537    }
538
539//  Device is quitting or has lost connection
540    public void shutdownThrottle() {
541
542        try {
543            if (isAddressSet) {
544                throttle.setSpeedSetting(0);
545                addressRelease();
546            }
547        } catch (NullPointerException e) {
548            log.warn("No throttle to shutdown");
549        }
550        clearLeadLoco();
551    }
552
553    /**
554     * handle the conversion from rawSpeed to the float value needed in the
555     * DccThrottle
556     *
557     * @param rawSpeed Value sent from mobile device, range 0 - 126
558     */
559    synchronized protected void setSpeed(int rawSpeed) {
560
561        float newSpeed = (rawSpeed * speedMultiplier);
562
563        log.debug("raw: {}, NewSpd: {}", rawSpeed, newSpeed);
564        while(lastSentSpeed.offer(Float.valueOf(newSpeed))==false){
565              log.debug("failed attempting to add speed to queue");
566        }
567        throttle.setSpeedSetting(newSpeed);
568    }
569
570    protected void setDirection(boolean isForward) {
571        log.debug("set direction to: {}", (isForward ? "Fwd" : "Rev"));
572        throttle.setIsForward(isForward);
573    }
574
575    protected void eStop() {
576        throttle.setSpeedSetting(-1);
577    }
578
579    protected void idle() {
580        throttle.setSpeedSetting(0);
581    }
582
583    protected void setAddress(int number, boolean isLong) {
584        log.debug("setAddress: {}, isLong: {}", number, isLong);
585        if (rosterLoco != null) {
586            jmri.InstanceManager.throttleManagerInstance().requestThrottle(rosterLoco, this, true);
587        } else {
588            jmri.InstanceManager.throttleManagerInstance().requestThrottle(new DccLocoAddress(number, isLong), this, true);
589
590        }
591    }
592
593    public void requestEntryFromID(String id) {
594        RosterEntry re;
595        List<RosterEntry> l = Roster.getDefault().matchingList(null, null, null, null, null, null, id);
596        if (l.size() > 0) {
597            log.debug("Roster Loco found: {} for ID: {}", l.get(0).getDccAddress(), id);
598            re = l.get(0);
599            rosterLoco = re;
600            setAddress(Integer.parseInt(re.getDccAddress()), re.isLongAddress());
601        } else {
602            log.debug("No Roster Loco found for: {}", id);
603        }
604    }
605
606    public DccThrottle getThrottle() {
607        return throttle;
608    }
609
610    public DccThrottle getFunctionThrottle() {
611        return functionThrottle;
612    }
613
614    public DccLocoAddress getCurrentAddress() {
615        return (DccLocoAddress) throttle.getLocoAddress();
616    }
617
618    /**
619     * Get the string representation of this throttles address. Returns 'Not
620     * Set' if no address in use.
621     *
622     * @return string value of throttle address
623     */
624    public String getCurrentAddressString() {
625        if (isAddressSet) {
626            return ((DccLocoAddress) throttle.getLocoAddress()).toString();
627        } else {
628            return "Not Set";
629        }
630    }
631
632    /**
633     * Get the string representation of this Roster ID. Returns empty string
634     * if no address in use.
635     * since 4.15.4
636     *
637     * @return string value of throttle Roster ID
638     */
639    public String getCurrentRosterIdString() {
640        if (rosterLoco != null) {
641            return rosterLoco.getId() ;
642        } else {
643            return " ";
644        }
645    }
646
647    public void sendAddress() {
648        for (ControllerInterface listener : controllerListeners) {
649            listener.sendPacketToDevice(whichThrottle + getCurrentAddressString());
650        }
651    }
652
653// Function methods
654    protected void handleFunction(String inPackage) {
655        // get the function # sent from device
656        int receivedFunction = Integer.parseInt(inPackage.substring(2));
657        if (inPackage.charAt(1) == '1') { // Function Button down
658            log.debug("Trying to set function {}", receivedFunction);
659            // Toggle button state:
660            boolean state = functionThrottle.getFunction(receivedFunction);
661            functionThrottle.setFunction(receivedFunction, !state);
662            log.debug("Throttle: {}, Function: {}, set state: {}", functionThrottle.getLocoAddress(), receivedFunction, !state);
663        } else { // Function Button up
664
665            //  F2 is momentary for horn, unless prefs are set to follow roster entry
666            if ((isMomF2) && (receivedFunction==2)) {
667                functionThrottle.setFunction(2, false);
668                return;
669            }
670
671            // Do nothing if lockable, turn off if momentary
672            if (functionThrottle.getFunctionMomentary(receivedFunction)) {
673                functionThrottle.setFunction(receivedFunction, false);
674                log.debug("Throttle: {}, Momentary Function: {}, set false", functionThrottle.getLocoAddress(), receivedFunction);
675            }
676        }
677    }
678
679    protected void forceFunction(String inPackage) {
680        int receivedFunction = Integer.parseInt(inPackage.substring(1));
681        boolean newVal = inPackage.charAt(0) == '1';
682        log.debug("Trying to set function {} to {}", receivedFunction,newVal);
683        throttle.setFunction(receivedFunction, newVal);
684    }
685
686    protected void handleSpeedStepMode(SpeedStepMode newMode) {
687        throttle.setSpeedStepMode(newMode);
688    }
689
690    protected void handleMomentary(String inPackage) {
691        int receivedFunction = Integer.parseInt(inPackage.substring(1));
692        boolean newVal = inPackage.charAt(0) == '1';
693        log.debug("Trying to set function {} to {}", receivedFunction,newVal ? "Momentary":"Locking");
694        throttle.setFunctionMomentary(receivedFunction, newVal);
695    }
696
697    protected void handleRequest(String inPackage) {
698        switch (inPackage.charAt(0)) {
699            case 'V': {
700                sendCurrentSpeed(throttle);
701                break;
702            }
703            case 'R': {
704                sendCurrentDirection(throttle);
705                break;
706            }
707            case 's': {
708                sendSpeedStepMode(throttle);
709                break;
710            }
711            case 'm': {
712                sendAllMomentaryStates(throttle);
713                break;
714            }
715            default:
716                log.warn("Unhandled code: {}", inPackage.charAt(0));
717                break;
718        }
719
720    }
721
722
723    private static SpeedStepMode decodeSpeedStepMode(String mode) {
724        // NOTE: old speed step modes use the original numeric values
725        // from when speed step modes were in DccThrottle. If the input does not match
726        // any of the old modes, decode based on the new speed step names.
727        if(mode.equals("1"))  {
728            return SpeedStepMode.NMRA_DCC_128;
729        } else if(mode.equals("2")) {
730            return SpeedStepMode.NMRA_DCC_28;
731        } else if(mode.equals("4")) {
732            return SpeedStepMode.NMRA_DCC_27;
733        } else if(mode.equals("8")) {
734            return SpeedStepMode.NMRA_DCC_14;
735        } else if(mode.equals("16")) {
736            return SpeedStepMode.MOTOROLA_28;
737        }
738        return SpeedStepMode.getByName(mode);
739    }
740
741    private final static Logger log = LoggerFactory.getLogger(ThrottleController.class);
742
743}