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            int highestCommon = Math.min(t.getFunctions().length, re.getMaxFnNumAsInt()+1);
276            for (int funcNum = 0; funcNum < highestCommon; funcNum++) {
277                t.setFunctionMomentary(funcNum, !(re.getFunctionLockable(funcNum)));
278            }
279        }
280    }
281
282
283    /**
284     * Send function labels for a roster entry, using old format.
285     * 
286     * This implementation is legacy and should not change from the limit of 29 functions.
287     *
288     * @param re The roster entry to get the labels from.
289     */
290    public void sendFunctionLabels(RosterEntry re) {
291
292        if (re != null) {
293            StringBuilder functionString = new StringBuilder();
294            if (whichThrottle == 'S') {
295                functionString.append("RS29}|{");
296            } else {
297                //  I know, it should have been 'RT' but this was before there were two throttles.
298                functionString.append("RF29}|{");
299            }
300            functionString.append(getCurrentAddressString());
301
302            int i;
303            for (i = 0; i < 29; i++) {
304                functionString.append("]\\[");
305                if ((re.getFunctionLabel(i) != null)) {
306                    functionString.append(re.getFunctionLabel(i));
307                }
308            }
309            for (ControllerInterface listener : controllerListeners) {
310                listener.sendPacketToDevice(functionString.toString());
311            }
312        }
313
314    }
315
316    /**
317     * send all function states, primarily for initial status Current Format:
318     * RPF}|{whichThrottle]\[function}|{state]\[function}|{state...
319     * 
320     * This implementation is legacy and should not change from the limit of 29 functions.
321     *
322     * @param t throttle to send functions to
323     */
324    public void sendAllFunctionStates(DccThrottle t) {
325
326        log.debug("Sending state of all functions");
327        StringBuilder message = new StringBuilder(buildFStatesHeader());
328
329        for (int cnt = 0; cnt < 29; cnt++) {
330            message.append("]\\[F");
331            message.append(cnt);
332            message.append("}|{");
333            message.append(t.getFunction(cnt) );
334        }
335
336        for (ControllerInterface listener : controllerListeners) {
337            listener.sendPacketToDevice(message.toString());
338        }
339
340    }
341
342    protected String buildFStatesHeader() {
343        return ("RPF}|{" + whichThrottle);
344    }
345
346    synchronized protected void sendCurrentSpeed(DccThrottle t) {
347    }
348
349    protected void sendCurrentDirection(DccThrottle t) {
350    }
351
352    protected void sendSpeedStepMode(DccThrottle t) {
353    }
354
355    protected void sendAllMomentaryStates(DccThrottle t) {
356    }
357
358    /**
359     * Figure out what the received command means, where it has to go, and
360     * translate to a jmri method.
361     *
362     * @param inPackage The package minus its prefix which steered it here.
363     * @return true to keep reading in run loop.
364     */
365    public boolean sort(String inPackage) {
366        if (inPackage.charAt(0) == 'Q') {// If device has Quit.
367            shutdownThrottle();
368            return false;
369        }
370        if (isAddressSet) {
371
372            try {
373                switch (inPackage.charAt(0)) {
374                    case 'V': // Velocity
375                        setSpeed(Integer.parseInt(inPackage.substring(1)));
376
377                        break;
378
379                    case 'X':
380                        eStop();
381
382                        break;
383
384                    case 'F': // Function
385
386                        handleFunction(inPackage);
387
388                        break;
389
390                    case 'f': //v>=2.0 Force function
391
392                        forceFunction(inPackage.substring(1));
393
394                        break;
395
396                    case 'R': // Direction
397                        setDirection(!inPackage.endsWith("0")); // 0 sets to reverse, all others forward
398                        break;
399
400                    case 'r': // Release
401                        addressRelease();
402                        break;
403
404                    case 'd': // Dispatch
405                        addressDispatch();
406                        break;
407
408                    case 'L': // Set a Long address.
409                        addressRelease();
410                        int addr = Integer.parseInt(inPackage.substring(1));
411                        setAddress(addr, true);
412                        break;
413
414                    case 'S': // Set a Short address.
415                        addressRelease();
416                        addr = Integer.parseInt(inPackage.substring(1));
417                        setAddress(addr, false);
418                        break;
419
420                    case 'E':       //v>=1.7    Address from RosterEntry
421                        addressRelease();
422                        requestEntryFromID(inPackage.substring(1));
423                        break;
424
425                    case 'C':
426                        setLocoForConsistFunctions(inPackage.substring(1));
427
428                        break;
429
430                    case 'c':       //v>=1.7      Consist Lead from RosterEntry
431                        setRosterLocoForConsistFunctions(inPackage.substring(1));
432                        break;
433
434                    case 'I':
435                        idle();
436                        break;
437
438                    case 's':       //v>=2.0
439                        handleSpeedStepMode(decodeSpeedStepMode(inPackage.substring(1)));
440                        break;
441
442                    case 'm':       //v>=2.0
443                        handleMomentary(inPackage.substring(1));
444                        break;
445
446                    case 'q':       //v>=2.0
447                        handleRequest(inPackage.substring(1));
448                        break;
449                    default:
450                        log.warn("Unhandled code: {}", inPackage.charAt(0));
451                        break;
452                }
453            } catch (NullPointerException e) {
454                log.warn("No throttle frame to receive: {}", inPackage);
455                return false;
456            }
457            try {    //  Some layout connections cannot handle rapid inputs
458                Thread.sleep(20);
459            } catch (java.lang.InterruptedException ex) {
460            }
461        } else {  //  Address not set
462            switch (inPackage.charAt(0)) {
463                case 'L': // Set a Long address.
464                    int addr = Integer.parseInt(inPackage.substring(1));
465                    setAddress(addr, true);
466                    break;
467
468                case 'S': // Set a Short address.
469                    addr = Integer.parseInt(inPackage.substring(1));
470                    setAddress(addr, false);
471                    break;
472
473                case 'E':       //v>=1.7      Address from RosterEntry
474                    requestEntryFromID(inPackage.substring(1));
475                    break;
476
477                case 'C':
478                    setLocoForConsistFunctions(inPackage.substring(1));
479
480                    break;
481
482                case 'c':       //v>=1.7      Consist Lead from RosterEntry
483                    setRosterLocoForConsistFunctions(inPackage.substring(1));
484                    break;
485
486                default:
487                    break;
488            }
489        }
490        return true;
491
492    }
493
494    private void clearLeadLoco() {
495        if (useLeadLocoF) {
496            leadLocoF.dispose();
497            functionThrottle.removePropertyChangeListener(this);
498            if (throttle != null) {
499                setFunctionThrottle(throttle);
500            }
501
502            leadLocoF = null;
503            useLeadLocoF = false;
504        }
505    }
506
507    public void setFunctionThrottle(DccThrottle t) {
508        functionThrottle = t;
509        functionThrottle.addPropertyChangeListener(this);
510    }
511
512    public void setLocoForConsistFunctions(String inPackage) {
513        /*
514         *      This is used to control speed and direction on the
515         *      consist address, but have functions mapped to lead.
516         *      Consist address must be set first!
517         */
518
519        leadAddress = new DccLocoAddress(Integer.parseInt(inPackage.substring(1)), (inPackage.charAt(0) != 'S'));
520        log.debug("Setting lead loco address: {}, for consist: {}", leadAddress, getCurrentAddressString());
521        clearLeadLoco();
522        leadLocoF = new ConsistFunctionController(this);
523        useLeadLocoF = leadLocoF.requestThrottle(leadAddress);
524
525        if (!useLeadLocoF) {
526            log.warn("Lead loco address not available.");
527            leadLocoF = null;
528        }
529    }
530
531    public void setRosterLocoForConsistFunctions(String id) {
532        RosterEntry re;
533        List<RosterEntry> l = Roster.getDefault().matchingList(null, null, null, null, null, null, id);
534        if (l.size() > 0) {
535            log.debug("Consist Lead Roster Loco found: {} for ID: {}", l.get(0).getDccAddress(), id);
536            re = l.get(0);
537            clearLeadLoco();
538            leadLocoF = new ConsistFunctionController(this, re);
539            useLeadLocoF = leadLocoF.requestThrottle(re.getDccLocoAddress());
540
541            if (!useLeadLocoF) {
542                log.warn("Lead loco address not available.");
543                leadLocoF = null;
544            }
545        } else {
546            log.debug("No Roster Loco found for: {}", id);
547        }
548    }
549
550//  Device is quitting or has lost connection
551    public void shutdownThrottle() {
552
553        try {
554            if (isAddressSet) {
555                throttle.setSpeedSetting(0);
556                addressRelease();
557            }
558        } catch (NullPointerException e) {
559            log.warn("No throttle to shutdown");
560        }
561        clearLeadLoco();
562    }
563
564    /**
565     * handle the conversion from rawSpeed to the float value needed in the
566     * DccThrottle
567     *
568     * @param rawSpeed Value sent from mobile device, range 0 - 126
569     */
570    synchronized protected void setSpeed(int rawSpeed) {
571
572        float newSpeed = (rawSpeed * speedMultiplier);
573
574        log.debug("raw: {}, NewSpd: {}", rawSpeed, newSpeed);
575        while(lastSentSpeed.offer(Float.valueOf(newSpeed))==false){
576              log.debug("failed attempting to add speed to queue");
577        }
578        throttle.setSpeedSetting(newSpeed);
579    }
580
581    protected void setDirection(boolean isForward) {
582        log.debug("set direction to: {}", (isForward ? "Fwd" : "Rev"));
583        throttle.setIsForward(isForward);
584    }
585
586    protected void eStop() {
587        throttle.setSpeedSetting(-1);
588    }
589
590    protected void idle() {
591        throttle.setSpeedSetting(0);
592    }
593
594    protected void setAddress(int number, boolean isLong) {
595        log.debug("setAddress: {}, isLong: {}", number, isLong);
596        if (rosterLoco != null) {
597            jmri.InstanceManager.throttleManagerInstance().requestThrottle(rosterLoco, this, true);
598        } else {
599            jmri.InstanceManager.throttleManagerInstance().requestThrottle(new DccLocoAddress(number, isLong), this, true);
600
601        }
602    }
603
604    public void requestEntryFromID(String id) {
605        RosterEntry re;
606        List<RosterEntry> l = Roster.getDefault().matchingList(null, null, null, null, null, null, id);
607        if (l.size() > 0) {
608            log.debug("Roster Loco found: {} for ID: {}", l.get(0).getDccAddress(), id);
609            re = l.get(0);
610            rosterLoco = re;
611            setAddress(Integer.parseInt(re.getDccAddress()), re.isLongAddress());
612        } else {
613            log.debug("No Roster Loco found for: {}", id);
614        }
615    }
616
617    public DccThrottle getThrottle() {
618        return throttle;
619    }
620
621    public DccThrottle getFunctionThrottle() {
622        return functionThrottle;
623    }
624
625    public DccLocoAddress getCurrentAddress() {
626        return (DccLocoAddress) throttle.getLocoAddress();
627    }
628
629    /**
630     * Get the string representation of this throttles address. Returns 'Not
631     * Set' if no address in use.
632     *
633     * @return string value of throttle address
634     */
635    public String getCurrentAddressString() {
636        if (isAddressSet) {
637            return ((DccLocoAddress) throttle.getLocoAddress()).toString();
638        } else {
639            return "Not Set";
640        }
641    }
642
643    /**
644     * Get the string representation of this Roster ID. Returns empty string
645     * if no address in use.
646     * since 4.15.4
647     *
648     * @return string value of throttle Roster ID
649     */
650    public String getCurrentRosterIdString() {
651        if (rosterLoco != null) {
652            return rosterLoco.getId() ;
653        } else {
654            return " ";
655        }
656    }
657
658    public void sendAddress() {
659        for (ControllerInterface listener : controllerListeners) {
660            listener.sendPacketToDevice(whichThrottle + getCurrentAddressString());
661        }
662    }
663
664// Function methods
665    protected void handleFunction(String inPackage) {
666        // get the function # sent from device
667        int receivedFunction = Integer.parseInt(inPackage.substring(2));
668        if (inPackage.charAt(1) == '1') { // Function Button down
669            log.debug("Trying to set function {}", receivedFunction);
670            // Toggle button state:
671            boolean state = functionThrottle.getFunction(receivedFunction);
672            functionThrottle.setFunction(receivedFunction, !state);
673            log.debug("Throttle: {}, Function: {}, set state: {}", functionThrottle.getLocoAddress(), receivedFunction, !state);
674        } else { // Function Button up
675
676            //  F2 is momentary for horn, unless prefs are set to follow roster entry
677            if ((isMomF2) && (receivedFunction==2)) {
678                functionThrottle.setFunction(2, false);
679                return;
680            }
681
682            // Do nothing if lockable, turn off if momentary
683            if (functionThrottle.getFunctionMomentary(receivedFunction)) {
684                functionThrottle.setFunction(receivedFunction, false);
685                log.debug("Throttle: {}, Momentary Function: {}, set false", functionThrottle.getLocoAddress(), receivedFunction);
686            }
687        }
688    }
689
690    protected void forceFunction(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);
694        throttle.setFunction(receivedFunction, newVal);
695    }
696
697    protected void handleSpeedStepMode(SpeedStepMode newMode) {
698        throttle.setSpeedStepMode(newMode);
699    }
700
701    protected void handleMomentary(String inPackage) {
702        int receivedFunction = Integer.parseInt(inPackage.substring(1));
703        boolean newVal = inPackage.charAt(0) == '1';
704        log.debug("Trying to set function {} to {}", receivedFunction,newVal ? "Momentary":"Locking");
705        throttle.setFunctionMomentary(receivedFunction, newVal);
706    }
707
708    protected void handleRequest(String inPackage) {
709        switch (inPackage.charAt(0)) {
710            case 'V': {
711                sendCurrentSpeed(throttle);
712                break;
713            }
714            case 'R': {
715                sendCurrentDirection(throttle);
716                break;
717            }
718            case 's': {
719                sendSpeedStepMode(throttle);
720                break;
721            }
722            case 'm': {
723                sendAllMomentaryStates(throttle);
724                break;
725            }
726            default:
727                log.warn("Unhandled code: {}", inPackage.charAt(0));
728                break;
729        }
730
731    }
732
733
734    private static SpeedStepMode decodeSpeedStepMode(String mode) {
735        // NOTE: old speed step modes use the original numeric values
736        // from when speed step modes were in DccThrottle. If the input does not match
737        // any of the old modes, decode based on the new speed step names.
738        if(mode.equals("1"))  {
739            return SpeedStepMode.NMRA_DCC_128;
740        } else if(mode.equals("2")) {
741            return SpeedStepMode.NMRA_DCC_28;
742        } else if(mode.equals("4")) {
743            return SpeedStepMode.NMRA_DCC_27;
744        } else if(mode.equals("8")) {
745            return SpeedStepMode.NMRA_DCC_14;
746        } else if(mode.equals("16")) {
747            return SpeedStepMode.MOTOROLA_28;
748        }
749        return SpeedStepMode.getByName(mode);
750    }
751
752    private final static Logger log = LoggerFactory.getLogger(ThrottleController.class);
753
754}