001package jmri.jmrix.loconet;
002
003import jmri.implementation.DefaultSignalHead;
004import org.slf4j.Logger;
005import org.slf4j.LoggerFactory;
006
007/**
008 * Extend jmri.SignalHead for signals implemented by an SE8C.
009 * <p>
010 * This implementation writes out to the physical signal when it's commanded to
011 * change appearance, and updates its internal state when it hears commands from
012 * other places.
013 * <p>
014 * To get a complete set of aspects, we assume that the SE8C board has been
015 * configured such that the 4th aspect is "dark". We then do flashing aspects by
016 * commanding the lit appearance to change.
017 * <p>
018 * This is a grandfathered implementation that is specific to LocoNet systems. A
019 * more general implementation, which can work with any system(s), is available
020 * in {@link jmri.implementation.SE8cSignalHead}. This package is maintained so
021 * that existing XML files can continue to be read. In particular, it only works
022 * with the first LocoNet connection (names LHnnn, not L2Hnnn etc).
023 * <p>
024 * The algorithms in this class are a collaborative effort of Digitrax, Inc and
025 * Bob Jacobsen.
026 * <p>
027 * Some of the message formats used in this class are Copyright Digitrax, Inc.
028 * and used with permission as part of the JMRI project. That permission does
029 * not extend to uses in other software products. If you wish to use this code,
030 * algorithm or these message formats outside of JMRI, please contact Digitrax
031 * Inc for separate permission.
032 *
033 * @author Bob Jacobsen Copyright (C) 2002
034 */
035public class SE8cSignalHead extends DefaultSignalHead implements LocoNetListener {
036
037    public SE8cSignalHead(int pNumber, String userName) {
038        // create systemname
039        super("LH" + pNumber, userName); // NOI18N
040        init(pNumber);
041    }
042
043    public SE8cSignalHead(int pNumber) {
044        // create systemname
045        super("LH" + pNumber); // NOI18N
046        init(pNumber);
047    }
048
049    void init(int pNumber) {
050        tc = jmri.InstanceManager.getDefault(LnTrafficController.class);
051        mNumber = pNumber;
052        mAppearance = DARK;  // start turned off
053        // At construction, register for messages
054        tc.addLocoNetListener(~0, this);
055        updateOutput();
056    }
057
058    LnTrafficController tc;
059
060    public int getNumber() {
061        return mNumber;
062    }
063
064    // Handle a request to change state by sending a LocoNet command
065    @Override
066    protected void updateOutput() {
067        // send SWREQ for close
068        LocoNetMessage l = new LocoNetMessage(4);
069        l.setOpCode(LnConstants.OPC_SW_REQ);
070
071        int address = 0;
072        boolean closed = false;
073        if (!mLit) {
074            address = mNumber + 1;
075            closed = true;
076        } else if (!mFlashOn
077                && ((mAppearance == FLASHGREEN)
078                || (mAppearance == FLASHYELLOW)
079                || (mAppearance == FLASHRED))) {
080            // flash says to make output dark; 
081            // flashing-but-lit is handled below
082            address = mNumber + 1;
083            closed = true;
084        } else {
085            // which of the four states?
086            switch (mAppearance) {
087                case FLASHRED:
088                case RED:
089                    address = mNumber;
090                    closed = false;
091                    break;
092                case FLASHYELLOW:
093                case YELLOW:
094                    address = mNumber + 1;
095                    closed = false;
096                    break;
097                case FLASHGREEN:
098                case GREEN:
099                    address = mNumber;
100                    closed = true;
101                    break;
102                case DARK:
103                    address = mNumber + 1;
104                    closed = true;
105                    break;
106                default:
107                    log.error("Invalid state request: {}", mAppearance);
108                    return;
109            }
110        }
111        // compute address fields
112        int hiadr = (address - 1) / 128;
113        int loadr = (address - 1) - hiadr * 128;
114        if (closed) {
115            hiadr |= 0x20;
116        }
117
118        // set "on" bit
119        hiadr |= 0x10;
120
121        // store and send
122        l.setElement(1, loadr);
123        l.setElement(2, hiadr);
124        tc.sendLocoNetMessage(l);
125    }
126
127    // implementing classes will typically have a function/listener to get
128    // updates from the layout, which will then call
129    //  public void firePropertyChange(String propertyName,
130    //      Object oldValue,
131    //      Object newValue)
132    // _once_ if anything has changed state (or set the commanded state directly)
133    @Override
134    public void message(LocoNetMessage l) {
135        int oldAppearance = mAppearance;
136        // parse message type
137        switch (l.getOpCode()) {
138            case LnConstants.OPC_SW_REQ: {               /* page 9 of LocoNet PE */
139
140                int sw1 = l.getElement(1);
141                int sw2 = l.getElement(2);
142                if (myAddress(sw1, sw2)) {
143                    if ((sw2 & LnConstants.OPC_SW_REQ_DIR) != 0) {
144                        // was set CLOSED
145                        if (mAppearance != FLASHGREEN) {
146                            mAppearance = GREEN;
147                        }
148                    } else {
149                        // was set THROWN
150                        if (mAppearance != FLASHRED) {
151                            mAppearance = RED;
152                        }
153                    }
154                }
155                if (myAddressPlusOne(sw1, sw2)) {
156                    if ((sw2 & LnConstants.OPC_SW_REQ_DIR) != 0) {
157                        // was set CLOSED, which means DARK
158                        // don't change if one of the possibilities already
159                        if (!(mAppearance == FLASHYELLOW || mAppearance == DARK
160                                || mAppearance == FLASHGREEN || mAppearance == FLASHRED
161                                || !mLit || !mFlashOn)) {
162                            mAppearance = DARK;    // that's the setting by default
163                        }
164                    } else {
165                        // was set THROWN
166                        if (mAppearance != FLASHYELLOW) {
167                            mAppearance = YELLOW;
168                        }
169                    }
170                }
171                break;
172            }
173            case LnConstants.OPC_SW_REP: {               /* page 9 of LocoNet PE */
174
175                int sw1 = l.getElement(1);
176                int sw2 = l.getElement(2);
177                if (myAddress(sw1, sw2)) {
178                    // see if its a turnout state report
179                    if ((sw2 & LnConstants.OPC_SW_REP_INPUTS) == 0) {
180                        // sort out states
181                        if ((sw2 & LnConstants.OPC_SW_REP_CLOSED) != 0) {
182                            // was set CLOSED
183                            if (mAppearance != FLASHGREEN) {
184                                mAppearance = GREEN;
185                            }
186                        }
187                        if ((sw2 & LnConstants.OPC_SW_REP_THROWN) != 0) {
188                            // was set THROWN
189                            if (mAppearance != FLASHRED) {
190                                mAppearance = RED;
191                            }
192                        }
193                    }
194                }
195                if (myAddressPlusOne(sw1, sw2)) {
196                    // see if its a turnout state report
197                    if ((sw2 & LnConstants.OPC_SW_REP_INPUTS) == 0) {
198                        // was set CLOSED, which means DARK
199                        // don't change if one of the possibilities already
200                        if (!(mAppearance == FLASHYELLOW || mAppearance == DARK
201                                || mAppearance == FLASHGREEN || mAppearance == FLASHRED
202                                || !mLit || !mFlashOn)) {
203                            mAppearance = DARK;    // that's the setting by default
204                        }
205                    }
206                    if ((sw2 & LnConstants.OPC_SW_REP_THROWN) != 0) {
207                        // was set THROWN
208                        if (mAppearance != FLASHYELLOW) {
209                            mAppearance = YELLOW;
210                        }
211                    }
212                }
213                return;
214            }
215            default:
216                return;
217        }
218        // reach here if the state has updated
219        if (oldAppearance != mAppearance) {
220            firePropertyChange("Appearance", Integer.valueOf(oldAppearance), Integer.valueOf(mAppearance)); // NOI18N
221        }
222    }
223
224    @Override
225    public void dispose() {
226        tc.removeLocoNetListener(~0, this);
227        super.dispose();
228    }
229
230    // data members
231    int mNumber;   // LocoNet Turnout number with lower address (0 based)
232
233    private boolean myAddress(int a1, int a2) {
234        // the "+ 1" in the following converts to throttle-visible numbering
235        return (((a2 & 0x0f) * 128) + (a1 & 0x7f) + 1) == mNumber;
236    }
237
238    private boolean myAddressPlusOne(int a1, int a2) {
239        // the "+ 1" in the following converts to throttle-visible numbering
240        return (((a2 & 0x0f) * 128) + (a1 & 0x7f) + 1) == mNumber + 1;
241    }
242    private final static Logger log = LoggerFactory.getLogger(SE8cSignalHead.class);
243
244}