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     * This replaces the previous method of sending a string of function labels.
109     * 
110     * Checks for labels across all possible functions of this roster entry.
111     * 
112     * Example:
113     * {@code MTLL1234<;>]\[Light]\[Bell]\[Horn]\[]\[]\[]\[]\[]\[Mute]\[]\[]\[]\[} etc.
114     */
115    @Override
116    public void sendFunctionLabels(RosterEntry re) {
117
118        if (re != null) {
119            StringBuilder functionString = new StringBuilder(buildPacketWithChar('L'));
120
121            int i;
122            for (i = 0; i < (re.getMaxFnNumAsInt()+1); i++) {
123                functionString.append("]\\[");
124                if ((re.getFunctionLabel(i) != null)) {
125                    functionString.append(re.getFunctionLabel(i));
126                }
127            }
128            for (ControllerInterface listener : controllerListeners) {
129                listener.sendPacketToDevice(functionString.toString());
130            }
131        }
132    }
133
134    /**
135     * This replaces the previous method of sending a string of function states,
136     * and now sends them individually, the same as a property change would.
137     *
138     * @param t the throttle to send the states of.
139     */
140    @Override
141    public void sendAllFunctionStates(DccThrottle t) {
142        log.debug("Sending state of all functions");
143        for (int cnt = 0; cnt < t.getFunctions().length; cnt++) {
144            StringBuilder message = new StringBuilder(buildPacketWithChar('A'));
145            message.append( t.getFunction(cnt) ? "F1" : "F0" );
146            message.append(cnt);
147            controllerListeners.forEach(listener -> {
148                listener.sendPacketToDevice(message.toString());
149            });
150        }
151    }
152
153    /**
154     * {@inheritDoc}
155     */
156    @Override
157    synchronized protected void sendCurrentSpeed(DccThrottle t) {
158        float currentSpeed = t.getSpeedSetting();
159        StringBuilder message = new StringBuilder(buildPacketWithChar('A'));
160        message.append("V");
161        int outSpeed = Math.round(currentSpeed / speedMultiplier);
162        if(currentSpeed < 0) {
163            outSpeed = -126;        // ensure estop is not rounded to zero
164        }
165        if(currentSpeed > 0 && outSpeed == 0) {
166            outSpeed = 1;           // ensure non-zero throttle speed is sent
167                                    // as non-zero speed to wiThrottle
168        }
169        message.append(outSpeed);
170        for (ControllerInterface listener : controllerListeners) {
171            listener.sendPacketToDevice(message.toString());
172        }
173    }
174
175    /**
176     * {@inheritDoc}
177     */
178    @Override
179    protected void sendCurrentDirection(DccThrottle t) {
180        StringBuilder message = new StringBuilder(buildPacketWithChar('A'));
181        message.append("R");
182        message.append(t.getIsForward() ? "1" : "0");
183        for (ControllerInterface listener : controllerListeners) {
184            listener.sendPacketToDevice(message.toString());
185        }
186    }
187
188    /**
189     * {@inheritDoc}
190     */
191    @Override
192    protected void sendSpeedStepMode(DccThrottle t) {
193        StringBuilder message = new StringBuilder(buildPacketWithChar('A'));
194        message.append("s");
195        message.append(encodeSpeedStepMode(throttle.getSpeedStepMode()));
196        for (ControllerInterface listener : controllerListeners) {
197            listener.sendPacketToDevice(message.toString());
198        }
199    }
200
201    /**
202     * {@inheritDoc}
203     */
204    @Override
205    protected void sendAllMomentaryStates(DccThrottle t) {
206        log.debug("Sending momentary state of all functions");
207        for (int cnt = 0; cnt < t.getFunctionsMomentary().length; cnt++) {
208            StringBuilder message = new StringBuilder(buildPacketWithChar('A'));
209            message.append( t.getFunctionMomentary(cnt) ? "m1" : "m0" );
210            message.append(cnt);
211            controllerListeners.forEach(listener -> {
212                listener.sendPacketToDevice(message.toString());
213            });
214        }
215    }
216
217    /**
218     * {@inheritDoc} A + indicates the address was acquired, - indicates
219     * released
220     */
221    @Override
222    public void sendAddress() {
223        for (ControllerInterface listener : controllerListeners) {
224            if (isAddressSet) {
225                listener.sendPacketToDevice(buildPacketWithChar('+'));
226            } else {
227                listener.sendPacketToDevice(buildPacketWithChar('-'));
228            }
229        }
230    }
231
232    /**
233     * Send a message to a device that steal is needed. This message can be sent 
234     * back to JMRI verbatim to complete a steal.
235     */
236    public void sendStealAddress() {
237        StringBuilder message = new StringBuilder(buildPacketWithChar('S'));
238        message.append(locoKey);
239        for (ControllerInterface listener : controllerListeners) {
240            listener.sendPacketToDevice(message.toString());
241        }
242    }
243
244    /**
245     * A decision is required for Throttle creation to continue.
246     * <p>
247     * Steal / Cancel, Share / Cancel, or Steal / Share Cancel
248     * <p>
249     * Callback of a request for an address that is in use.
250     * Will initiate a steal only if this MTC is flagged to do so.
251     * Otherwise, it will remove the request for the address.
252     *
253     * {@inheritDoc}
254     */
255    @Override
256    public void notifyDecisionRequired(LocoAddress address, DecisionType question) {
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 ){ // using the same process as a Steal
270            if (isStealAddress) {
271                //  Address is now staged in ThrottleManager and has been requested as a steal
272                //  Complete the process
273                InstanceManager.throttleManagerInstance().responseThrottleDecision(address, this, DecisionType.STEAL);
274                isStealAddress = false;
275            } else {
276                //  Address has not been requested as a steal yet
277                sendStealAddress();
278                notifyFailedThrottleRequest(address, "Steal Required");
279            }
280        }
281        else { // if encountered likely to be DecisionType.SHARE
282            log.info("{} question not supported by WiThrottle.",question );
283        }
284        
285        
286    }
287    
288    /**
289     * Add option to not silently share ("steal") the requested address
290     * 
291     * {@inheritDoc}
292     */
293    @Override
294    protected void setAddress(int number, boolean isLong) {
295        if(isStealAddress
296                || jmri.InstanceManager.throttleManagerInstance().getThrottleUsageCount(number, isLong) == 0 
297                || ! InstanceManager.getDefault(WiThrottlePreferences.class).isExclusiveUseOfAddress()) {
298            super.setAddress(number, isLong);
299        }
300        else {
301            log.debug("Loco address {} already controlled by another JMRI throttle.", number);
302            sendStealAddress();
303            notifyFailedThrottleRequest(new DccLocoAddress(number, isLong), "Steal from other WiThrottle or JMRI throttle Required");
304        }
305
306    }
307
308    // Encode a SpeedStepMode to a string.
309    private static String encodeSpeedStepMode(SpeedStepMode mode) {
310        switch(mode) {
311            // NOTE: old speed step modes use the original numeric values
312            // from when speed step modes were in DccThrottle. New speed step
313            // modes use the mode name.
314            case NMRA_DCC_128:
315                return "1";
316            case NMRA_DCC_28:
317                 return "2";
318            case NMRA_DCC_27:
319                return "4";
320            case NMRA_DCC_14:
321                return "8";
322            case MOTOROLA_28:
323                return "16";
324            default:
325                return mode.name;
326        }
327    }
328
329    private final static Logger log = LoggerFactory.getLogger(MultiThrottleController.class);
330
331}