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