001package jmri.jmrix.tams;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004import java.util.LinkedList;
005import java.util.Queue;
006import jmri.DccLocoAddress;
007import jmri.LocoAddress;
008import jmri.jmrix.AbstractThrottle;
009import jmri.util.StringUtil;
010import org.slf4j.Logger;
011import org.slf4j.LoggerFactory;
012
013/**
014 * An implementation of DccThrottle with code specific to a TAMS connection.
015 * <p>
016 * Based on Glen Oberhauser's original LnThrottle implementation and work by
017 * Kevin Dickerson
018 *
019 * @author Jan Boen
020 */
021public class TamsThrottle extends AbstractThrottle implements TamsListener {
022
023    //Create a local TamsMessage Queue which we will use in combination with TamsReplies
024    private final Queue<TamsMessage> tmq = new LinkedList<>();
025
026    //This dummy message is used in case we expect a reply from polling
027    static private TamsMessage myDummy() {
028        log.trace("*** myDummy ***");
029        TamsMessage m = new TamsMessage(2);
030        m.setElement(0, TamsConstants.POLLMSG & TamsConstants.MASKFF);
031        m.setElement(1, TamsConstants.XEVTLOK & TamsConstants.MASKFF);
032        m.setBinary(true);
033        m.setReplyOneByte(false);
034        m.setReplyType('L');
035        return m;
036    }
037
038    public TamsThrottle(TamsSystemConnectionMemo memo, DccLocoAddress address) {
039        super(memo);
040        super.speedStepMode = jmri.SpeedStepMode.NMRA_DCC_128;
041        tc = memo.getTrafficController();
042
043        // cache settings. It would be better to read the
044        // actual state, but I don't know how to do this
045        synchronized(this) {
046            this.speedSetting = 0;
047        }
048        // Functions default to false
049        this.address = address;
050        this.isForward = true;
051
052        //get the status if known of the current loco
053        TamsMessage tm = new TamsMessage("xL " + address.getNumber());
054        tm.setTimeout(10000);
055        tm.setBinary(false);
056        tm.setReplyType('L');
057        tc.sendTamsMessage(tm, this);
058        tmq.add(tm);
059        //tc.addPollMessage(m, this);
060
061        tm = new TamsMessage("xF " + address.getNumber());
062        tm.setBinary(false);
063        tm.setReplyType('L');
064        tc.sendTamsMessage(tm, this);
065        tmq.add(tm);
066        //tc.addPollMessage(tm, this);
067
068        tm = new TamsMessage("xFX " + address.getNumber());
069        tm.setBinary(false);
070        tm.setReplyType('L');
071        tc.sendTamsMessage(tm, this);
072        tmq.add(tm);
073        //tc.addPollMessage(tm, this);
074
075        //Add binary polling message
076        tm = TamsMessage.getXEvtLok();
077        tc.sendTamsMessage(tm, this);
078        tmq.add(tm);
079        tc.addPollMessage(tm, this);
080
081    }
082
083    /**
084     * Send the message to set the state of functions F0, F1, F2, F3, F4. To
085     * send function group 1 we have to also send speed, direction etc.
086     */
087    @Override
088    protected void sendFunctionGroup1() {
089
090        StringBuilder sb = new StringBuilder();
091        sb.append("xL ");
092        sb.append(address.getNumber());
093        sb.append(",");
094        sb.append(",");
095        sb.append((getFunction(0) ? "1" : "0"));
096        sb.append(",");
097        sb.append(",");
098        sb.append((getFunction(1) ? "1" : "0"));
099        sb.append(",");
100        sb.append((getFunction(2) ? "1" : "0"));
101        sb.append(",");
102        sb.append((getFunction(3) ? "1" : "0"));
103        sb.append(",");
104        sb.append((getFunction(4) ? "1" : "0"));
105        TamsMessage tm = new TamsMessage(sb.toString());
106        tm.setBinary(false);
107        tm.setReplyType('L');
108        tc.sendTamsMessage(tm, this);
109        tmq.add(tm);
110    }
111
112    /**
113     * Send the message to set the state of functions F5, F6, F7, F8.
114     */
115    @Override
116    protected void sendFunctionGroup2() {
117        StringBuilder sb = new StringBuilder();
118        sb.append("xF ");
119        sb.append(address.getNumber());
120        sb.append(",");
121        sb.append(",");
122        sb.append(",");
123        sb.append(",");
124        sb.append(",");
125        sb.append((getFunction(5) ? "1" : "0"));
126        sb.append(",");
127        sb.append((getFunction(6) ? "1" : "0"));
128        sb.append(",");
129        sb.append((getFunction(7) ? "1" : "0"));
130        sb.append(",");
131        sb.append((getFunction(8) ? "1" : "0"));
132
133        TamsMessage tm = new TamsMessage(sb.toString());
134        tm.setBinary(false);
135        tm.setReplyType('T');
136        tc.sendTamsMessage(tm, this);
137        tmq.add(tm);
138    }
139
140    @Override
141    protected void sendFunctionGroup3() {
142        StringBuilder sb = new StringBuilder();
143        sb.append("xFX ");
144        sb.append(address.getNumber());
145        sb.append(",");
146        sb.append((getFunction(9) ? "1" : "0"));
147        sb.append(",");
148        sb.append((getFunction(10) ? "1" : "0"));
149        sb.append(",");
150        sb.append((getFunction(11) ? "1" : "0"));
151        sb.append(",");
152        sb.append((getFunction(12) ? "1" : "0"));
153
154        TamsMessage tm = new TamsMessage(sb.toString());
155        tm.setBinary(false);
156        tm.setReplyType('L');
157        tc.sendTamsMessage(tm, this);
158        tmq.add(tm);
159    }
160
161    /**
162     * Set the speed and direction.
163     * <p>
164     * This intentionally skips the emergency stop value of 1.
165     *
166     * @param speed Number from 0 to 1; less than zero is emergency stop
167     */
168    @SuppressFBWarnings(value = "FE_FLOATING_POINT_EQUALITY") // OK to compare floating point, notify on any change
169    @Override
170    public synchronized void setSpeedSetting(float speed) {
171        float oldSpeed = this.speedSetting;
172        this.speedSetting = speed;
173
174        int value = (int) ((127 - 1) * this.speedSetting);     // -1 for rescale to avoid estop
175        if (value > 0) {
176            value = value + 1;  // skip estop
177        }
178        if (value > 127) {
179            value = 127;    // max possible speed
180        }
181        if (value < 0) {
182            value = 1;        // emergency stop
183        }
184        StringBuilder sb = new StringBuilder();
185        sb.append("xL ");
186        sb.append(address.getNumber());
187        sb.append(",");
188        sb.append(value);
189        sb.append(",");
190        sb.append(",");
191        sb.append((isForward ? "f" : "r"));
192        sb.append(",");
193        sb.append(",");
194        sb.append(",");
195        sb.append(",");
196
197        TamsMessage tm = new TamsMessage(sb.toString());
198        tm.setBinary(false);
199        tm.setReplyType('L');
200        tc.sendTamsMessage(tm, this);
201        tmq.add(tm);
202
203        firePropertyChange(SPEEDSETTING, oldSpeed, this.speedSetting);
204        record(speed);
205    }
206
207    @Override
208    public void setIsForward(boolean forward) {
209        boolean old = isForward;
210        isForward = forward;
211        synchronized(this) {
212            setSpeedSetting(speedSetting);  // send the command
213        }
214        firePropertyChange(ISFORWARD, old, isForward);
215    }
216
217    private final DccLocoAddress address;
218
219    TamsTrafficController tc;
220
221    @Override
222    public LocoAddress getLocoAddress() {
223        return address;
224    }
225
226    @Override
227    public void throttleDispose() {
228        active = false;
229        TamsMessage tm = TamsMessage.getXEvtLok();
230        tc.removePollMessage(tm, this);
231        finishRecord();
232    }
233
234    @Override
235    public void message(TamsMessage m) {
236        // messages are ignored
237    }
238
239    /**
240     * Convert a Tams speed integer to a float speed value.
241     *
242     * @param lSpeed Tams speed
243     * @return speed as -1 or number between 0 and 1, inclusive
244     */
245    protected float floatSpeed(int lSpeed) {
246        if (lSpeed == 0) {
247            return 0.f;
248        } else if (lSpeed == 1) {
249            return -1.f;   // estop
250        } else if (super.speedStepMode == jmri.SpeedStepMode.NMRA_DCC_128) {
251            return ((lSpeed - 1) / 126.f);
252        } else {
253            return (int) (lSpeed * 27.f + 0.5) + 1;
254        }
255    }
256
257    @Override
258    public void reply(TamsReply tr) {
259        log.trace("*** Loco reply ***");
260        TamsMessage tm = tmq.isEmpty() ? myDummy() : tmq.poll();
261        if (tm.isBinary()) {//Binary reply
262            //The binary logic as created by Jan
263            //Complete Loco status is given by:
264            //    element(0) = Speed: 0..127, 0 = Stop, 1 = not used, 2 = min. Speed, 127 = max. Speed
265            //    element(1) = F1..F8 (bit #0..7)
266            //    element(2) = low byte of Loco# (A7..A0)
267            //    element(3) = high byte of Loco#, plus Dir and Light status as in:
268            //        bit#   7     6     5     4     3     2     1     0
269            //            +-----+-----+-----+-----+-----+-----+-----+-----+
270            //            | Dir |  FL | A13 | A12 | A11 | A10 | A9  | A8  |
271            //            +-----+-----+-----+-----+-----+-----+-----+-----+
272            //        where:
273            //            Dir Loco direction (1 = forward)
274            //            FL  Light status
275            //            A13..8  high bits of Loco#
276            //    element(4) = 'real' Loco speed (in terms of the Loco type/configuration)
277            //        (please check XLokSts in P50X_LT.TXT for doc on 'real' speed)
278            //Decode address
279            int msb = tr.getElement(3) & 0x3F;
280            int lsb = tr.getElement(2) & 0xFF;
281            int receivedAddress = msb * 256 + lsb;
282            if (log.isTraceEnabled()) { // avoid overhead of StringUtil calls
283                log.trace("reply for loco = {}", receivedAddress);
284                log.trace("reply = {} {} {} {} {}", StringUtil.appendTwoHexFromInt(tr.getElement(4) & 0xFF, ""), StringUtil.appendTwoHexFromInt(tr.getElement(3) & 0xFF, ""), StringUtil.appendTwoHexFromInt(tr.getElement(2) & 0xFF, ""), StringUtil.appendTwoHexFromInt(tr.getElement(1) & 0xFF, ""), StringUtil.appendTwoHexFromInt(tr.getElement(0) & 0xFF, ""));
285            }
286            if (receivedAddress == address.getNumber()) {//If correct address then decode the content
287                log.trace("Is my address");
288                try {
289                    StringBuilder sb = new StringBuilder();
290                    Float newSpeed = floatSpeed(tr.getElement(0));
291                    super.setSpeedSetting(newSpeed);
292                    log.trace("f0 = {}", tr.getElement(3) & 0x40);
293                    
294                    appendFuncString(0,sb,((tr.getElement(3) & 0x40) == 64));
295                    
296                    if (((tr.getElement(3) & 0x80) == 0) && isForward) {
297                        isForward = false;
298                        firePropertyChange(ISFORWARD, true, isForward);
299                    }
300                    if (((tr.getElement(3) & 0x80) == 128) && !isForward) {
301                        isForward = true;
302                        firePropertyChange(ISFORWARD, false, isForward);
303                    }
304                    
305                    appendFuncString(1,sb,((tr.getElement(1) & 0x01) == 0x01));
306                    appendFuncString(2,sb,((tr.getElement(1) & 0x02) == 0x02));
307                    appendFuncString(3,sb,((tr.getElement(1) & 0x04) == 0x04));
308                    appendFuncString(4,sb,((tr.getElement(1) & 0x08) == 0x08));
309                    appendFuncString(5,sb,((tr.getElement(1) & 0x10) == 0x10));
310                    appendFuncString(6,sb,((tr.getElement(1) & 0x20) == 0x20));
311                    appendFuncString(7,sb,((tr.getElement(1) & 0x40) == 0x40));
312                    appendFuncString(8,sb,((tr.getElement(1) & 0x80) == 0x80));
313                    
314                    log.trace(sb.toString());
315                } catch (RuntimeException ex) {
316                    log.error("Error handling reply from MC", ex);
317                }
318            }
319
320        } else {//ASCII reply
321            //The original logic as provided by Kevin
322            if (tr.match("WARNING") >= 0) {
323                return;
324            }
325            if (tr.match("L " + address.getNumber()) >= 0) {
326                try {
327                    log.trace("ASCII address = {}", address.getNumber());
328                    String[] lines = tr.toString().split(" ");
329                    Float newSpeed = floatSpeed(Integer.parseInt(lines[2]));
330                    super.setSpeedSetting(newSpeed);
331                    updateFunction(0,lines[3].equals("1"));
332                    
333                    if (lines[4].equals("r") && isForward) {
334                        isForward = false;
335                        firePropertyChange(ISFORWARD, true, isForward);
336                    } else if (lines[4].equals("f") && !isForward) {
337                        isForward = true;
338                        firePropertyChange(ISFORWARD, false, isForward);
339                    }
340                    
341                    updateFunction(1,lines[5].equals("1"));
342                    updateFunction(2,lines[6].equals("1"));
343                    updateFunction(3,lines[7].equals("1"));
344                    updateFunction(4,lines[8].equals("1"));
345                } catch (NumberFormatException ex) {
346                    log.error("Error phrasing reply from MC", ex);
347                }
348            } else if (tr.match("FX " + address.getNumber()) >= 0) {
349                String[] lines = tr.toString().split(" ");
350                try {
351                    updateFunction(9,lines[2].equals("1"));
352                    updateFunction(10,lines[3].equals("1"));
353                    updateFunction(11,lines[4].equals("1"));
354                    updateFunction(12,lines[5].equals("1"));
355                    updateFunction(13,lines[6].equals("1"));
356                    updateFunction(14,lines[7].equals("1"));
357                } catch (RuntimeException ex) {
358                    log.error("Error phrasing reply from MC", ex);
359                }
360            } else if (tr.match("F " + address.getNumber()) >= 0) {
361                String[] lines = tr.toString().split(" ");
362                try {
363                    updateFunction(1,lines[2].equals("1"));
364                    updateFunction(2,lines[3].equals("1"));
365                    updateFunction(3,lines[4].equals("1"));
366                    updateFunction(4,lines[5].equals("1"));
367                    updateFunction(5,lines[6].equals("1"));
368                    updateFunction(6,lines[7].equals("1"));
369                    updateFunction(7,lines[8].equals("1"));
370                    updateFunction(8,lines[9].equals("1"));
371                } catch (RuntimeException ex) {
372                    log.error("Error phrasing reply from MC", ex);
373                }
374            } else if (tr.toString().equals("ERROR: no data.")) {
375                log.debug("Loco has no data");
376            }
377        }
378    }
379    
380    private void appendFuncString(int Fn, StringBuilder sb, boolean value){
381        updateFunction(Fn,value);
382        if (getFunction(Fn)){
383            sb.append("f");
384            sb.append(String.valueOf(Fn));
385        } else {
386            sb.append(String.valueOf(Fn));
387            sb.append("f");
388        }
389        if (Fn<8){
390            sb.append(" ");
391        }
392    }
393
394    // initialize logging
395    private final static Logger log = LoggerFactory.getLogger(TamsThrottle.class);
396
397}