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    @Override
241    public void reply(EcosReply m) {
242        int resultCode = m.getResultCode();
243        if (resultCode == 0) {
244            String replyType = m.getReplyType();
245            if (replyType.equals("create")) {
246                String[] msgDetails = m.getContents();
247                for (String line : msgDetails) {
248                    if (line.startsWith("10 id[")) {
249                        String EcosAddr = EcosReply.getContentDetail(line);
250                        objEcosLoco.setEcosObject(EcosAddr);
251                        objEcosLocoManager.deregister(objEcosLoco);
252                        objEcosLocoManager.register(objEcosLoco);
253                        objEcosLoco.setEcosTempEntry(true);
254                        objEcosLoco.doNotAddToRoster();
255                        this.objectNumber = EcosAddr;
256                        getControl();
257                    }
258                }
259                return;
260            }
261
262            /*if (lines[lines.length-1].contains("<END 0 (NERROR_OK)>")){
263             //Need to investigate this a bit futher to see what the significance of the message is
264             //we may not have to worry much about it.
265             log.info("Loco has been created on the ECoS Sucessfully.");
266             return;
267             }*/
268            if (m.getEcosObjectId() != objEcosLoco.getEcosObjectAsInt()) {
269                log.debug("message is not for us");
270                return;
271            }
272            if (replyType.equals("set")) {
273                //This might need to use speedstep, rather than speed
274                //This is for standard response to set and request.
275                String[] msgDetails = m.getContents();
276                for (String line : msgDetails) {
277                    if (line.contains("speed") && !line.contains("speedstep")) {
278                        speedMessageSent--;
279                        if (speedMessageSent <= 0) {
280                            Float newSpeed = floatSpeed(Integer.parseInt(EcosReply.getContentDetails(line, "speed")));
281                            super.setSpeedSetting(newSpeed);
282                        }
283                    }
284                    if (line.contains("dir")) {
285                        boolean newDirection = false;
286                        if (EcosReply.getContentDetails(line, "dir").equals("0")) {
287                            newDirection = true;
288                        }
289                        super.setIsForward(newDirection);
290                    }
291                }
292                if (msgDetails.length == 0) {
293                    //For some reason in recent ECOS software releases we do not get the contents, only a header and End State
294                    if (m.toString().contains("speed") && !m.toString().contains("speedstep")) {
295                        speedMessageSent--;
296                        if (speedMessageSent <= 0) {
297                            Float newSpeed = floatSpeed(Integer.parseInt(EcosReply.getContentDetails(m.toString(), "speed")));
298                            super.setSpeedSetting(newSpeed);
299                        }
300                    }
301                    if (m.toString().contains("dir")) {
302                        boolean newDirection = false;
303                        if (EcosReply.getContentDetails(m.toString(), "dir").equals("0")) {
304                            newDirection = true;
305                        }
306                        super.setIsForward(newDirection);
307                    }
308                }
309            } //Treat gets and events as the same.
310            else if ((replyType.equals("get")) || (m.isUnsolicited())) {
311                //log.debug("The last command was accepted by the ecos");
312                String[] msgDetails = m.getContents();
313                for (String line : msgDetails) {
314                    if (speedMessageSent > 0 && m.isUnsolicited() && line.contains("speed")) {
315                        //We want to ignore these messages.
316                    } else if (speedMessageSent <= 0 && line.contains("speed") && !line.contains("speedstep")) {
317                        Float newSpeed = floatSpeed(Integer.parseInt(EcosReply.getContentDetails(line, "speed")));
318                        super.setSpeedSetting(newSpeed);
319                    } else if (line.contains("dir")) {
320                        boolean newDirection = false;
321                        if (EcosReply.getContentDetails(line, "dir").equals("0")) {
322                            newDirection = true;
323                        }
324                        super.setIsForward(newDirection);
325                    } else if (line.contains("protocol")) {
326                        String pro = EcosReply.getContentDetails(line, "protocol");
327                        if (pro.equals("DCC128")) {
328                            setSpeedStepMode(SpeedStepMode.NMRA_DCC_128);
329                        } else if (pro.equals("DCC28")) {
330                            setSpeedStepMode(SpeedStepMode.NMRA_DCC_28);
331                        } else if (pro.equals("DCC14")) {
332                            setSpeedStepMode(SpeedStepMode.NMRA_DCC_14);
333                        }
334                    } else if (line.contains("func[")) {
335                        String funcStr = EcosReply.getContentDetails(line, "func");
336                        int function = Integer.parseInt(funcStr.substring(0, funcStr.indexOf(",")).trim());
337                        int functionValue = Integer.parseInt(funcStr.substring((funcStr.indexOf(",") + 1), funcStr.length()).trim());
338                        updateFunction(function,functionValue == 1);
339                        
340                    } else if (line.contains("msg")) {
341                        //We get this lost control error because we have registered as a viewer.
342                        if (line.contains("CONTROL_LOST")) {
343                            retryControl();
344                            log.debug("We have no control over the ecos object, but will retry.");
345                        }
346
347                    }
348
349                }
350            } else if (replyType.equals("release")) {
351                log.debug("Released {} from the Ecos", this.objectNumber);
352                _haveControl = false;
353            } else if (replyType.equals("request")) {
354                log.debug("We have control over {} from the Ecos", this.objectNumber);
355                ecosretry = 0;
356                if (_control) {
357                    _haveControl = true;
358                }
359                if (!_hadControl) {
360                    ((EcosDccThrottleManager) adapterMemo.get(jmri.ThrottleManager.class)).throttleSetup(this, this.address, true);
361                    getInitialStates();
362                }
363            }
364        } else if (resultCode == 35) {
365            /**
366             * This message occurs when have already created a loco, but have
367             * not appended it to the database. The Ecos will not allow another
368             * loco to be created until the previous entry has been appended.
369             */
370
371            //Potentially need to deal with this error better.
372            log.info("Another loco create operation is already taking place unable to create another.");
373
374        } else if (resultCode == 25) {
375            /**
376             * This section deals with no longer having control over the ecos
377             * loco object. we try three times to request control, on the fourth
378             * attempt we try a forced control, if that fails we inform the user
379             * and reset the counter to zero.
380             */
381            retryControl();
382        } else if (resultCode == 15) {
383            log.info("Loco can not be accessed via the Ecos Object Id {}", this.objectNumber);
384            try {
385                JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("UnknownLocoDialog", this.address),
386                        Bundle.getMessage("WarningTitle"), JmriJOptionPane.WARNING_MESSAGE);
387            } catch (HeadlessException he) {
388                // silently ignore inability to display dialog
389            }
390            jmri.InstanceManager.throttleManagerInstance().releaseThrottle(this, null);
391        } else {
392            log.debug("Last Message resulted in an END code we do not understand {}", resultCode);
393        }
394    }
395
396    /** 
397     * Messages ignored.
398     * {@inheritDoc} 
399     */
400    @Override
401    public void message(EcosMessage m) {
402    }
403
404    public void forceControl() {
405        String message = "request(" + this.objectNumber + ", control, force)";
406        EcosMessage ms = new EcosMessage(message);
407        tc.sendEcosMessage(ms, this);
408    }
409
410    //Converts the int value of the protocol to the ESU protocol string
411    private String protocol(LocoAddress.Protocol protocol) {
412        switch (protocol) {
413            case MOTOROLA:
414                return "MM28";
415            case SELECTRIX:
416                return "SX28";
417            case MFX:
418                return "MMFKT";
419            case LGB:
420                return "LGB";
421            default:
422                return "DCC128";
423        }
424    }
425
426    private void createEcosLoco() {
427        objEcosLoco.setEcosDescription(Bundle.getMessage("CreatedByJMRI"));
428        objEcosLoco.setProtocol(protocol(address.getProtocol()));
429        String message = "create(10, addr[" + objEcosLoco.getNumber() + "], name[\"Created By JMRI\"], protocol[" + objEcosLoco.getECOSProtocol() + "], append)";
430        EcosMessage m = new EcosMessage(message);
431        tc.sendEcosMessage(m, this);
432    }
433
434    private void retryControl() {
435        if (_haveControl) {
436            _hadControl = true;
437        }
438        _haveControl = false;
439        if (ecosretry < 3) {
440            //It might be worth adding in a sleep/pause of discription between retries.
441            ecosretry++;
442
443            String message = "request(" + this.objectNumber + ", view, control)";
444            EcosMessage ms = new EcosMessage(message);
445            tc.sendEcosMessage(ms, this);
446            log.error("We have no control over the ecos object {} Retrying Attempt {}", this.objectNumber, ecosretry);
447        } else if (ecosretry == 3) {
448            ecosretry++;
449            int val = 0;
450            if (p.getForceControlFromEcos() == 0x00) {
451                try {
452                    val = JmriJOptionPane.showConfirmDialog(null, "UnableToGainDialog",
453                            Bundle.getMessage("WarningTitle"),
454                            JmriJOptionPane.YES_NO_OPTION, JmriJOptionPane.QUESTION_MESSAGE);
455                } catch (HeadlessException he) {
456                    val = 1;
457                }
458            } else {
459                if (p.getForceControlFromEcos() == 0x01) {
460                    val = 1;
461                }
462            }
463            if (val == 0) {
464                String message = "request(" + this.objectNumber + ", view, control, force)";
465                EcosMessage ms = new EcosMessage(message);
466                tc.sendEcosMessage(ms, this);
467                log.error("We have no control over the ecos object {}Trying a forced control", this.objectNumber);
468            } else {
469                if (_hadControl) {
470                    firePropertyChange("LostControl", 0, 0);
471                    _hadControl = false;
472                    ecosretry = 0;
473                } else {
474                    ((EcosDccThrottleManager) adapterMemo.get(jmri.ThrottleManager.class)).throttleSetup(this, this.address, false);
475                }
476            }
477        } else {
478            ecosretry = 0;
479            if (_hadControl) {
480                firePropertyChange("LostControl", 0, 0);
481            } else {
482                ((EcosDccThrottleManager) adapterMemo.get(jmri.ThrottleManager.class)).throttleSetup(this, this.address, false);
483            }
484            ((EcosDccThrottleManager) adapterMemo.get(jmri.ThrottleManager.class)).releaseThrottle(this, null);
485        }
486    }
487
488    void getInitialStates() {
489        String message = "get(" + this.objectNumber + ", speed)";
490        EcosMessage m = new EcosMessage(message);
491        tc.sendEcosMessage(m, this);
492        message = "get(" + this.objectNumber + ", dir)";
493        m = new EcosMessage(message);
494        tc.sendEcosMessage(m, this);
495        for (int i = 0; i <= 28; i++) {
496            message = "get(" + this.objectNumber + ", func[" + i + "])";
497            m = new EcosMessage(message);
498            tc.sendEcosMessage(m, this);
499        }
500    }
501
502    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(EcosDccThrottle.class);
503
504}