001package jmri.jmrix.ecos;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004import java.awt.HeadlessException;
005import javax.swing.JOptionPane;
006import jmri.DccLocoAddress;
007import jmri.LocoAddress;
008import jmri.SpeedStepMode;
009import jmri.jmrix.AbstractThrottle;
010import org.slf4j.Logger;
011import org.slf4j.LoggerFactory;
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 = (int) ((127 - 1) * speed);     // -1 for rescale to avoid estop
151        if (value > 128) {
152            value = 126;    // max possible speed
153        }
154        if ((value > 0) || (value == 0.0)) {
155            String message = "set(" + this.objectNumber + ", speed[" + value + "])";
156            EcosMessage m = new EcosMessage(message);
157            tc.sendEcosMessage(m, this);
158            if (speedMessageSent != 0) {
159                if (System.currentTimeMillis() - lastSpeedMessageTime > 500 || speedMessageSent < 0) {
160                    speedMessageSent = 0;
161                }
162            }
163            lastSpeedMessageTime = System.currentTimeMillis();
164            speedMessageSent++;
165        } else {
166            //Not sure if this performs an emergency stop or a normal one.
167            String message = "set(" + this.objectNumber + ", stop)";
168            synchronized (this) {
169                this.speedSetting = 0.0f;
170            }
171            EcosMessage m = new EcosMessage(message);
172            tc.sendEcosMessage(m, this);
173
174        }
175        record(speed);
176    }
177
178    long lastSpeedMessageTime = 0L;
179
180    EcosTrafficController tc;
181
182    int speedMessageSent = 0;
183
184    /** 
185     * {@inheritDoc} 
186     */
187    @Override
188    public void setIsForward(boolean forward) {
189        if (!_haveControl) {
190            return;
191        }
192
193        String message;
194        synchronized (this) {
195            if (this.speedSetting > 0.0f) {
196                // Need to send current speed as well as direction, otherwise
197                // speed will be set to zero on direction change
198                int speedValue = (int) ((127 - 1) * this.speedSetting);     // -1 for rescale to avoid estop
199                if (speedValue > 128) {
200                    speedValue = 126;    // max possible speed
201                }
202                message = "set(" + this.objectNumber + ", dir[" + (forward ? 0 : 1) + "], speed[" + speedValue + "])";
203            } else {
204                message = "set(" + this.objectNumber + ", dir[" + (forward ? 0 : 1) + "])";
205            }
206        }
207        EcosMessage m = new EcosMessage(message);
208        tc.sendEcosMessage(m, this);
209    }
210
211    private DccLocoAddress address;
212
213    /** 
214     * {@inheritDoc} 
215     */
216    @Override
217    public LocoAddress getLocoAddress() {
218        return address;
219    }
220
221    /** 
222     * {@inheritDoc} 
223     */
224    @Override
225    public void throttleDispose() {
226        String message = "release(" + this.objectNumber + ", control)";
227        EcosMessage m = new EcosMessage(message);
228        tc.sendEcosMessage(m, this);
229        _haveControl = false;
230        _hadControl = false;
231        finishRecord();
232    }
233
234    /** 
235     * {@inheritDoc} 
236     */
237    @SuppressFBWarnings(value = "FE_FLOATING_POINT_EQUALITY") // OK to compare floating point
238    @Override
239    public void reply(EcosReply m) {
240        int resultCode = m.getResultCode();
241        if (resultCode == 0) {
242            String replyType = m.getReplyType();
243            if (replyType.equals("create")) {
244                String[] msgDetails = m.getContents();
245                for (String line : msgDetails) {
246                    if (line.startsWith("10 id[")) {
247                        String EcosAddr = EcosReply.getContentDetail(line);
248                        objEcosLoco.setEcosObject(EcosAddr);
249                        objEcosLocoManager.deregister(objEcosLoco);
250                        objEcosLocoManager.register(objEcosLoco);
251                        objEcosLoco.setEcosTempEntry(true);
252                        objEcosLoco.doNotAddToRoster();
253                        this.objectNumber = EcosAddr;
254                        getControl();
255                    }
256                }
257                return;
258            }
259
260            /*if (lines[lines.length-1].contains("<END 0 (NERROR_OK)>")){
261             //Need to investigate this a bit futher to see what the significance of the message is
262             //we may not have to worry much about it.
263             log.info("Loco has been created on the ECoS Sucessfully.");
264             return;
265             }*/
266            if (m.getEcosObjectId() != objEcosLoco.getEcosObjectAsInt()) {
267                log.debug("message is not for us");
268                return;
269            }
270            if (replyType.equals("set")) {
271                //This might need to use speedstep, rather than speed
272                //This is for standard response to set and request.
273                String[] msgDetails = m.getContents();
274                for (String line : msgDetails) {
275                    if (line.contains("speed") && !line.contains("speedstep")) {
276                        speedMessageSent--;
277                        if (speedMessageSent <= 0) {
278                            Float newSpeed = floatSpeed(Integer.parseInt(EcosReply.getContentDetails(line, "speed")));
279                            super.setSpeedSetting(newSpeed);
280                        }
281                    }
282                    if (line.contains("dir")) {
283                        boolean newDirection = false;
284                        if (EcosReply.getContentDetails(line, "dir").equals("0")) {
285                            newDirection = true;
286                        }
287                        super.setIsForward(newDirection);
288                    }
289                }
290                if (msgDetails.length == 0) {
291                    //For some reason in recent ECOS software releases we do not get the contents, only a header and End State
292                    if (m.toString().contains("speed") && !m.toString().contains("speedstep")) {
293                        speedMessageSent--;
294                        if (speedMessageSent <= 0) {
295                            Float newSpeed = floatSpeed(Integer.parseInt(EcosReply.getContentDetails(m.toString(), "speed")));
296                            super.setSpeedSetting(newSpeed);
297                        }
298                    }
299                    if (m.toString().contains("dir")) {
300                        boolean newDirection = false;
301                        if (EcosReply.getContentDetails(m.toString(), "dir").equals("0")) {
302                            newDirection = true;
303                        }
304                        super.setIsForward(newDirection);
305                    }
306                }
307            } //Treat gets and events as the same.
308            else if ((replyType.equals("get")) || (m.isUnsolicited())) {
309                //log.debug("The last command was accepted by the ecos");
310                String[] msgDetails = m.getContents();
311                for (String line : msgDetails) {
312                    if (speedMessageSent > 0 && m.isUnsolicited() && line.contains("speed")) {
313                        //We want to ignore these messages.
314                    } else if (speedMessageSent <= 0 && line.contains("speed") && !line.contains("speedstep")) {
315                        Float newSpeed = floatSpeed(Integer.parseInt(EcosReply.getContentDetails(line, "speed")));
316                        super.setSpeedSetting(newSpeed);
317                    } else if (line.contains("dir")) {
318                        boolean newDirection = false;
319                        if (EcosReply.getContentDetails(line, "dir").equals("0")) {
320                            newDirection = true;
321                        }
322                        super.setIsForward(newDirection);
323                    } else if (line.contains("protocol")) {
324                        String pro = EcosReply.getContentDetails(line, "protocol");
325                        if (pro.equals("DCC128")) {
326                            setSpeedStepMode(SpeedStepMode.NMRA_DCC_128);
327                        } else if (pro.equals("DCC28")) {
328                            setSpeedStepMode(SpeedStepMode.NMRA_DCC_28);
329                        } else if (pro.equals("DCC14")) {
330                            setSpeedStepMode(SpeedStepMode.NMRA_DCC_14);
331                        }
332                    } else if (line.contains("func[")) {
333                        String funcStr = EcosReply.getContentDetails(line, "func");
334                        int function = Integer.parseInt(funcStr.substring(0, funcStr.indexOf(",")).trim());
335                        int functionValue = Integer.parseInt(funcStr.substring((funcStr.indexOf(",") + 1), funcStr.length()).trim());
336                        updateFunction(function,functionValue == 1);
337                        
338                    } else if (line.contains("msg")) {
339                        //We get this lost control error because we have registered as a viewer.
340                        if (line.contains("CONTROL_LOST")) {
341                            retryControl();
342                            log.debug("We have no control over the ecos object, but will retry.");
343                        }
344
345                    }
346
347                }
348            } else if (replyType.equals("release")) {
349                log.debug("Released {} from the Ecos", this.objectNumber);
350                _haveControl = false;
351            } else if (replyType.equals("request")) {
352                log.debug("We have control over {} from the Ecos", this.objectNumber);
353                ecosretry = 0;
354                if (_control) {
355                    _haveControl = true;
356                }
357                if (!_hadControl) {
358                    ((EcosDccThrottleManager) adapterMemo.get(jmri.ThrottleManager.class)).throttleSetup(this, this.address, true);
359                    getInitialStates();
360                }
361            }
362        } else if (resultCode == 35) {
363            /**
364             * This message occurs when have already created a loco, but have
365             * not appended it to the database. The Ecos will not allow another
366             * loco to be created until the previous entry has been appended.
367             */
368
369            //Potentially need to deal with this error better.
370            log.info("Another loco create operation is already taking place unable to create another.");
371
372        } else if (resultCode == 25) {
373            /**
374             * This section deals with no longer having control over the ecos
375             * loco object. we try three times to request control, on the fourth
376             * attempt we try a forced control, if that fails we inform the user
377             * and reset the counter to zero.
378             */
379            retryControl();
380        } else if (resultCode == 15) {
381            log.info("Loco can not be accessed via the Ecos Object Id {}", this.objectNumber);
382            try {
383                javax.swing.JOptionPane.showMessageDialog(null, Bundle.getMessage("UnknownLocoDialog", this.address),
384                        Bundle.getMessage("WarningTitle"), javax.swing.JOptionPane.WARNING_MESSAGE);
385            } catch (HeadlessException he) {
386                // silently ignore inability to display dialog
387            }
388            jmri.InstanceManager.throttleManagerInstance().releaseThrottle(this, null);
389        } else {
390            log.debug("Last Message resulted in an END code we do not understand {}", resultCode);
391        }
392    }
393
394    /** 
395     * Messages ignored.
396     * {@inheritDoc} 
397     */
398    @Override
399    public void message(EcosMessage m) {
400    }
401
402    public void forceControl() {
403        String message = "request(" + this.objectNumber + ", control, force)";
404        EcosMessage ms = new EcosMessage(message);
405        tc.sendEcosMessage(ms, this);
406    }
407
408    //Converts the int value of the protocol to the ESU protocol string
409    private String protocol(LocoAddress.Protocol protocol) {
410        switch (protocol) {
411            case MOTOROLA:
412                return "MM28";
413            case SELECTRIX:
414                return "SX28";
415            case MFX:
416                return "MMFKT";
417            case LGB:
418                return "LGB";
419            default:
420                return "DCC128";
421        }
422    }
423
424    private void createEcosLoco() {
425        objEcosLoco.setEcosDescription(Bundle.getMessage("CreatedByJMRI"));
426        objEcosLoco.setProtocol(protocol(address.getProtocol()));
427        String message = "create(10, addr[" + objEcosLoco.getNumber() + "], name[\"Created By JMRI\"], protocol[" + objEcosLoco.getECOSProtocol() + "], append)";
428        EcosMessage m = new EcosMessage(message);
429        tc.sendEcosMessage(m, this);
430    }
431
432    private void retryControl() {
433        if (_haveControl) {
434            _hadControl = true;
435        }
436        _haveControl = false;
437        if (ecosretry < 3) {
438            //It might be worth adding in a sleep/pause of discription between retries.
439            ecosretry++;
440
441            String message = "request(" + this.objectNumber + ", view, control)";
442            EcosMessage ms = new EcosMessage(message);
443            tc.sendEcosMessage(ms, this);
444            log.error("We have no control over the ecos object {} Retrying Attempt {}", this.objectNumber, ecosretry);
445        } else if (ecosretry == 3) {
446            ecosretry++;
447            int val = 0;
448            if (p.getForceControlFromEcos() == 0x00) {
449                try {
450                    val = javax.swing.JOptionPane.showConfirmDialog(null, "UnableToGainDialog",
451                            Bundle.getMessage("WarningTitle"),
452                            JOptionPane.YES_NO_OPTION, javax.swing.JOptionPane.QUESTION_MESSAGE);
453                } catch (HeadlessException he) {
454                    val = 1;
455                }
456            } else {
457                if (p.getForceControlFromEcos() == 0x01) {
458                    val = 1;
459                }
460            }
461            if (val == 0) {
462                String message = "request(" + this.objectNumber + ", view, control, force)";
463                EcosMessage ms = new EcosMessage(message);
464                tc.sendEcosMessage(ms, this);
465                log.error("We have no control over the ecos object {}Trying a forced control", this.objectNumber);
466            } else {
467                if (_hadControl) {
468                    firePropertyChange("LostControl", 0, 0);
469                    _hadControl = false;
470                    ecosretry = 0;
471                } else {
472                    ((EcosDccThrottleManager) adapterMemo.get(jmri.ThrottleManager.class)).throttleSetup(this, this.address, false);
473                }
474            }
475        } else {
476            ecosretry = 0;
477            if (_hadControl) {
478                firePropertyChange("LostControl", 0, 0);
479            } else {
480                ((EcosDccThrottleManager) adapterMemo.get(jmri.ThrottleManager.class)).throttleSetup(this, this.address, false);
481            }
482            ((EcosDccThrottleManager) adapterMemo.get(jmri.ThrottleManager.class)).releaseThrottle(this, null);
483        }
484    }
485
486    void getInitialStates() {
487        String message = "get(" + this.objectNumber + ", speed)";
488        EcosMessage m = new EcosMessage(message);
489        tc.sendEcosMessage(m, this);
490        message = "get(" + this.objectNumber + ", dir)";
491        m = new EcosMessage(message);
492        tc.sendEcosMessage(m, this);
493        for (int i = 0; i <= 28; i++) {
494            message = "get(" + this.objectNumber + ", func[" + i + "])";
495            m = new EcosMessage(message);
496            tc.sendEcosMessage(m, this);
497        }
498    }
499
500    // initialize logging
501    private final static Logger log = LoggerFactory.getLogger(EcosDccThrottle.class);
502
503}