001package jmri.jmrix.ecos;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004
005import java.awt.HeadlessException;
006
007import jmri.DccLocoAddress;
008import jmri.LocoAddress;
009import jmri.SpeedStepMode;
010import jmri.jmrix.AbstractThrottle;
011import jmri.util.swing.JmriJOptionPane;
012
013/**
014 * An implementation of DccThrottle with code specific to an ECoS connection.
015 *
016 * Based on Glen Oberhauser's original LnThrottleManager implementation
017 *
018 * @author Bob Jacobsen Copyright (C) 2001, modified 2009 by Kevin Dickerson
019 */
020public class EcosDccThrottle extends AbstractThrottle implements EcosListener {
021
022    
023    String objectNumber;
024    int ecosretry = 0;
025    private EcosLocoAddress objEcosLoco;
026    private EcosLocoAddressManager objEcosLocoManager;
027    final EcosPreferences p;
028    //This boolean is used to prevent un-necessary commands from being sent to the ECOS if we have already lost
029    //control of the object
030    private boolean _haveControl = false;
031    private boolean _hadControl = false;
032    private boolean _control = true;
033
034    /** 
035     * Create a new EcosDccThrottle.
036     * @param address Throttle Address
037     * @param memo System Connection
038     * @param control sets _control flag which NEEDS CLARIFICATION.
039     */
040    public EcosDccThrottle(DccLocoAddress address, EcosSystemConnectionMemo memo, boolean control) {
041        super(memo,32);
042        super.speedStepMode = SpeedStepMode.NMRA_DCC_128;
043        p = memo.getPreferenceManager();
044        tc = memo.getTrafficController();
045        objEcosLocoManager = memo.getLocoAddressManager();
046        //The script will go through and read the values from the Ecos
047        synchronized (this) {
048            this.speedSetting = 0;
049        }
050        // Functions 0-31 default to false
051        this.address = address;
052        this.isForward = true;
053        this._control = control;
054
055        ecosretry = 0;
056
057        log.debug("EcosDccThrottle constructor {}", address);
058
059        //We go on a hunt to find an object with the dccaddress sent by our controller.
060        if (address.getNumber() < EcosLocoAddress.MFX_DCCAddressOffset) {
061            objEcosLoco = objEcosLocoManager.provideByDccAddress(address.getNumber());
062        } else {
063            int ecosID = address.getNumber()-EcosLocoAddress.MFX_DCCAddressOffset;
064            objEcosLoco = objEcosLocoManager.provideByEcosObject(String.valueOf(ecosID));
065        }
066
067        this.objectNumber = objEcosLoco.getEcosObject();
068        if (this.objectNumber == null) {
069            createEcosLoco();
070        } else {
071            getControl();
072        }
073    }
074
075    private void getControl() {
076        String message;
077        setSpeedStepMode(objEcosLoco.getSpeedStepMode());
078        message = "get(" + this.objectNumber + ", speed)";
079        EcosMessage m = new EcosMessage(message);
080        tc.sendEcosMessage(m, this);
081
082        message = "get(" + this.objectNumber + ", dir)";
083        m = new EcosMessage(message);
084        tc.sendEcosMessage(m, this);
085
086        if (_control) {
087            if (p.getLocoControl()) {
088                message = "request(" + this.objectNumber + ", view, control, force)";
089            } else {
090                message = "request(" + this.objectNumber + ", view, control)";
091            }
092        } else {
093            message = "request(" + this.objectNumber + ", view)";
094        }
095
096        m = new EcosMessage(message);
097        tc.sendEcosMessage(m, this);
098    }
099
100    //The values here might need a bit of re-working
101
102    /**
103     * Convert an Ecos speed integer to a float speed value.
104     * @param lSpeed speed value as an integer
105     * @return speed value as a float
106     */
107    protected float floatSpeed(int lSpeed) {
108        if (lSpeed == 0) {
109            return 0.0f;
110        }
111        if (getSpeedStepMode() == SpeedStepMode.NMRA_DCC_28) {
112            int step = (int) Math.ceil(lSpeed / 4.65);
113            return step * getSpeedIncrement();
114        }
115        return ((lSpeed) / 126.f);
116    }
117    
118    /** 
119     * {@inheritDoc} 
120     */
121    @Override
122    public void setFunction(int functionNum, boolean newState){
123        updateFunction(functionNum,newState);
124        if (_haveControl) {
125            EcosMessage m = new EcosMessage("set(" + this.objectNumber + ", func[" + 
126                String.valueOf(functionNum) + ", " + (newState ? 1 : 0) + "])");
127            tc.sendEcosMessage(m, this);
128        }
129    }
130
131    /**
132     * Set the speed and direction.
133     * <p>
134     * This intentionally skips the emergency stop value of 1.
135     *
136     * @param speed Number from 0 to 1; less than zero is emergency stop
137     */
138    //The values here might need a bit of re-working
139    @SuppressFBWarnings(value = "FE_FLOATING_POINT_EQUALITY") // OK to compare floating point
140    @Override
141    public void setSpeedSetting(float speed) {
142        if (!_haveControl) {
143            return;
144        }
145        synchronized (this) {
146            if (speed == this.speedSetting && speedMessageSent <= 0) {
147                return;
148            }
149        }
150        int value = Math.round((127 - 1) * speed);     // -1 for rescale to avoid estop
151        if (value == 0 && speed > 0) {                 // make sure to output non-zero speed for non-zero input speed
152            value = 1;
153        }
154        if (value > 126) {
155            value = 126;    // max possible speed
156        }
157        if ((value > 0) || (value == 0)) {
158            String message = "set(" + this.objectNumber + ", speed[" + value + "])";
159            EcosMessage m = new EcosMessage(message);
160            tc.sendEcosMessage(m, this);
161            if (speedMessageSent != 0) {
162                if (System.currentTimeMillis() - lastSpeedMessageTime > 500 || speedMessageSent < 0) {
163                    speedMessageSent = 0;
164                }
165            }
166            lastSpeedMessageTime = System.currentTimeMillis();
167            speedMessageSent++;
168        } else {
169            //Not sure if this performs an emergency stop or a normal one.
170            String message = "set(" + this.objectNumber + ", stop)";
171            synchronized (this) {
172                this.speedSetting = 0.0f;
173            }
174            EcosMessage m = new EcosMessage(message);
175            tc.sendEcosMessage(m, this);
176
177        }
178        record(speed);
179    }
180
181    long lastSpeedMessageTime = 0L;
182
183    EcosTrafficController tc;
184
185    int speedMessageSent = 0;
186
187    /** 
188     * {@inheritDoc} 
189     */
190    @Override
191    public void setIsForward(boolean forward) {
192        if (!_haveControl) {
193            return;
194        }
195
196        String message;
197        synchronized (this) {
198            if (this.speedSetting > 0.0f) {
199                // Need to send current speed as well as direction, otherwise
200                // speed will be set to zero on direction change
201                int speedValue = (int) ((127 - 1) * this.speedSetting);     // -1 for rescale to avoid estop
202                if (speedValue > 128) {
203                    speedValue = 126;    // max possible speed
204                }
205                message = "set(" + this.objectNumber + ", dir[" + (forward ? 0 : 1) + "], speed[" + speedValue + "])";
206            } else {
207                message = "set(" + this.objectNumber + ", dir[" + (forward ? 0 : 1) + "])";
208            }
209        }
210        EcosMessage m = new EcosMessage(message);
211        tc.sendEcosMessage(m, this);
212    }
213
214    private DccLocoAddress address;
215
216    /** 
217     * {@inheritDoc} 
218     */
219    @Override
220    public LocoAddress getLocoAddress() {
221        return address;
222    }
223
224    /** 
225     * {@inheritDoc} 
226     */
227    @Override
228    public void throttleDispose() {
229        String message = "release(" + this.objectNumber + ", control)";
230        EcosMessage m = new EcosMessage(message);
231        tc.sendEcosMessage(m, this);
232        _haveControl = false;
233        _hadControl = false;
234        finishRecord();
235    }
236
237    /** 
238     * {@inheritDoc} 
239     */
240    @SuppressFBWarnings(value = "FE_FLOATING_POINT_EQUALITY") // OK to compare floating point
241    @Override
242    public void reply(EcosReply m) {
243        int resultCode = m.getResultCode();
244        if (resultCode == 0) {
245            String replyType = m.getReplyType();
246            if (replyType.equals("create")) {
247                String[] msgDetails = m.getContents();
248                for (String line : msgDetails) {
249                    if (line.startsWith("10 id[")) {
250                        String EcosAddr = EcosReply.getContentDetail(line);
251                        objEcosLoco.setEcosObject(EcosAddr);
252                        objEcosLocoManager.deregister(objEcosLoco);
253                        objEcosLocoManager.register(objEcosLoco);
254                        objEcosLoco.setEcosTempEntry(true);
255                        objEcosLoco.doNotAddToRoster();
256                        this.objectNumber = EcosAddr;
257                        getControl();
258                    }
259                }
260                return;
261            }
262
263            /*if (lines[lines.length-1].contains("<END 0 (NERROR_OK)>")){
264             //Need to investigate this a bit futher to see what the significance of the message is
265             //we may not have to worry much about it.
266             log.info("Loco has been created on the ECoS Sucessfully.");
267             return;
268             }*/
269            if (m.getEcosObjectId() != objEcosLoco.getEcosObjectAsInt()) {
270                log.debug("message is not for us");
271                return;
272            }
273            if (replyType.equals("set")) {
274                //This might need to use speedstep, rather than speed
275                //This is for standard response to set and request.
276                String[] msgDetails = m.getContents();
277                for (String line : msgDetails) {
278                    if (line.contains("speed") && !line.contains("speedstep")) {
279                        speedMessageSent--;
280                        if (speedMessageSent <= 0) {
281                            Float newSpeed = floatSpeed(Integer.parseInt(EcosReply.getContentDetails(line, "speed")));
282                            super.setSpeedSetting(newSpeed);
283                        }
284                    }
285                    if (line.contains("dir")) {
286                        boolean newDirection = false;
287                        if (EcosReply.getContentDetails(line, "dir").equals("0")) {
288                            newDirection = true;
289                        }
290                        super.setIsForward(newDirection);
291                    }
292                }
293                if (msgDetails.length == 0) {
294                    //For some reason in recent ECOS software releases we do not get the contents, only a header and End State
295                    if (m.toString().contains("speed") && !m.toString().contains("speedstep")) {
296                        speedMessageSent--;
297                        if (speedMessageSent <= 0) {
298                            Float newSpeed = floatSpeed(Integer.parseInt(EcosReply.getContentDetails(m.toString(), "speed")));
299                            super.setSpeedSetting(newSpeed);
300                        }
301                    }
302                    if (m.toString().contains("dir")) {
303                        boolean newDirection = false;
304                        if (EcosReply.getContentDetails(m.toString(), "dir").equals("0")) {
305                            newDirection = true;
306                        }
307                        super.setIsForward(newDirection);
308                    }
309                }
310            } //Treat gets and events as the same.
311            else if ((replyType.equals("get")) || (m.isUnsolicited())) {
312                //log.debug("The last command was accepted by the ecos");
313                String[] msgDetails = m.getContents();
314                for (String line : msgDetails) {
315                    if (speedMessageSent > 0 && m.isUnsolicited() && line.contains("speed")) {
316                        //We want to ignore these messages.
317                    } else if (speedMessageSent <= 0 && line.contains("speed") && !line.contains("speedstep")) {
318                        Float newSpeed = floatSpeed(Integer.parseInt(EcosReply.getContentDetails(line, "speed")));
319                        super.setSpeedSetting(newSpeed);
320                    } else if (line.contains("dir")) {
321                        boolean newDirection = false;
322                        if (EcosReply.getContentDetails(line, "dir").equals("0")) {
323                            newDirection = true;
324                        }
325                        super.setIsForward(newDirection);
326                    } else if (line.contains("protocol")) {
327                        String pro = EcosReply.getContentDetails(line, "protocol");
328                        if (pro.equals("DCC128")) {
329                            setSpeedStepMode(SpeedStepMode.NMRA_DCC_128);
330                        } else if (pro.equals("DCC28")) {
331                            setSpeedStepMode(SpeedStepMode.NMRA_DCC_28);
332                        } else if (pro.equals("DCC14")) {
333                            setSpeedStepMode(SpeedStepMode.NMRA_DCC_14);
334                        }
335                    } else if (line.contains("func[")) {
336                        String funcStr = EcosReply.getContentDetails(line, "func");
337                        int function = Integer.parseInt(funcStr.substring(0, funcStr.indexOf(",")).trim());
338                        int functionValue = Integer.parseInt(funcStr.substring((funcStr.indexOf(",") + 1), funcStr.length()).trim());
339                        updateFunction(function,functionValue == 1);
340                        
341                    } else if (line.contains("msg")) {
342                        //We get this lost control error because we have registered as a viewer.
343                        if (line.contains("CONTROL_LOST")) {
344                            retryControl();
345                            log.debug("We have no control over the ecos object, but will retry.");
346                        }
347
348                    }
349
350                }
351            } else if (replyType.equals("release")) {
352                log.debug("Released {} from the Ecos", this.objectNumber);
353                _haveControl = false;
354            } else if (replyType.equals("request")) {
355                log.debug("We have control over {} from the Ecos", this.objectNumber);
356                ecosretry = 0;
357                if (_control) {
358                    _haveControl = true;
359                }
360                if (!_hadControl) {
361                    ((EcosDccThrottleManager) adapterMemo.get(jmri.ThrottleManager.class)).throttleSetup(this, this.address, true);
362                    getInitialStates();
363                }
364            }
365        } else if (resultCode == 35) {
366            /**
367             * This message occurs when have already created a loco, but have
368             * not appended it to the database. The Ecos will not allow another
369             * loco to be created until the previous entry has been appended.
370             */
371
372            //Potentially need to deal with this error better.
373            log.info("Another loco create operation is already taking place unable to create another.");
374
375        } else if (resultCode == 25) {
376            /**
377             * This section deals with no longer having control over the ecos
378             * loco object. we try three times to request control, on the fourth
379             * attempt we try a forced control, if that fails we inform the user
380             * and reset the counter to zero.
381             */
382            retryControl();
383        } else if (resultCode == 15) {
384            log.info("Loco can not be accessed via the Ecos Object Id {}", this.objectNumber);
385            try {
386                JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("UnknownLocoDialog", this.address),
387                        Bundle.getMessage("WarningTitle"), JmriJOptionPane.WARNING_MESSAGE);
388            } catch (HeadlessException he) {
389                // silently ignore inability to display dialog
390            }
391            jmri.InstanceManager.throttleManagerInstance().releaseThrottle(this, null);
392        } else {
393            log.debug("Last Message resulted in an END code we do not understand {}", resultCode);
394        }
395    }
396
397    /** 
398     * Messages ignored.
399     * {@inheritDoc} 
400     */
401    @Override
402    public void message(EcosMessage m) {
403    }
404
405    public void forceControl() {
406        String message = "request(" + this.objectNumber + ", control, force)";
407        EcosMessage ms = new EcosMessage(message);
408        tc.sendEcosMessage(ms, this);
409    }
410
411    //Converts the int value of the protocol to the ESU protocol string
412    private String protocol(LocoAddress.Protocol protocol) {
413        switch (protocol) {
414            case MOTOROLA:
415                return "MM28";
416            case SELECTRIX:
417                return "SX28";
418            case MFX:
419                return "MMFKT";
420            case LGB:
421                return "LGB";
422            default:
423                return "DCC128";
424        }
425    }
426
427    private void createEcosLoco() {
428        objEcosLoco.setEcosDescription(Bundle.getMessage("CreatedByJMRI"));
429        objEcosLoco.setProtocol(protocol(address.getProtocol()));
430        String message = "create(10, addr[" + objEcosLoco.getNumber() + "], name[\"Created By JMRI\"], protocol[" + objEcosLoco.getECOSProtocol() + "], append)";
431        EcosMessage m = new EcosMessage(message);
432        tc.sendEcosMessage(m, this);
433    }
434
435    private void retryControl() {
436        if (_haveControl) {
437            _hadControl = true;
438        }
439        _haveControl = false;
440        if (ecosretry < 3) {
441            //It might be worth adding in a sleep/pause of discription between retries.
442            ecosretry++;
443
444            String message = "request(" + this.objectNumber + ", view, control)";
445            EcosMessage ms = new EcosMessage(message);
446            tc.sendEcosMessage(ms, this);
447            log.error("We have no control over the ecos object {} Retrying Attempt {}", this.objectNumber, ecosretry);
448        } else if (ecosretry == 3) {
449            ecosretry++;
450            int val = 0;
451            if (p.getForceControlFromEcos() == 0x00) {
452                try {
453                    val = JmriJOptionPane.showConfirmDialog(null, "UnableToGainDialog",
454                            Bundle.getMessage("WarningTitle"),
455                            JmriJOptionPane.YES_NO_OPTION, JmriJOptionPane.QUESTION_MESSAGE);
456                } catch (HeadlessException he) {
457                    val = 1;
458                }
459            } else {
460                if (p.getForceControlFromEcos() == 0x01) {
461                    val = 1;
462                }
463            }
464            if (val == 0) {
465                String message = "request(" + this.objectNumber + ", view, control, force)";
466                EcosMessage ms = new EcosMessage(message);
467                tc.sendEcosMessage(ms, this);
468                log.error("We have no control over the ecos object {}Trying a forced control", this.objectNumber);
469            } else {
470                if (_hadControl) {
471                    firePropertyChange("LostControl", 0, 0);
472                    _hadControl = false;
473                    ecosretry = 0;
474                } else {
475                    ((EcosDccThrottleManager) adapterMemo.get(jmri.ThrottleManager.class)).throttleSetup(this, this.address, false);
476                }
477            }
478        } else {
479            ecosretry = 0;
480            if (_hadControl) {
481                firePropertyChange("LostControl", 0, 0);
482            } else {
483                ((EcosDccThrottleManager) adapterMemo.get(jmri.ThrottleManager.class)).throttleSetup(this, this.address, false);
484            }
485            ((EcosDccThrottleManager) adapterMemo.get(jmri.ThrottleManager.class)).releaseThrottle(this, null);
486        }
487    }
488
489    void getInitialStates() {
490        String message = "get(" + this.objectNumber + ", speed)";
491        EcosMessage m = new EcosMessage(message);
492        tc.sendEcosMessage(m, this);
493        message = "get(" + this.objectNumber + ", dir)";
494        m = new EcosMessage(message);
495        tc.sendEcosMessage(m, this);
496        for (int i = 0; i <= 28; i++) {
497            message = "get(" + this.objectNumber + ", func[" + i + "])";
498            m = new EcosMessage(message);
499            tc.sendEcosMessage(m, this);
500        }
501    }
502
503    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(EcosDccThrottle.class);
504
505}