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