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        // the command station session state is now known, so the next
108        // speed / direction set by JMRI must always be sent to the layout
109        oldCbusSpeed = -2;
110        updateSpeedSetting( speed & 0x7f );
111        updateIsForward ( (speed & 0x80) == 0x80 );
112        updateFunctionGroup(1,f0f4);
113        updateFunctionGroup(2,f5f8);
114        updateFunctionGroup(3,f9f12);
115    }
116
117    /**
118     * setSpeedStepMode - set the speed step value.
119     * <p>
120     * Overridden to capture mode changes to be forwarded to the hardware.
121     * New throttles default to 128 step mode.
122     * CBUS Command stations also default to 128SS so this does not
123     * need to be sent if unchanged.
124     *
125     * @param stepMode the current speed step mode - default should be 128
126     *              speed step mode in most cases
127     */
128    @Override
129    public void setSpeedStepMode(SpeedStepMode stepMode) {
130        if (speedStepMode==stepMode){
131            return;
132        }
133        super.setSpeedStepMode(stepMode);
134        int mode;
135        switch (speedStepMode) {
136            case NMRA_DCC_28:
137                mode = CbusConstants.CBUS_SS_28;
138                break;
139            case NMRA_DCC_14:
140                mode = CbusConstants.CBUS_SS_14;
141                break;
142            default:
143                mode = CbusConstants.CBUS_SS_128;
144                break;
145        }
146        if (hasActiveSession()) {
147            cs.setSpeedSteps(_handle, mode);
148        } else {
149            log.debug("no active session, speed step mode {} not sent to layout", stepMode);
150        }
151    }
152
153    /**
154     * Check if this Throttle currently holds a command station session which
155     * can be sent to.
156     * <p>
157     * False when the Throttle has been disposed, or when the session has been
158     * stolen and is awaiting recovery, in which case the handle is -1.
159     * Local Throttle state is still updated in these cases, but nothing is
160     * sent to the layout.
161     * @return true if a valid session handle and command station are present.
162     */
163    private boolean hasActiveSession() {
164        return ( cs != null && _handle >= 0 && !_isStolen );
165    }
166
167    /**
168     * Convert a CBUS speed integer to a float speed value
169     * @param lSpeed -1 to 127
170     * @return float value -1 to 1
171     */
172    protected float floatSpeed(int lSpeed) {
173        if (this.getSpeedStepMode()== SpeedStepMode.NMRA_DCC_28) {
174            float toReturn = 0.f;
175            switch (lSpeed) {
176                case 0:
177                case 1:
178                    break;
179                case 2:
180                case 3:
181                    toReturn =  -1.f;   // estop
182                    break;
183                default:
184                    toReturn = ((lSpeed - 3) / (float) speedStepMode.numSteps );
185            }
186            return Math.min(toReturn, 1.0f); // return smallest value
187        }
188
189        switch (lSpeed) {
190            case 0:
191                return 0.f;
192            case 1:
193                return -1.f;   // estop
194            default:
195                return ((lSpeed - 1) / (float) speedStepMode.numSteps );
196        }
197    }
198
199    /**
200     * Send the CBUS message to set the state of functions F0, F1, F2, F3, F4.
201     */
202    @Override
203    protected void sendFunctionGroup1() {
204        sendFunctionGroup(1);
205    }
206
207    /**
208     * Send the CBUS message to set the state of functions F5, F6, F7, F8.
209     */
210    @Override
211    protected void sendFunctionGroup2() {
212        sendFunctionGroup(2);
213    }
214
215    /**
216     * Send the CBUS message to set the state of functions F9, F10, F11, F12.
217     */
218    @Override
219    protected void sendFunctionGroup3() {
220        sendFunctionGroup(3);
221    }
222
223    /**
224     * Send the CBUS message to set the state of functions F13, F14, F15, F16,
225     * F17, F18, F19, F20
226     */
227    @Override
228    protected void sendFunctionGroup4() {
229        sendFunctionGroup(4);
230    }
231
232    /**
233     * Send the CBUS message to set the state of functions F21, F22, F23, F24,
234     * F25, F26, F27, F28
235     */
236    @Override
237    protected void sendFunctionGroup5() {
238        sendFunctionGroup(5);
239    }
240    
241    /**
242     * Send the CBUS message to set the state of functions F29 - F36
243     */
244    @Override
245    protected void sendFunctionGroup6() {
246        sendFunctionGroup(6);
247    }
248    
249    protected void sendFunctionGroup(int group) {
250        if (!hasActiveSession()) {
251            log.debug("no active session, function group {} not sent to layout", group);
252            return;
253        }
254        int totVal = 0;
255        for ( int i=0; i<CbusConstants.MAX_FUNCTIONS; i++ ){
256            if (FUNCTION_GROUPS[i]==group && getFunction(i)){
257                totVal += CbusConstants.CBUS_FUNCTION_BITS[i];
258            }
259        }
260        cs.setFunctions(group, _handle, totVal);
261    }
262
263    protected void updateFunctionGroup(int group, int fns) {
264        for ( int i=0; i<CbusConstants.MAX_FUNCTIONS; i++ ){
265            if (FUNCTION_GROUPS[i]==group){
266                updateFunction( i, (fns & CbusConstants.CBUS_FUNCTION_BITS[i]) == CbusConstants.CBUS_FUNCTION_BITS[i] );
267            }
268        }
269    }
270
271    /**
272     * Set the speed.
273     * <p>
274     * This intentionally skips the emergency stop value of 1.
275     *
276     * @param speed Number from 0 to 1; less than zero is emergency stop
277     */
278    @Override
279    public synchronized void setSpeedSetting(float speed, boolean allowDuplicates, boolean allowDuplicatesOnStop) {
280        log.debug("setSpeedSetting({})", speed);
281        float oldSpeed = this.speedSetting;
282        this.speedSetting = speed;
283        if (speed < 0) {
284            this.speedSetting = -1.f;
285        }
286
287        setDispatchActive(this.speedSetting > 0);
288
289        if ( oldCbusSpeed != getCbusSpeedDirection() || allowDuplicates || ( speed==0 && allowDuplicatesOnStop ) ) {
290            sendToLayout();
291            firePropertyChange(SPEEDSETTING, oldSpeed, this.speedSetting);
292            record(this.speedSetting); // float
293        }
294    }
295
296    private synchronized int getCbusSpeedFromFloat(){
297        switch (speedStepMode) {
298            case NMRA_DCC_28:
299                int ints = intSpeed( this.speedSetting, 29 ); 
300                // speed 1 starts at cbus 4, not 2. 
301                if (ints>1){
302                    ints += 2;
303                }
304                return ints;
305            case NMRA_DCC_14:
306                return intSpeed( this.speedSetting, 15 );
307            case NMRA_DCC_128:
308            default:
309                // cbus speeds 0x00 to 0x80 , excluding 0x01 for estop gives 127 possible values including 0.
310                return intSpeed( this.speedSetting );
311        }
312    }
313    
314    private synchronized int getCbusSpeedDirection(){
315        int speed = getCbusSpeedFromFloat();
316        if (this.isForward) {
317            speed |= 0x80;
318        }
319        return speed;
320    }
321
322    private int oldCbusSpeed = -2;
323
324    // following a speed or direction change, sends to layout
325    private void sendToLayout() {
326        int newCbusSpeed = getCbusSpeedDirection();
327        if (!hasActiveSession()) {
328            // disposed, or stolen and awaiting session recovery,
329            // local state is updated but nothing is sent
330            log.debug("no active session, speed/dir {} not sent to layout", newCbusSpeed);
331            return;
332        }
333        oldCbusSpeed = newCbusSpeed;
334
335        log.debug("Sending speed/dir for speed: {}",oldCbusSpeed);
336        // reset keep-alive timeout, timer may have been removed by a concurrent dispose
337        javax.swing.Timer timer = mRefreshTimer;
338        if ( timer != null ) {
339            timer.restart();
340        }
341        cs.setSpeedDir(_handle, oldCbusSpeed);
342    }
343
344    /**
345     * Update the throttles speed setting without sending to hardware. Used to
346     * support CBUS sharing by taking speed received <b>from</b> the hardware in
347     * an OPC_DSPD message.
348     * <p>
349     * No compensation required for a direction flag
350     * @param speed integer speed value
351     */
352    protected synchronized void updateSpeedSetting(int speed) {
353        
354        log.debug("Updated speed/dir for speed:{}",speed);
355        
356        float oldSpeed = this.speedSetting;
357        this.speedSetting = floatSpeed(speed);
358        if (speed < 0) {
359            this.speedSetting = -1.f;
360        }
361
362        setDispatchActive(this.speedSetting > 0);
363
364        if (Math.abs(oldSpeed - this.speedSetting) > 0.0001) { // an increment FROM CBUS will always be > 0.00793
365            firePropertyChange(SPEEDSETTING, oldSpeed, this.speedSetting);
366            record(this.speedSetting); // float
367        }
368    }
369
370    /**
371     * Set the direction and reset speed.
372     * Forwards to the layout
373     * {@inheritDoc}
374     */
375    @Override
376    public void setIsForward(boolean forward) {
377        boolean old = this.isForward;
378        this.isForward = forward;
379        if (old != this.isForward) {
380            sendToLayout();
381            firePropertyChange(ISFORWARD, old, isForward);
382        }
383    }
384    
385    /**
386     * Update the throttles direction without sending to hardware.Used to
387     * support CBUS sharing by taking direction received <b>from</b> the
388     * hardware in an OPC_DSPD message.
389     * @param forward True if Forward, else False
390     */
391    protected void updateIsForward(boolean forward){
392        super.setIsForward(forward);
393    }
394
395    /**
396     * {@inheritDoc}
397     */
398    @Override
399    public String toString() {
400        return getLocoAddress().toString();
401    }
402
403    /**
404     * Return the handle for this throttle
405     *
406     * @return integer session handle
407     */
408    protected int getHandle() {
409        return _handle;
410    }
411    
412    /**
413     * Set the handle for this throttle
414     * <p>
415     * This is normally done on Throttle Construction but certain
416     * operations, eg. recovering from an external steal
417     * may need to change this.
418     * @param newHandle session handle
419     */
420    protected void setHandle(int newHandle){
421        _handle = newHandle;
422    }
423
424    /**
425     * Set Throttle Stolen Flag
426     * <p>
427     * This is false on Throttle Construction but certain
428     * operations may need to change this, eg. an external steal.
429     * <p>
430     * Sends IsAvailable Property Change Notification
431     * @param isStolen true if Throttle has been stolen, else false
432     */
433    protected void setStolen(boolean isStolen){
434        if (isStolen != _isStolen){
435            boolean wasStolen = _isStolen;
436            _isStolen = isStolen; // update state before notifying listeners
437            firePropertyChange("IsAvailable", !wasStolen, !isStolen); // PCL is opposite of local boolean
438        }
439        if (isStolen){ // stop keep-alive messages
440            if ( mRefreshTimer != null ) {
441                mRefreshTimer.stop();
442            }
443            mRefreshTimer = null;
444        }
445        else {
446            startRefresh(); // resume keep-alive messages
447        }
448    }
449    
450    /**
451     * Get Throttle Stolen Flag
452     * <p>
453     * This is false on Throttle Construction but certain
454     * operations may need to change this, eg. an external steal.
455     * @return true if Throttle has been stolen, else false
456     */
457    protected boolean isStolen(){
458        return _isStolen;
459    }
460
461    /**
462     * Get the number of external steal recovery attempts
463     * @return Number of attempts since last reset
464     */
465    protected int getNumRecoverAttempts(){
466        return _recoveryAttempts;
467    }
468
469    /**
470     * Increase a count of external steal recovery attempts
471     */
472    protected void increaseNumRecoverAttempts(){
473        _recoveryAttempts++;
474    }
475    
476    /**
477     * Reset count of recovery attempts
478     */
479    protected void resetNumRecoverAttempts(){
480        _recoveryAttempts = 0;
481    }
482
483    /**
484     * Release session from a command station
485     * ie. throttle with clean full dispose called from releaseThrottle
486     */
487    protected void releaseFromCommandStation(){
488        if ( hasActiveSession() ) {
489            cs.releaseSession(_handle);
490        } else {
491            log.debug("no active session to release for {}", this);
492        }
493    }
494
495    /**
496     * Dispose when finished with this object. After this, further usage of this
497     * Throttle object will result in a JmriException.
498     */
499    @Override
500    public void throttleDispose() {
501        log.debug("dispose");
502        
503        finishRecord();
504        
505        notifyThrottleDisconnect();
506        
507        // stop timeout
508        if ( mRefreshTimer != null ) {
509            mRefreshTimer.stop();
510        }
511        mRefreshTimer = null;
512        cs = null;
513        _handle = -1;
514        
515    }
516
517    private javax.swing.Timer mRefreshTimer;
518
519    // CBUS command stations expect DSPD per sesison every 4s
520    protected final void startRefresh() {
521        mRefreshTimer = new javax.swing.Timer(4000, (java.awt.event.ActionEvent e) -> keepAlive());
522        mRefreshTimer.setRepeats(true);     // refresh until stopped by dispose
523        mRefreshTimer.start();
524    }
525
526    /**
527     * Internal routine to resend the speed on a timeout
528     */
529    private synchronized void keepAlive() {
530        // cs is null and handle -1 if terminated, handle -1 if stolen
531        if (!hasActiveSession()) {
532            return;
533        }
534        cs.sendKeepAlive(_handle);
535
536        // reset timeout, refresh until stopped by dispose
537        // timer may have been removed by a concurrent dispose or steal
538        javax.swing.Timer timer = mRefreshTimer;
539        if ( timer != null ) {
540            timer.restart();
541        }
542    }
543
544    /**
545     * {@inheritDoc}
546     */
547    @Override
548    public LocoAddress getLocoAddress() {
549        return dccAddress;
550    }
551    
552    /**
553     * Adds extra check for num of this JMRI throttle users before notifying
554     * and makes sure these always get sent as a pair
555     * the abstracts only send to ThrottleListeners if value has been changed
556     * @param newval set true if dispatch can be enabled, else false
557     */
558    protected void setDispatchActive( boolean newval){
559        
560        // feature disabled if command station not listed in CBUS node table,
561        // could be CANCMD v3 or in test
562        if ( cs == null ) {
563            return;
564        }
565        if ( cs.getMasterCommandStation() == null ) {
566            return;
567        }
568        
569        if ( newval ) {
570            int numThrottles = adapterMemo.get(ThrottleManager.class).getThrottleUsageCount(dccAddress);
571            log.debug("numThrottles {}",numThrottles);
572            if ( numThrottles < 2 ){
573                notifyThrottleReleaseEnabled(false);
574                notifyThrottleDispatchEnabled(true);
575                return;
576            }
577        }
578        notifyThrottleReleaseEnabled(true);
579        notifyThrottleDispatchEnabled(false);
580    }
581
582    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CbusThrottle.class);
583
584}