001package jmri.jmrix.cmri.serial;
002
003import jmri.Turnout;
004import jmri.implementation.AbstractTurnout;
005import jmri.jmrix.cmri.CMRISystemConnectionMemo;
006import javax.annotation.Nonnull;
007import javax.annotation.CheckReturnValue;
008import org.slf4j.Logger;
009import org.slf4j.LoggerFactory;
010
011/**
012 * Turnout implementation for C/MRI serial systems.
013 * <p>
014 * This object doesn't listen to the C/MRI communications. This is because it
015 * should be the only object that is sending messages for this turnout; more
016 * than one Turnout object pointing to a single device is not allowed.
017 * <p>
018 * Turnouts on the layout may be controlled by one or two output bits. To
019 * control a turnout from one Turnout object via two output bits, the output
020 * bits must be on the same node, the Turnout address must point to the first
021 * output bit, and the second output bit must follow the output bit at the next
022 * address. Valid states for the two bits controlling the two-bit turnout are:
023 * ON OFF, and OFF ON for the two bits.
024 * <p>
025 * This class can also drive pulsed outputs, which can be combined with the
026 * two-bit option in the expected ways.
027 * <p>
028 * When a Turnout is configured for pulsed and two-output, a request to go to a
029 * new CommandedState sets the desired configuration for the pulse interval,
030 * then sets both leads to their off condition.
031 * <p>
032 * When a Turnout is configured for pulsed and one output, a request to go to a
033 * new CommandedState just sets the output on for the interval; it's assumed
034 * that there's something out on the layout that converts that pulse into a
035 * "flip to other state" operation.
036 * <p>
037 * Finally, this implementation supports the "inverted" option. Inverted applies
038 * to the status of the lead on the C/MRI output itself.
039 * <p>
040 * For example, a pulsed, two-output, inverted turnout will have both pins set
041 * to 1 in the resting state. When THROWN, one lead will be set to 0 for the
042 * configured interval, then set back to 1.
043 * <p>
044 * For more discussion of this, please see the
045 * <a href="http://jmri.org/help/en/html/hardware/cmri/CMRI.shtml#options">documentation
046 * page</a>.
047 *
048 * @author Bob Jacobsen Copyright (C) 2003, 2007, 2008
049 * @author David Duchamp Copyright (C) 2004, 2007
050 * @author Dan Boudreau Copyright (C) 2007
051 */
052public class SerialTurnout extends AbstractTurnout {
053
054     CMRISystemConnectionMemo _memo = null;
055
056    /**
057     * Create a Turnout object, with both system and user names.
058     * <p>
059     * 'systemName' was previously validated in SerialTurnoutManager
060     * @param systemName system name
061     * @param userName user name
062     * @param memo system connection
063     */
064    public SerialTurnout(@Nonnull String systemName, String userName, CMRISystemConnectionMemo memo) {
065        super(systemName, userName);
066        // Save system Name
067        tSystemName = systemName;
068        _memo = memo;
069        // Extract the Bit from the name
070        tBit = _memo.getBitFromSystemName(systemName);
071    }
072
073    /**
074     * Handle a request to change state by sending a turnout command
075     *
076     * @param newState desired new state, one of the Turnout class constants
077     */
078     @Override
079    protected void forwardCommandChangeToLayout(int newState) {
080        // implementing classes will typically have a function/listener to get
081        // updates from the layout, which will then call
082        //  public void firePropertyChange(String propertyName,
083        //                    Object oldValue,
084        //      Object newValue)
085        // _once_ if anything has changed state (or set the commanded state directly)
086
087        // sort out states
088        if ((newState & Turnout.CLOSED) != 0) {
089            // first look for the double case, which we can't handle
090            if ((newState & Turnout.THROWN) != 0) {
091                // this is the disaster case!
092                log.error("Cannot command both CLOSED and THROWN: {}", newState);
093                return;
094            } else {
095                // send a CLOSED command
096                sendMessage(true);
097            }
098        } else {
099            // send a THROWN command
100            sendMessage(false);
101        }
102    }
103
104    /**
105     * C/MRI turnouts do support inversion
106     */
107     @Override
108    public boolean canInvert() {
109        return true;
110    }
111
112     @Override
113    protected void turnoutPushbuttonLockout(boolean _pushButtonLockout) {
114         log.debug("Send command to {} Pushbutton", (_pushButtonLockout ? "Lock" : "Unlock"));
115    }
116
117    // data members
118    String tSystemName; // System Name of this turnout
119    protected int tBit;   // bit number of turnout control in Serial node
120    protected SerialNode tNode = null;
121    protected javax.swing.Timer mPulseClosedTimer = null;
122    protected javax.swing.Timer mPulseThrownTimer = null;
123    protected boolean mPulseTimerOn = false;
124
125    /**
126     * Control the actual layout hardware. The request is for a particular
127     * functional setting, e.g. CLOSED or THROWN. The "inverted" status of the
128     * output leads is handled here.
129     * @param closed True sets the turnout CLOSED
130     */
131    protected void sendMessage(boolean closed) {
132        // if a Pulse Timer is running, ignore the call
133        if (!mPulseTimerOn) {
134            if (tNode == null) {
135                tNode = (SerialNode) _memo.getNodeFromSystemName(tSystemName, _memo.getTrafficController());
136                if (tNode == null) {
137                    // node does not exist, ignore call
138                    log.error("Trying to set a C/MRI turnout that doesn't exist: {} - ignored", tSystemName);
139                    return;
140                }
141            }
142            if (getNumberOutputBits() == 1) {
143                // check for pulsed control
144                if (getControlType() == 0) {
145                    // steady state control, get current status of the output bit
146                    if ((tNode.getOutputBit(tBit) ^ getInverted()) != closed) {
147                        // bit state is different from the requested state, set it
148                        tNode.setOutputBit(tBit, closed ^ getInverted());
149                    } else {
150                        // Bit state is the same as requested state, so nothing
151                        // will happen if requested state is set.
152                        // Check if turnout known state is different from requested state
153                        int kState = getKnownState();
154                        if (closed) {
155                            // CLOSED is being requested
156                            if ((kState & Turnout.THROWN) != 0) {
157                                // known state is different from output bit, set output bit to be correct
158                                //     for known state, then start a timer to set it to requested state
159                                tNode.setOutputBit(tBit, false ^ getInverted());
160                                // start a timer to finish setting this turnout
161                                if (mPulseClosedTimer == null) {
162                                    mPulseClosedTimer = new javax.swing.Timer(tNode.getPulseWidth(), new java.awt.event.ActionListener() {
163                                        @Override
164                                        public void actionPerformed(java.awt.event.ActionEvent e) {
165                                            tNode.setOutputBit(tBit, true ^ getInverted());
166                                            mPulseClosedTimer.stop();
167                                            mPulseTimerOn = false;
168                                        }
169                                    });
170                                }
171                                mPulseTimerOn = true;
172                                mPulseClosedTimer.start();
173                            }
174                        } else {
175                            // THROWN is being requested
176                            if ((kState & Turnout.CLOSED) != 0) {
177                                // known state is different from output bit, set output bit to be correct
178                                //     for known state, then start a timer to set it to requested state
179                                tNode.setOutputBit(tBit, true ^ getInverted());
180                                // start a timer to finish setting this turnout
181                                if (mPulseThrownTimer == null) {
182                                    mPulseThrownTimer = new javax.swing.Timer(tNode.getPulseWidth(), new java.awt.event.ActionListener() {
183                                        @Override
184                                        public void actionPerformed(java.awt.event.ActionEvent e) {
185                                            tNode.setOutputBit(tBit, false ^ getInverted());
186                                            mPulseThrownTimer.stop();
187                                            mPulseTimerOn = false;
188                                        }
189                                    });
190                                }
191                                mPulseTimerOn = true;
192                                mPulseThrownTimer.start();
193                            }
194                        }
195                    }
196                } else {
197                    // Pulse control
198                    int iTime = tNode.getPulseWidth();
199                    // Get current known state of turnout
200                    int kState = getKnownState();
201                    if ((closed && ((kState & Turnout.THROWN) != 0))
202                            || (!closed && ((kState & Turnout.CLOSED) != 0))) {
203                        // known and requested are different, a change is requested
204                        //   Pulse the line, first turn bit on
205                        tNode.setOutputBit(tBit, false ^ getInverted());
206                        // Start a timer to return bit to off state
207                        if (mPulseClosedTimer == null) {
208                            mPulseClosedTimer = new javax.swing.Timer(iTime, new java.awt.event.ActionListener() {
209                                @Override
210                                public void actionPerformed(java.awt.event.ActionEvent e) {
211                                    tNode.setOutputBit(tBit, true ^ getInverted());
212                                    mPulseClosedTimer.stop();
213                                    mPulseTimerOn = false;
214                                }
215                            });
216                        }
217                        mPulseTimerOn = true;
218                        mPulseClosedTimer.start();
219                    }
220                }
221            } else if (getNumberOutputBits() == 2) {
222                // two output bits
223                if (getControlType() == 0) {
224                    // Steady state control e.g. stall motor turnout control
225                    tNode.setOutputBit(tBit, closed ^ getInverted());
226                    tNode.setOutputBit(tBit + 1, !(closed ^ getInverted()));
227                } else {
228                    // Pulse control, 2-bits
229                    int iTime = tNode.getPulseWidth();
230                    // Get current known state of turnout
231                    int kState = getKnownState();
232                    if (closed && ((kState & Turnout.THROWN) != 0)) {
233                        // CLOSED is requested, currently THROWN - Pulse first bit
234                        //   Turn bit on
235                        tNode.setOutputBit(tBit, false ^ getInverted());
236                        // Start a timer to return bit to off state
237                        if (mPulseClosedTimer == null) {
238                            mPulseClosedTimer = new javax.swing.Timer(iTime, new java.awt.event.ActionListener() {
239                                @Override
240                                public void actionPerformed(java.awt.event.ActionEvent e) {
241                                    tNode.setOutputBit(tBit, true ^ getInverted());
242                                    mPulseClosedTimer.stop();
243                                    mPulseTimerOn = false;
244                                }
245                            });
246                        }
247                        mPulseTimerOn = true;
248                        mPulseClosedTimer.start();
249                    } else if (!closed && ((kState & Turnout.CLOSED) != 0)) {
250                        // THROWN is requested, currently CLOSED - Pulse second bit
251                        //   Turn bit on
252                        tNode.setOutputBit(tBit + 1, false ^ getInverted());
253                        // Start a timer to return bit to off state
254                        if (mPulseThrownTimer == null) {
255                            mPulseThrownTimer = new javax.swing.Timer(iTime, new java.awt.event.ActionListener() {
256                                @Override
257                                public void actionPerformed(java.awt.event.ActionEvent e) {
258                                    tNode.setOutputBit(tBit + 1, true ^ getInverted());
259                                    mPulseThrownTimer.stop();
260                                    mPulseTimerOn = false;
261                                }
262                            });
263                        }
264                        mPulseTimerOn = true;
265                        mPulseThrownTimer.start();
266                    }
267                }
268            }
269        }
270    }
271
272    /**
273     * {@inheritDoc} 
274     * 
275     * Sorts by node number and then by bit
276     */
277    @CheckReturnValue
278     @Override
279    public int compareSystemNameSuffix(@Nonnull String suffix1, @Nonnull String suffix2, @Nonnull jmri.NamedBean n) {
280        return CMRISystemConnectionMemo.compareSystemNameSuffix(suffix1, suffix2);
281    }
282
283    private final static Logger log = LoggerFactory.getLogger(SerialTurnout.class);
284
285}