001package jmri.jmrix.can.cbus;
002
003import jmri.DccLocoAddress;
004import jmri.LocoAddress;
005import jmri.SpeedStepMode;
006import jmri.ThrottleManager;
007import jmri.jmrix.AbstractThrottle;
008import jmri.jmrix.can.CanSystemConnectionMemo;
009
010/**
011 * An implementation of DccThrottle via AbstractThrottle with code specific to a
012 * CBUS connection.
013 * <p>
014 * Speed in the Throttle interfaces and AbstractThrottle is a float, but in CBUS
015 * is normally an int with values from 0 to 127.
016 * <table>
017 * <caption>CBUS 128 Speed Steps</caption>
018 * <tr><td>setSpeedSetting</td><td>CBUS DSPD</td><td>Translated</td><td>Throttle</td></tr>
019 * <tr><td>0</td><td> 0 </td><td> Speed 0 </td><td> 0 % </td></tr>
020 * <tr><td>-1</td><td> 1 </td><td> E Stop </td><td> 0 % </td></tr>
021 * <tr><td>0.007937</td><td> 2 </td><td> Speed 1 </td><td> 1/126 % </td></tr>
022 * <tr><td>0.015873</td><td> 3 </td><td> Speed 2 </td><td> 2/126 % </td></tr>
023 * <tr><td>0.984127</td><td> 125 </td><td> Speed 124 </td><td> 124/126 % </td></tr>
024 * <tr><td>0.992063</td><td> 126 </td><td> Speed 125 </td><td> 125/126 % </td></tr>
025 * <tr><td>1</td><td> 127 </td><td> Speed 126 </td><td> 100 % </td></tr>
026 * </table>
027 * 
028 * <table>
029 * <caption>CBUS 28 Speed Steps</caption>
030 * <tr><td>CBUS DSPD</td><td>Translated</td><td>Throttle</td></tr>
031 * <tr><td> 0 </td><td> Speed 0 Encoding 1 </td><td> 0 % </td></tr>
032 * <tr><td> 1 </td><td> Speed 0 Encoding 2 </td><td> 0 % </td></tr>
033 * <tr><td> 2 </td><td> E Stop Encoding 1 </td><td> 0 % </td></tr>
034 * <tr><td> 3 </td><td> E Stop Encoding 2 </td><td> 0 % </td></tr>
035 * <tr><td> 4 </td><td> Speed 1 </td><td> 1/28 % </td></tr>
036 * <tr><td> 5 </td><td> Speed 2 </td><td> 2/28 % </td></tr>
037 * <tr><td> 29 </td><td> Speed 26 </td><td> 26/28 % </td></tr>
038 * <tr><td> 30 </td><td> Speed 27 </td><td> 27/28 % </td></tr>
039 * <tr><td> 31 </td><td> Speed 28 </td><td> 100 % </td></tr>
040 * </table>
041 * 
042 * <table>
043 * <caption>CBUS 14 Speed Steps</caption>
044 * <tr><td>CBUS DSPD</td><td>Translated</td><td>Throttle</td></tr>
045 * <tr><td> 0 </td><td> Speed 0  </td><td> 0 % </td></tr>
046 * <tr><td> 1 </td><td> E Stop </td><td> 0 % </td></tr>
047 * <tr><td> 2 </td><td> Speed 1 </td><td> 1/14 % </td></tr>
048 * <tr><td> 3 </td><td> Speed 2 </td><td> 2/14 % </td></tr>
049 * <tr><td> 13 0x0D </td><td> Speed 12 </td><td> 12/14 % </td></tr>
050 * <tr><td> 14 0x0E </td><td> Speed 13 </td><td> 13/14 % </td></tr>
051 * <tr><td> 15 0x0F </td><td> Speed 14 </td><td> 100 % </td></tr>
052 * </table>
053 * 
054 * @author Andrew Crosland Copyright (C) 2009
055 */
056public class CbusThrottle extends AbstractThrottle {
057
058    private CbusCommandStation cs = null;
059    private int _handle = -1;
060    private DccLocoAddress dccAddress = null;
061    private boolean _isStolen;
062    private int _recoveryAttempts;
063
064    /**
065     * Constructor
066     *
067     * @param memo System Connection
068     * @param address The address this throttle relates to.
069     * @param handle the Session ID for the Throttle
070     */
071    public CbusThrottle(CanSystemConnectionMemo memo, LocoAddress address, int handle) {
072        super(memo, CbusConstants.MAX_FUNCTIONS);
073        log.debug("creating new CbusThrottle address {} handle {}",address,handle);
074        if (!( address instanceof DccLocoAddress )  ){
075            log.error("{} is not a DccLocoAddress",address);
076            return;
077        }
078        cs = (CbusCommandStation) adapterMemo.get(jmri.CommandStation.class);
079        _handle = handle;
080        _isStolen = false;
081        _recoveryAttempts = 0;
082
083        // cache settings
084        synchronized(this) {
085            this.speedSetting = 0;
086        }
087        // Functions 0-28 default to false
088        this.dccAddress = (DccLocoAddress) address;
089        this.isForward = true;
090        this.speedStepMode = SpeedStepMode.NMRA_DCC_128;
091
092        // start periodically sending keep alives, to keep this attached
093        log.debug("Start Throttle refresh");
094        startRefresh();
095    }
096
097    /**
098     * Set initial throttle values as taken from PLOC reply from hardware
099     *
100     * @param speed including direction flag
101     * @param f0f4 Functions 0-4
102     * @param f5f8 Functions 5-8
103     * @param f9f12 Functions 9-12
104     */
105    protected void throttleInit(int speed, int f0f4, int f5f8, int f9f12) {
106        log.debug("Setting throttle initial values");
107        updateSpeedSetting( speed & 0x7f );
108        updateIsForward ( (speed & 0x80) == 0x80 );
109        updateFunctionGroup(1,f0f4);
110        updateFunctionGroup(2,f5f8);
111        updateFunctionGroup(3,f9f12);
112    }
113
114    /**
115     * setSpeedStepMode - set the speed step value.
116     * <p>
117     * Overridden to capture mode changes to be forwarded to the hardware.
118     * New throttles default to 128 step mode.
119     * CBUS Command stations also default to 128SS so this does not
120     * need to be sent if unchanged.
121     *
122     * @param stepMode the current speed step mode - default should be 128
123     *              speed step mode in most cases
124     */
125    @Override
126    public void setSpeedStepMode(SpeedStepMode stepMode) {
127        if (speedStepMode==stepMode){
128            return;
129        }
130        super.setSpeedStepMode(stepMode);
131        int mode;
132        switch (speedStepMode) {
133            case NMRA_DCC_28:
134                mode = CbusConstants.CBUS_SS_28;
135                break;
136            case NMRA_DCC_14:
137                mode = CbusConstants.CBUS_SS_14;
138                break;
139            default:
140                mode = CbusConstants.CBUS_SS_128;
141                break;
142        }
143        cs.setSpeedSteps(_handle, mode);
144    }
145
146    /**
147     * Convert a CBUS speed integer to a float speed value
148     * @param lSpeed -1 to 127
149     * @return float value -1 to 1
150     */
151    protected float floatSpeed(int lSpeed) {
152        if (this.getSpeedStepMode()== SpeedStepMode.NMRA_DCC_28) {
153            float toReturn = 0.f;
154            switch (lSpeed) {
155                case 0:
156                case 1:
157                    break;
158                case 2:
159                case 3:
160                    toReturn =  -1.f;   // estop
161                    break;
162                default:
163                    toReturn = ((lSpeed - 3) / (float) speedStepMode.numSteps );
164            }
165            return Math.min(toReturn, 1.0f); // return smallest value
166        }
167
168        switch (lSpeed) {
169            case 0:
170                return 0.f;
171            case 1:
172                return -1.f;   // estop
173            default:
174                return ((lSpeed - 1) / (float) speedStepMode.numSteps );
175        }
176    }
177
178    /**
179     * Send the CBUS message to set the state of functions F0, F1, F2, F3, F4.
180     */
181    @Override
182    protected void sendFunctionGroup1() {
183        sendFunctionGroup(1);
184    }
185
186    /**
187     * Send the CBUS message to set the state of functions F5, F6, F7, F8.
188     */
189    @Override
190    protected void sendFunctionGroup2() {
191        sendFunctionGroup(2);
192    }
193
194    /**
195     * Send the CBUS message to set the state of functions F9, F10, F11, F12.
196     */
197    @Override
198    protected void sendFunctionGroup3() {
199        sendFunctionGroup(3);
200    }
201
202    /**
203     * Send the CBUS message to set the state of functions F13, F14, F15, F16,
204     * F17, F18, F19, F20
205     */
206    @Override
207    protected void sendFunctionGroup4() {
208        sendFunctionGroup(4);
209    }
210
211    /**
212     * Send the CBUS message to set the state of functions F21, F22, F23, F24,
213     * F25, F26, F27, F28
214     */
215    @Override
216    protected void sendFunctionGroup5() {
217        sendFunctionGroup(5);
218    }
219    
220    /**
221     * Send the CBUS message to set the state of functions F29 - F36
222     */
223    @Override
224    protected void sendFunctionGroup6() {
225        sendFunctionGroup(6);
226    }
227    
228    protected void sendFunctionGroup(int group) {
229        int totVal = 0;
230        for ( int i=0; i<CbusConstants.MAX_FUNCTIONS; i++ ){
231            if (FUNCTION_GROUPS[i]==group && getFunction(i)){
232                totVal += CbusConstants.CBUS_FUNCTION_BITS[i];
233            }
234        }
235        cs.setFunctions(group, _handle, totVal);
236    }
237
238    protected void updateFunctionGroup(int group, int fns) {
239        for ( int i=0; i<CbusConstants.MAX_FUNCTIONS; i++ ){
240            if (FUNCTION_GROUPS[i]==group){
241                updateFunction( i, (fns & CbusConstants.CBUS_FUNCTION_BITS[i]) == CbusConstants.CBUS_FUNCTION_BITS[i] );
242            }
243        }
244    }
245
246    /**
247     * Set the speed.
248     * <p>
249     * This intentionally skips the emergency stop value of 1.
250     *
251     * @param speed Number from 0 to 1; less than zero is emergency stop
252     */
253    @Override
254    public synchronized void setSpeedSetting(float speed, boolean allowDuplicates, boolean allowDuplicatesOnStop) {
255        log.debug("setSpeedSetting({})", speed);
256        float oldSpeed = this.speedSetting;
257        this.speedSetting = speed;
258        if (speed < 0) {
259            this.speedSetting = -1.f;
260        }
261
262        setDispatchActive(this.speedSetting > 0);
263
264        if ( oldCbusSpeed != getCbusSpeedDirection() || allowDuplicates || ( speed==0 && allowDuplicatesOnStop ) ) {
265            sendToLayout();
266            firePropertyChange(SPEEDSETTING, oldSpeed, this.speedSetting);
267            record(this.speedSetting); // float
268        }
269    }
270
271    private synchronized int getCbusSpeedFromFloat(){
272        switch (speedStepMode) {
273            case NMRA_DCC_28:
274                int ints = intSpeed( this.speedSetting, 29 ); 
275                // speed 1 starts at cbus 4, not 2. 
276                if (ints>1){
277                    ints += 2;
278                }
279                return ints;
280            case NMRA_DCC_14:
281                return intSpeed( this.speedSetting, 15 );
282            case NMRA_DCC_128:
283            default:
284                // cbus speeds 0x00 to 0x80 , excluding 0x01 for estop gives 127 possible values including 0.
285                return intSpeed( this.speedSetting );
286        }
287    }
288    
289    private synchronized int getCbusSpeedDirection(){
290        int speed = getCbusSpeedFromFloat();
291        if (this.isForward) {
292            speed |= 0x80;
293        }
294        return speed;
295    }
296
297    private int oldCbusSpeed = -2;
298
299    // following a speed or direction change, sends to layout
300    private void sendToLayout() {
301        oldCbusSpeed = getCbusSpeedDirection();
302        
303        log.debug("Sending speed/dir for speed: {}",oldCbusSpeed);
304        // reset timeout
305        mRefreshTimer.stop();
306        mRefreshTimer.setRepeats(true);
307        mRefreshTimer.start();
308        if (cs != null ) {
309            cs.setSpeedDir(_handle, oldCbusSpeed);
310        }
311    }
312
313    /**
314     * Update the throttles speed setting without sending to hardware. Used to
315     * support CBUS sharing by taking speed received <b>from</b> the hardware in
316     * an OPC_DSPD message.
317     * <p>
318     * No compensation required for a direction flag
319     * @param speed integer speed value
320     */
321    protected synchronized void updateSpeedSetting(int speed) {
322        
323        log.debug("Updated speed/dir for speed:{}",speed);
324        
325        float oldSpeed = this.speedSetting;
326        this.speedSetting = floatSpeed(speed);
327        if (speed < 0) {
328            this.speedSetting = -1.f;
329        }
330
331        setDispatchActive(this.speedSetting > 0);
332
333        if (Math.abs(oldSpeed - this.speedSetting) > 0.0001) { // an increment FROM CBUS will always be > 0.00793
334            firePropertyChange(SPEEDSETTING, oldSpeed, this.speedSetting);
335            record(this.speedSetting); // float
336        }
337    }
338
339    /**
340     * Set the direction and reset speed.
341     * Forwards to the layout
342     * {@inheritDoc}
343     */
344    @Override
345    public void setIsForward(boolean forward) {
346        boolean old = this.isForward;
347        this.isForward = forward;
348        if (old != this.isForward) {
349            sendToLayout();
350            firePropertyChange(ISFORWARD, old, isForward);
351        }
352    }
353    
354    /**
355     * Update the throttles direction without sending to hardware.Used to
356     * support CBUS sharing by taking direction received <b>from</b> the
357     * hardware in an OPC_DSPD message.
358     * @param forward True if Forward, else False
359     */
360    protected void updateIsForward(boolean forward){
361        super.setIsForward(forward);
362    }
363
364    /**
365     * {@inheritDoc}
366     */
367    @Override
368    public String toString() {
369        return getLocoAddress().toString();
370    }
371
372    /**
373     * Return the handle for this throttle
374     *
375     * @return integer session handle
376     */
377    protected int getHandle() {
378        return _handle;
379    }
380    
381    /**
382     * Set the handle for this throttle
383     * <p>
384     * This is normally done on Throttle Construction but certain
385     * operations, eg. recovering from an external steal
386     * may need to change this.
387     * @param newHandle session handle
388     */
389    protected void setHandle(int newHandle){
390        _handle = newHandle;
391    }
392
393    /**
394     * Set Throttle Stolen Flag
395     * <p>
396     * This is false on Throttle Construction but certain
397     * operations may need to change this, eg. an external steal.
398     * <p>
399     * Sends IsAvailable Property Change Notification
400     * @param isStolen true if Throttle has been stolen, else false
401     */
402    protected void setStolen(boolean isStolen){
403        if (isStolen != _isStolen){
404            firePropertyChange("IsAvailable", isStolen, _isStolen); // PCL is opposite of local boolean
405            _isStolen = isStolen;
406        }
407        if (isStolen){ // stop keep-alive messages
408            if ( mRefreshTimer != null ) {
409                mRefreshTimer.stop();
410            }
411            mRefreshTimer = null;
412        }
413        else {
414            startRefresh(); // resume keep-alive messages
415        }
416    }
417    
418    /**
419     * Get Throttle Stolen Flag
420     * <p>
421     * This is false on Throttle Construction but certain
422     * operations may need to change this, eg. an external steal.
423     * @return true if Throttle has been stolen, else false
424     */
425    protected boolean isStolen(){
426        return _isStolen;
427    }
428
429    /**
430     * Get the number of external steal recovery attempts
431     * @return Number of attempts since last reset
432     */
433    protected int getNumRecoverAttempts(){
434        return _recoveryAttempts;
435    }
436
437    /**
438     * Increase a count of external steal recovery attempts
439     */
440    protected void increaseNumRecoverAttempts(){
441        _recoveryAttempts++;
442    }
443    
444    /**
445     * Reset count of recovery attempts
446     */
447    protected void resetNumRecoverAttempts(){
448        _recoveryAttempts = 0;
449    }
450
451    /**
452     * Release session from a command station
453     * ie. throttle with clean full dispose called from releaseThrottle
454     */
455    protected void releaseFromCommandStation(){
456        if ( cs != null ) {
457            cs.releaseSession(_handle);
458        }
459    }
460
461    /**
462     * Dispose when finished with this object. After this, further usage of this
463     * Throttle object will result in a JmriException.
464     */
465    @Override
466    public void throttleDispose() {
467        log.debug("dispose");
468        
469        finishRecord();
470        
471        notifyThrottleDisconnect();
472        
473        // stop timeout
474        if ( mRefreshTimer != null ) {
475            mRefreshTimer.stop();
476        }
477        mRefreshTimer = null;
478        cs = null;
479        _handle = -1;
480        
481    }
482
483    private javax.swing.Timer mRefreshTimer;
484
485    // CBUS command stations expect DSPD per sesison every 4s
486    protected final void startRefresh() {
487        mRefreshTimer = new javax.swing.Timer(4000, (java.awt.event.ActionEvent e) -> keepAlive());
488        mRefreshTimer.setRepeats(true);     // refresh until stopped by dispose
489        mRefreshTimer.start();
490    }
491
492    /**
493     * Internal routine to resend the speed on a timeout
494     */
495    private synchronized void keepAlive() {
496        if (cs != null) { // cs can be null if in process of terminating?
497            cs.sendKeepAlive(_handle);
498
499            // reset timeout
500            mRefreshTimer.stop();
501            mRefreshTimer.setRepeats(true);     // refresh until stopped by dispose
502            mRefreshTimer.start();
503        }
504    }
505
506    /**
507     * {@inheritDoc}
508     */
509    @Override
510    public LocoAddress getLocoAddress() {
511        return dccAddress;
512    }
513    
514    /**
515     * Adds extra check for num of this JMRI throttle users before notifying
516     * and makes sure these always get sent as a pair
517     * the abstracts only send to ThrottleListeners if value has been changed
518     * @param newval set true if dispatch can be enabled, else false
519     */
520    protected void setDispatchActive( boolean newval){
521        
522        // feature disabled if command station not listed in CBUS node table,
523        // could be CANCMD v3 or in test
524        if ( cs == null ) {
525            return;
526        }
527        if ( cs.getMasterCommandStation() == null ) {
528            return;
529        }
530        
531        if ( newval ) {
532            int numThrottles = adapterMemo.get(ThrottleManager.class).getThrottleUsageCount(dccAddress);
533            log.debug("numThrottles {}",numThrottles);
534            if ( numThrottles < 2 ){
535                notifyThrottleReleaseEnabled(false);
536                notifyThrottleDispatchEnabled(true);
537                return;
538            }
539        }
540        notifyThrottleReleaseEnabled(true);
541        notifyThrottleDispatchEnabled(false);
542    }
543
544    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CbusThrottle.class);
545
546}