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