001package jmri.jmrix.openlcb;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004import jmri.DccLocoAddress;
005import jmri.LocoAddress;
006import jmri.SpeedStepMode;
007import jmri.jmrix.AbstractThrottle;
008import jmri.SystemConnectionMemo;
009
010import org.openlcb.NodeID;
011import org.openlcb.OlcbInterface;
012import org.openlcb.implementations.VersionedValueListener;
013import org.openlcb.implementations.throttle.RemoteTrainNode;
014import org.openlcb.implementations.throttle.TractionThrottle;
015import org.slf4j.Logger;
016import org.slf4j.LoggerFactory;
017
018import java.util.ArrayList;
019import java.util.List;
020
021import static org.openlcb.messages.TractionControlRequestMessage.MPH;
022
023/**
024 * An implementation of DccThrottle for OpenLCB.
025 *
026 * @author Bob Jacobsen Copyright (C) 2012
027 */
028public class OlcbThrottle extends AbstractThrottle {
029        
030    /**
031     * Constructor
032     * @param address Dcc loco address
033     * @param memo system connection memo
034     */
035    public OlcbThrottle(DccLocoAddress address, SystemConnectionMemo memo) {
036        super(memo);
037        OlcbInterface iface = memo.get(OlcbInterface.class);
038
039        // cache settings. It would be better to read the
040        // actual state, but I don't know how to do this
041        synchronized(this) {
042            this.speedSetting = 0;
043            speedStepMode = SpeedStepMode.NMRA_DCC_128;
044        }
045        // Functions default to false
046        this.isForward = true;
047
048        this.address = address;
049
050        // create OpenLCB library object that does the magic & activate
051        if (iface.getNodeStore() == null) {
052            log.error("Failed to access Mimic Node Store");
053        }
054        if (iface.getDatagramService() == null) {
055            log.error("Failed to access Datagram Service");
056        }
057        ot = new TractionThrottle(iface);
058        NodeID nid;
059        if (address instanceof OpenLcbLocoAddress) {
060            nid = ((OpenLcbLocoAddress) address).getNode();
061        } else {
062            nid = guessDCCNodeID(this.address.isLongAddress(), this.address.getNumber());
063        }
064        ot.start(new RemoteTrainNode(nid, iface));
065
066        speedListener = new VersionedValueListener<Float>(ot.getSpeed()) {
067            @Override
068            public void update(Float speedAndDir) {
069                updateSpeedAndDirFromNetwork(speedAndDir);
070            }
071        };
072        for (int i = 0; i <= 28; i++) {
073            int finalI = i;
074            fnListeners.add(new VersionedValueListener<Boolean>(ot.getFunction(finalI)) {
075                @Override
076                public void update(Boolean state) {
077                   updateFunction(finalI, state);
078                }
079            });
080        }
081    }
082
083    public static NodeID guessDCCNodeID(boolean isLong, int dccAddress) {
084        // Here we make a guess at the OpenLCB Node ID that represents the given DCC address.
085        // This should be replaced by a lookup protocol, but we don't have code for that yet.
086        // 0x060100000000 is reserved by the OpenLCB Unique Identifiers Standard for DCC
087        // locomotives. Within that range we guess using a simple encoding of short address
088        // being as-is, long address being OR-ed with 0xC000. This is close to the DCC
089        // protocol's bit layout (e.g. CV17/CV18, CV1).
090        if (isLong) {
091            return new NodeID(new byte[]{6, 1, 0, 0, (byte) (((dccAddress >> 8) & 0xFF) | 0xC0),
092                    (byte) (dccAddress & 0xFF)});
093        } else {
094            return new NodeID(new byte[]{6, 1, 0, 0, 0, (byte) (dccAddress & 0xFF)});
095        }
096    }
097
098    final TractionThrottle ot;
099
100    final DccLocoAddress address;
101    VersionedValueListener<Float> speedListener;
102    List<VersionedValueListener<Boolean>> fnListeners = new ArrayList<>();
103
104    /** 
105     * {@inheritDoc} 
106     */
107    @Override
108    public LocoAddress getLocoAddress() {
109        return address;
110    }
111
112    /** 
113     * {@inheritDoc} 
114     */
115    @Override
116    public String toString() {
117        return getLocoAddress().toString();
118    }
119
120    /**
121     * Set the speed and direction
122     * <p>
123     * This intentionally skips the emergency stop value of 1.
124     *
125     * @param speed Number from 0 to 1; less than zero is emergency stop
126     */
127    @SuppressFBWarnings(value = "FE_FLOATING_POINT_EQUALITY") // OK to compare floating point, notify on any change
128    @Override
129    public synchronized void setSpeedSetting(float speed) {
130        float oldSpeed = this.speedSetting;
131        if (speed > 1.0) {
132            log.warn("Speed was set too high: {}", speed);
133        }
134        this.speedSetting = speed;
135
136        // send to OpenLCB
137        if (speed >= 0.0) {
138            speedListener.setFromOwner(getSpeedAndDir());
139        } else {
140            speedListener.setFromOwner(Float.NaN);
141        }
142        log.debug("Speed set update old {} new {} int", oldSpeed, speedSetting);
143
144        // notify 
145        firePropertyChange(SPEEDSETTING, oldSpeed, this.speedSetting);
146        record(speed);
147    }
148
149    /**
150     * Called when the speed and direction value is updated from a network feedback. This is
151     * typically originating from another throttle, possibly controlling another consist member.
152     * @param speedAndDir speed and direction in meters per second, negative for reverse; -0.0 is
153     *                   different than +0.0
154     */
155    private void updateSpeedAndDirFromNetwork(Float speedAndDir) {
156        float newSpeed;
157        float direction = Math.copySign(1.0f, speedAndDir);
158        if (speedAndDir.isNaN()) {
159            // e-stop
160            newSpeed = -1.0f;
161            direction = isForward ? 1.0f : -1.0f;
162        } else {
163            newSpeed = speedAndDir / (126 * (float) MPH);
164            if (direction < 0) {
165                newSpeed = -newSpeed;
166            }
167        }
168        float oldSpeed;
169        boolean oldDir;
170        synchronized(this) {
171            oldSpeed = speedSetting;
172            oldDir = isForward;
173            speedSetting = newSpeed;
174            isForward = direction > 0;
175            log.debug("Speed listener update old {} new {}", oldSpeed, speedSetting);
176            firePropertyChange(SPEEDSETTING, oldSpeed, speedSetting);
177            if (oldDir != isForward) {
178                firePropertyChange(ISFORWARD, oldDir, isForward);
179            }
180        }
181    }
182
183    /** 
184     * {@inheritDoc} 
185     */
186    @Override
187    public void setIsForward(boolean forward) {
188        boolean old = isForward;
189        isForward = forward;
190        synchronized(this) {
191            speedListener.setFromOwner(getSpeedAndDir());
192        }
193        firePropertyChange(ISFORWARD, old, isForward);
194    }
195
196    /**
197     * @return the speed and direction as an OpenLCB value.
198     */
199    private float getSpeedAndDir() {
200        float sp = speedSetting * 126 * (float)MPH;
201        if (speedSetting < 0) {
202            // e-stop is encoded as negative speed setting.
203            sp = 0;
204        }
205        return Math.copySign(sp, isForward ? 1.0f : -1.0f);
206    }
207
208    /** 
209     * {@inheritDoc} 
210     */
211    @Override
212    public void setFunction(int functionNum, boolean newState) {
213        updateFunction(functionNum, newState);
214        // send to OpenLCB
215        if (functionNum >= 0 && functionNum < fnListeners.size()) {
216            fnListeners.get(functionNum).setFromOwner(newState);
217        }
218    }
219
220    /** 
221     * {@inheritDoc} 
222     */
223    @Override
224    public void throttleDispose() {
225        log.debug("throttleDispose() called for address {}", address);
226        speedListener.release();
227        for (VersionedValueListener<Boolean> l: fnListeners) {
228            l.release();
229        }
230        ot.release();
231        finishRecord();
232    }
233
234    // initialize logging
235    private final static Logger log = LoggerFactory.getLogger(OlcbThrottle.class);
236
237}