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 = Math.round((127 - 1) * this.speedSetting);     // -1 for rescale to avoid estop
175        if (this.speedSetting > 0 && value == 0) {
176            value = 1;          // ensure non-zero input results in non-zero output
177        }
178        if (value > 0) {
179            value = value + 1;  // skip estop
180        }
181        if (value > 127) {
182            value = 127;    // max possible speed
183        }
184        if (value < 0) {
185            value = 1;        // emergency stop
186        }
187        StringBuilder sb = new StringBuilder();
188        sb.append("xL ");
189        sb.append(address.getNumber());
190        sb.append(",");
191        sb.append(value);
192        sb.append(",");
193        sb.append(",");
194        sb.append((isForward ? "f" : "r"));
195        sb.append(",");
196        sb.append(",");
197        sb.append(",");
198        sb.append(",");
199
200        TamsMessage tm = new TamsMessage(sb.toString());
201        tm.setBinary(false);
202        tm.setReplyType('L');
203        tc.sendTamsMessage(tm, this);
204        tmq.add(tm);
205
206        firePropertyChange(SPEEDSETTING, oldSpeed, this.speedSetting);
207        record(speed);
208    }
209
210    @Override
211    public void setIsForward(boolean forward) {
212        boolean old = isForward;
213        isForward = forward;
214        synchronized(this) {
215            setSpeedSetting(speedSetting);  // send the command
216        }
217        firePropertyChange(ISFORWARD, old, isForward);
218    }
219
220    private final DccLocoAddress address;
221
222    TamsTrafficController tc;
223
224    @Override
225    public LocoAddress getLocoAddress() {
226        return address;
227    }
228
229    @Override
230    public void throttleDispose() {
231        active = false;
232        TamsMessage tm = TamsMessage.getXEvtLok();
233        tc.removePollMessage(tm, this);
234        finishRecord();
235    }
236
237    @Override
238    public void message(TamsMessage m) {
239        // messages are ignored
240    }
241
242    /**
243     * Convert a Tams speed integer to a float speed value.
244     *
245     * @param lSpeed Tams speed
246     * @return speed as -1 or number between 0 and 1, inclusive
247     */
248    protected float floatSpeed(int lSpeed) {
249        if (lSpeed == 0) {
250            return 0.f;
251        } else if (lSpeed == 1) {
252            return -1.f;   // estop
253        } else if (super.speedStepMode == jmri.SpeedStepMode.NMRA_DCC_128) {
254            return ((lSpeed - 1) / 126.f);
255        } else {
256            // pretty sure this is wrong, it's definitely never going to be < 1.
257            return (int) (lSpeed * 27.f + 0.5) + 1;
258        }
259    }
260
261    @Override
262    public void reply(TamsReply tr) {
263        log.trace("*** Loco reply ***");
264        TamsMessage tm = tmq.isEmpty() ? myDummy() : tmq.poll();
265        if (tm.isBinary()) {//Binary reply
266            //The binary logic as created by Jan
267            //Complete Loco status is given by:
268            //    element(0) = Speed: 0..127, 0 = Stop, 1 = not used, 2 = min. Speed, 127 = max. Speed
269            //    element(1) = F1..F8 (bit #0..7)
270            //    element(2) = low byte of Loco# (A7..A0)
271            //    element(3) = high byte of Loco#, plus Dir and Light status as in:
272            //        bit#   7     6     5     4     3     2     1     0
273            //            +-----+-----+-----+-----+-----+-----+-----+-----+
274            //            | Dir |  FL | A13 | A12 | A11 | A10 | A9  | A8  |
275            //            +-----+-----+-----+-----+-----+-----+-----+-----+
276            //        where:
277            //            Dir Loco direction (1 = forward)
278            //            FL  Light status
279            //            A13..8  high bits of Loco#
280            //    element(4) = 'real' Loco speed (in terms of the Loco type/configuration)
281            //        (please check XLokSts in P50X_LT.TXT for doc on 'real' speed)
282            //Decode address
283            int msb = tr.getElement(3) & 0x3F;
284            int lsb = tr.getElement(2) & 0xFF;
285            int receivedAddress = msb * 256 + lsb;
286            if (log.isTraceEnabled()) { // avoid overhead of StringUtil calls
287                log.trace("reply for loco = {}", receivedAddress);
288                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, ""));
289            }
290            if (receivedAddress == address.getNumber()) {//If correct address then decode the content
291                log.trace("Is my address");
292                try {
293                    StringBuilder sb = new StringBuilder();
294                    Float newSpeed = floatSpeed(tr.getElement(0));
295                    super.setSpeedSetting(newSpeed);
296                    log.trace("f0 = {}", tr.getElement(3) & 0x40);
297                    
298                    appendFuncString(0,sb,((tr.getElement(3) & 0x40) == 64));
299                    
300                    if (((tr.getElement(3) & 0x80) == 0) && isForward) {
301                        isForward = false;
302                        firePropertyChange(ISFORWARD, true, isForward);
303                    }
304                    if (((tr.getElement(3) & 0x80) == 128) && !isForward) {
305                        isForward = true;
306                        firePropertyChange(ISFORWARD, false, isForward);
307                    }
308                    
309                    appendFuncString(1,sb,((tr.getElement(1) & 0x01) == 0x01));
310                    appendFuncString(2,sb,((tr.getElement(1) & 0x02) == 0x02));
311                    appendFuncString(3,sb,((tr.getElement(1) & 0x04) == 0x04));
312                    appendFuncString(4,sb,((tr.getElement(1) & 0x08) == 0x08));
313                    appendFuncString(5,sb,((tr.getElement(1) & 0x10) == 0x10));
314                    appendFuncString(6,sb,((tr.getElement(1) & 0x20) == 0x20));
315                    appendFuncString(7,sb,((tr.getElement(1) & 0x40) == 0x40));
316                    appendFuncString(8,sb,((tr.getElement(1) & 0x80) == 0x80));
317                    
318                    log.trace("Functions: {}", sb );
319                } catch (RuntimeException ex) {
320                    log.error("Error handling reply from MC", ex);
321                }
322            }
323
324        } else {//ASCII reply
325            //The original logic as provided by Kevin
326            if (tr.match("WARNING") >= 0) {
327                return;
328            }
329            if (tr.match("L " + address.getNumber()) >= 0) {
330                try {
331                    log.trace("ASCII address = {}", address.getNumber());
332                    String[] lines = tr.toString().split(" ");
333                    Float newSpeed = floatSpeed(Integer.parseInt(lines[2]));
334                    super.setSpeedSetting(newSpeed);
335                    updateFunction(0,lines[3].equals("1"));
336                    
337                    if (lines[4].equals("r") && isForward) {
338                        isForward = false;
339                        firePropertyChange(ISFORWARD, true, isForward);
340                    } else if (lines[4].equals("f") && !isForward) {
341                        isForward = true;
342                        firePropertyChange(ISFORWARD, false, isForward);
343                    }
344                    
345                    updateFunction(1,lines[5].equals("1"));
346                    updateFunction(2,lines[6].equals("1"));
347                    updateFunction(3,lines[7].equals("1"));
348                    updateFunction(4,lines[8].equals("1"));
349                } catch (NumberFormatException ex) {
350                    log.error("Error phrasing reply from MC", ex);
351                }
352            } else if (tr.match("FX " + address.getNumber()) >= 0) {
353                String[] lines = tr.toString().split(" ");
354                try {
355                    updateFunction(9,lines[2].equals("1"));
356                    updateFunction(10,lines[3].equals("1"));
357                    updateFunction(11,lines[4].equals("1"));
358                    updateFunction(12,lines[5].equals("1"));
359                    updateFunction(13,lines[6].equals("1"));
360                    updateFunction(14,lines[7].equals("1"));
361                } catch (RuntimeException ex) {
362                    log.error("Error phrasing reply from MC", ex);
363                }
364            } else if (tr.match("F " + address.getNumber()) >= 0) {
365                String[] lines = tr.toString().split(" ");
366                try {
367                    updateFunction(1,lines[2].equals("1"));
368                    updateFunction(2,lines[3].equals("1"));
369                    updateFunction(3,lines[4].equals("1"));
370                    updateFunction(4,lines[5].equals("1"));
371                    updateFunction(5,lines[6].equals("1"));
372                    updateFunction(6,lines[7].equals("1"));
373                    updateFunction(7,lines[8].equals("1"));
374                    updateFunction(8,lines[9].equals("1"));
375                } catch (RuntimeException ex) {
376                    log.error("Error phrasing reply from MC", ex);
377                }
378            } else if (tr.toString().equals("ERROR: no data.")) {
379                log.debug("Loco has no data");
380            }
381        }
382    }
383    
384    private void appendFuncString(int Fn, StringBuilder sb, boolean value){
385        updateFunction(Fn,value);
386        if (getFunction(Fn)){
387            sb.append("f");
388            sb.append(String.valueOf(Fn));
389        } else {
390            sb.append(String.valueOf(Fn));
391            sb.append("f");
392        }
393        if (Fn<8){
394            sb.append(" ");
395        }
396    }
397
398    // initialize logging
399    private final static Logger log = LoggerFactory.getLogger(TamsThrottle.class);
400
401}