001package jmri.jmrit.withrottle;
002
003import java.beans.PropertyChangeEvent;
004
005import jmri.*;
006import jmri.jmrit.roster.RosterEntry;
007import org.slf4j.Logger;
008import org.slf4j.LoggerFactory;
009
010/**
011 * @author Brett Hoffman Copyright (C) 2011
012 */
013public class MultiThrottleController extends ThrottleController {
014    
015    protected boolean isStealAddress;
016
017    public MultiThrottleController(char id, String key, ThrottleControllerListener tcl, ControllerInterface ci) {
018        super(id, tcl, ci);
019        log.debug("New MT controller");
020        locoKey = key;
021        isStealAddress = false;
022    }
023
024    /**
025     * Builds a header to send to the wi-fi device for use in a message.
026     * Includes a separator - {@literal <;>}
027     *
028     * @param chr the character indicating what action is performed
029     * @return a pre-assembled header for this DccThrottle
030     */
031    public String buildPacketWithChar(char chr) {
032        return ("M" + whichThrottle + chr + locoKey + "<;>");
033    }
034
035
036    /*
037     * Send a message to the wi-fi device that a bound property of a DccThrottle
038     * has changed.  Currently only handles function state.
039     * Current Format:  Header + F(0 or 1) + function number
040     *
041     * Event may be from regular throttle or consist throttle, but is handled the same.
042     *
043     * Bound params: SpeedSteps, IsForward, SpeedSetting, F##, F##Momentary
044     */
045    @Override
046    public void propertyChange(PropertyChangeEvent event) {
047        String eventName = event.getPropertyName();
048        log.debug("property change: {}",eventName);
049        if (eventName.startsWith("F")) {
050            if (eventName.contains("Momentary")) {
051                return;
052            }
053            StringBuilder message = new StringBuilder(buildPacketWithChar('A'));
054
055            try {
056                if ((Boolean) event.getNewValue()) {
057                    message.append("F1");
058                } else {
059                    message.append("F0");
060                }
061                message.append(eventName.substring(1));
062            } catch (ClassCastException cce) {
063                log.debug("Invalid event value. {}", cce.getMessage());
064            } catch (IndexOutOfBoundsException oob) {
065                log.debug("Invalid event name. {}", oob.getMessage());
066            }
067
068            for (ControllerInterface listener : controllerListeners) {
069                listener.sendPacketToDevice(message.toString());
070            }
071        }
072        if (eventName.matches(Throttle.SPEEDSTEPS)) {
073            StringBuilder message = new StringBuilder(buildPacketWithChar('A'));
074            message.append("s");
075            message.append(encodeSpeedStepMode((SpeedStepMode)event.getNewValue()));
076            for (ControllerInterface listener : controllerListeners) {
077                listener.sendPacketToDevice(message.toString());
078            }
079        }
080        if (eventName.matches(Throttle.ISFORWARD)) {
081            StringBuilder message = new StringBuilder(buildPacketWithChar('A'));
082            message.append("R");
083            message.append((Boolean) event.getNewValue() ? "1" : "0");
084            for (ControllerInterface listener : controllerListeners) {
085               listener.sendPacketToDevice(message.toString());
086            }
087        }
088        if (eventName.matches(Throttle.SPEEDSETTING)) {
089            float currentSpeed = ((Float) event.getNewValue()).floatValue();
090            log.debug("Speed Setting: {} head of queue {}",currentSpeed, lastSentSpeed.peek());
091            if(lastSentSpeed.isEmpty()) { 
092               StringBuilder message = new StringBuilder(buildPacketWithChar('A'));
093               message.append("V");
094               message.append(Math.round(currentSpeed / speedMultiplier));
095               for (ControllerInterface listener : controllerListeners) {
096                   listener.sendPacketToDevice(message.toString());
097               }
098            } else {
099               if( Math.abs(lastSentSpeed.peek().floatValue()-currentSpeed)<0.0005 ) {
100                  Float f = lastSentSpeed.poll(); // remove the value from the list.
101                  log.debug("removed value {} from queue",f);
102               }
103            }
104        }
105    }
106
107    /**
108     * {@inheritDoc}
109     */
110    @Override
111    public void sendFunctionLabels(RosterEntry re) {
112
113        if (re != null) {
114            StringBuilder functionString = new StringBuilder(buildPacketWithChar('L'));
115
116            int i;
117            for (i = 0; i < 29; i++) {
118                functionString.append("]\\[");
119                if ((re.getFunctionLabel(i) != null)) {
120                    functionString.append(re.getFunctionLabel(i));
121                }
122            }
123            for (ControllerInterface listener : controllerListeners) {
124                listener.sendPacketToDevice(functionString.toString());
125            }
126        }
127    }
128
129    /**
130     * This replaces the previous method of sending a string of function states,
131     * and now sends them individually, the same as a property change would.
132     *
133     * @param t the throttle to send the states of.
134     */
135    @Override
136    public void sendAllFunctionStates(DccThrottle t) {
137        log.debug("Sending state of all functions");
138        for (int cnt = 0; cnt < 29; cnt++) {
139            StringBuilder message = new StringBuilder(buildPacketWithChar('A'));
140            message.append( t.getFunction(cnt) ? "F1" : "F0" );
141            message.append(cnt);
142            controllerListeners.forEach(listener -> {
143                listener.sendPacketToDevice(message.toString());
144            });
145        }
146    }
147
148    /**
149     * {@inheritDoc}
150     */
151    @Override
152    synchronized protected void sendCurrentSpeed(DccThrottle t) {
153        float currentSpeed = t.getSpeedSetting();
154        StringBuilder message = new StringBuilder(buildPacketWithChar('A'));
155        message.append("V");
156        message.append(Math.round(currentSpeed / speedMultiplier));
157        for (ControllerInterface listener : controllerListeners) {
158            listener.sendPacketToDevice(message.toString());
159        }
160    }
161
162    /**
163     * {@inheritDoc}
164     */
165    @Override
166    protected void sendCurrentDirection(DccThrottle t) {
167        StringBuilder message = new StringBuilder(buildPacketWithChar('A'));
168        message.append("R");
169        message.append(t.getIsForward() ? "1" : "0");
170        for (ControllerInterface listener : controllerListeners) {
171            listener.sendPacketToDevice(message.toString());
172        }
173    }
174
175    /**
176     * {@inheritDoc}
177     */
178    @Override
179    protected void sendSpeedStepMode(DccThrottle t) {
180        StringBuilder message = new StringBuilder(buildPacketWithChar('A'));
181        message.append("s");
182        message.append(encodeSpeedStepMode(throttle.getSpeedStepMode()));
183        for (ControllerInterface listener : controllerListeners) {
184            listener.sendPacketToDevice(message.toString());
185        }
186    }
187
188    /**
189     * {@inheritDoc}
190     */
191    @Override
192    protected void sendAllMomentaryStates(DccThrottle t) {
193        log.debug("Sending momentary state of all functions");
194        for (int cnt = 0; cnt < 29; cnt++) {
195            StringBuilder message = new StringBuilder(buildPacketWithChar('A'));
196            message.append( t.getFunctionMomentary(cnt) ? "m1" : "m0" );
197            message.append(cnt);
198            controllerListeners.forEach(listener -> {
199                listener.sendPacketToDevice(message.toString());
200            });
201        }
202    }
203
204    /**
205     * {@inheritDoc} A + indicates the address was acquired, - indicates
206     * released
207     */
208    @Override
209    public void sendAddress() {
210        for (ControllerInterface listener : controllerListeners) {
211            if (isAddressSet) {
212                listener.sendPacketToDevice(buildPacketWithChar('+'));
213            } else {
214                listener.sendPacketToDevice(buildPacketWithChar('-'));
215            }
216        }
217    }
218
219    /**
220     * Send a message to a device that steal is needed. This message can be sent 
221     * back to JMRI verbatim to complete a steal.
222     */
223    public void sendStealAddress() {
224        StringBuilder message = new StringBuilder(buildPacketWithChar('S'));
225        message.append(locoKey);
226        for (ControllerInterface listener : controllerListeners) {
227            listener.sendPacketToDevice(message.toString());
228        }
229    }
230
231    /**
232     * A decision is required for Throttle creation to continue.
233     * <p>
234     * Steal / Cancel, Share / Cancel, or Steal / Share Cancel
235     * <p>
236     * Callback of a request for an address that is in use.
237     * Will initiate a steal only if this MTC is flagged to do so.
238     * Otherwise, it will remove the request for the address.
239     *
240     * {@inheritDoc}
241     */
242    @Override
243    public void notifyDecisionRequired(LocoAddress address, DecisionType question) {
244        if ( question == DecisionType.STEAL ){
245            if (isStealAddress) {
246                //  Address is now staged in ThrottleManager and has been requested as a steal
247                //  Complete the process
248                InstanceManager.throttleManagerInstance().responseThrottleDecision(address, this, DecisionType.STEAL);
249                isStealAddress = false;
250            } else {
251                //  Address has not been requested as a steal yet
252                sendStealAddress();
253                notifyFailedThrottleRequest(address, "Steal Required");
254            }
255        }
256        else if ( question == DecisionType.STEAL_OR_SHARE ){ // using the same process as a Steal
257            if (isStealAddress) {
258                //  Address is now staged in ThrottleManager and has been requested as a steal
259                //  Complete the process
260                InstanceManager.throttleManagerInstance().responseThrottleDecision(address, this, DecisionType.STEAL);
261                isStealAddress = false;
262            } else {
263                //  Address has not been requested as a steal yet
264                sendStealAddress();
265                notifyFailedThrottleRequest(address, "Steal Required");
266            }
267        }
268        else { // if encountered likely to be DecisionType.SHARE
269            log.info("{} question not supported by WiThrottle.",question );
270        }
271        
272        
273    }
274
275    // Encode a SpeedStepMode to a string.
276    private static String encodeSpeedStepMode(SpeedStepMode mode) {
277        switch(mode) {
278            // NOTE: old speed step modes use the original numeric values
279            // from when speed step modes were in DccThrottle. New speed step
280            // modes use the mode name.
281            case NMRA_DCC_128:
282                return "1";
283            case NMRA_DCC_28:
284                 return "2";
285            case NMRA_DCC_27:
286                return "4";
287            case NMRA_DCC_14:
288                return "8";
289            case MOTOROLA_28:
290                return "16";
291            default:
292                return mode.name;
293        }
294    }
295
296    private final static Logger log = LoggerFactory.getLogger(MultiThrottleController.class);
297
298}