001package jmri.jmrix.loconet;
002
003import java.util.ArrayList;
004import java.util.List;
005import java.util.Locale;
006import javax.annotation.Nonnull;
007import jmri.BooleanPropertyDescriptor;
008import jmri.NamedBean;
009import jmri.NamedBeanPropertyDescriptor;
010import jmri.Turnout;
011import jmri.managers.AbstractTurnoutManager;
012import org.slf4j.Logger;
013import org.slf4j.LoggerFactory;
014
015/**
016 * Manage the LocoNet-specific Turnout implementation.
017 * System names are "LTnnn", where L is the user configurable system prefix,
018 * nnn is the turnout number without padding.
019 * <p>
020 * Some of the message formats used in this class are Copyright Digitrax, Inc.
021 * and used with permission as part of the JMRI project. That permission does
022 * not extend to uses in other software products. If you wish to use this code,
023 * algorithm or these message formats outside of JMRI, please contact Digitrax
024 * Inc for separate permission.
025 * <p>
026 * Since LocoNet messages requesting turnout operations can arrive faster than
027 * the command station can send them on the rails, the command station has a
028 * short queue of messages. When that gets full, it sends a LACK, indicating
029 * that the request was not forwarded on the rails. In that case, this class
030 * goes into a tight loop, resending the last turnout message seen until it's
031 * received without a LACK reply. Note two things about this:
032 * <ul>
033 *   <li>We provide this service for any turnout request, whether or not it came
034 *   from JMRI. (This might be a problem if more than one computer is executing
035 *   this algorithm)
036 *   <li>By sending the message as fast as we can, we tie up the LocoNet during
037 *   the recovery. This is a mixed bag; delaying can cause messages to get out
038 *   of sequence on the rails. But not delaying takes up a lot of LocoNet
039 *   bandwidth.
040 * </ul>
041 * In the end, this implementation is OK, but not great. An improvement would be
042 * to control JMRI turnout operations centrally, so that retransmissions can be
043 * controlled.
044 *
045 * @author Bob Jacobsen Copyright (C) 2001, 2007
046 */
047public class LnTurnoutManager extends AbstractTurnoutManager implements LocoNetListener {
048
049    // ctor has to register for LocoNet events
050    public LnTurnoutManager(LocoNetSystemConnectionMemo memo, LocoNetInterface throttledController, boolean mTurnoutNoRetry) {
051        super(memo);
052        this.fastcontroller = memo.getLnTrafficController();
053        this.throttledcontroller = throttledController;
054        this.mTurnoutNoRetry = mTurnoutNoRetry;
055
056        if (fastcontroller != null) {
057            fastcontroller.addLocoNetListener(~0, this);
058        } else {
059            log.error("No layout connection, turnout manager can't function");
060        }
061    }
062
063    LocoNetInterface fastcontroller;
064    LocoNetInterface throttledcontroller;
065    boolean mTurnoutNoRetry;
066
067    /**
068     * {@inheritDoc}
069     */
070    @Override
071    @Nonnull
072    public LocoNetSystemConnectionMemo getMemo() {
073        return (LocoNetSystemConnectionMemo) memo;
074    }
075
076    @Override
077    public void dispose() {
078        if (fastcontroller != null) {
079            fastcontroller.removeLocoNetListener(~0, this);
080        }
081        super.dispose();
082    }
083
084    protected boolean _binaryOutput = false;
085    protected boolean _useOffSwReqAsConfirmation = false;
086
087    public void setUhlenbrockMonitoring() {
088        _binaryOutput = true;
089        mTurnoutNoRetry = true;
090        _useOffSwReqAsConfirmation = true;
091    }
092
093    /**
094     * {@inheritDoc}
095     */
096    @Nonnull
097    @Override
098    protected Turnout createNewTurnout(@Nonnull String systemName, String userName) throws IllegalArgumentException {
099        String prefix = getSystemPrefix();
100        int addr;
101        try {
102            addr = Integer.parseInt(systemName.substring(prefix.length() + 1));
103        } catch (NumberFormatException e) {
104            throw new IllegalArgumentException("Can't convert " +  // NOI18N
105                    systemName.substring(prefix.length() + 1) +
106                    " to LocoNet turnout address"); // NOI18N
107        }
108        LnTurnout t = new LnTurnout(prefix, addr, throttledcontroller);
109        t.setUserName(userName);
110        if (_binaryOutput) t.setBinaryOutput(true);
111        if (_useOffSwReqAsConfirmation) {
112            t.setUseOffSwReqAsConfirmation(true);
113            t.setFeedbackMode("MONITORING"); // NOI18N
114        }
115        return t;
116    }
117
118    // holds last seen turnout request for possible resend
119    LocoNetMessage lastSWREQ = null;
120
121    /**
122     * Listen for turnouts, creating them as needed.
123     */
124    @Override
125    public void message(LocoNetMessage l) {
126        log.debug("LnTurnoutManager message {}", l);
127        String prefix = getSystemPrefix();
128        // parse message type
129        int addr;
130        switch (l.getOpCode()) {
131            case LnConstants.OPC_SW_REQ:
132            case LnConstants.OPC_SW_ACK: {               /* page 9 of LocoNet PE */
133
134                int sw1 = l.getElement(1);
135                int sw2 = l.getElement(2);
136                addr = address(sw1, sw2);
137
138                // store message in case resend is needed
139                lastSWREQ = new LocoNetMessage(l);
140
141                // LocoNet spec says 0x10 of SW2 must be 1, but we observe 0
142                if (((sw1 & 0xFC) == 0x78) && ((sw2 & 0xCF) == 0x07)) {
143                    return;  // turnout interrogate msg
144                }
145                log.debug("SW_REQ received with address {}", addr);
146                break;
147            }
148            case LnConstants.OPC_SW_REP: {                /* page 9 of LocoNet PE */
149
150                // clear resend message, indicating not to resend
151
152                lastSWREQ = null;
153
154                // process this request
155                int sw1 = l.getElement(1);
156                int sw2 = l.getElement(2);
157                addr = address(sw1, sw2);
158                log.debug("SW_REP received with address {}", addr);
159                break;
160            }
161            case LnConstants.OPC_LONG_ACK: {
162                // might have to resend, check 2nd byte
163                if (lastSWREQ != null && l.getElement(1) == 0x30 && l.getElement(2) == 0 && !mTurnoutNoRetry) {
164                    // received LONG_ACK reject msg, resend?
165                    // Skip if this is a status inquiry
166                    int sw1 = lastSWREQ.getElement(1);
167                    int sw2 = lastSWREQ.getElement(2);
168                    addr = address(sw1, sw2);
169
170                    if (addr < 1017 || addr > 1020) { // enquiries are above this
171                        fastcontroller.sendLocoNetMessage(lastSWREQ);
172                    }
173                }
174
175                // clear so can't resend recursively (we'll see
176                // the resent message echo'd back)
177                lastSWREQ = null;
178                return;
179            }
180            default:  // here we didn't find an interesting command
181                // clear resend message, indicating not to resend
182                lastSWREQ = null;
183                return;
184        }
185        // reach here for LocoNet switch command; make sure that a Turnout with this name exists
186        String s = prefix + "T" + addr; // NOI18N
187        LnTurnout lnT = (LnTurnout) getBySystemName(s);
188        if (lnT == null) {
189            // no turnout with this address, is there a light?
190            String sx = prefix + "L" + addr; // NOI18N
191            if (jmri.InstanceManager.lightManagerInstance().getBySystemName(sx) == null) {
192                // no light, create a turnout
193                LnTurnout t = (LnTurnout) provideTurnout(s);
194
195                // process the message to put the turnout in the right state
196                t.messageFromManager(l);
197            }
198        } else {
199            lnT.messageFromManager(l);
200        }
201    }
202
203    private int address(int a1, int a2) {
204        // the "+ 1" in the following converts to throttle-visible numbering
205        return (((a2 & 0x0f) * 128) + (a1 & 0x7f) + 1);
206    }
207
208    @Override
209    public boolean allowMultipleAdditions(@Nonnull String systemName) {
210        return true;
211    }
212
213    /**
214     * {@inheritDoc}
215     */
216    @Override
217    public NameValidity validSystemNameFormat(@Nonnull String systemName) {
218        return (getBitFromSystemName(systemName) != 0) ? NameValidity.VALID : NameValidity.INVALID;
219    }
220
221    /**
222     * {@inheritDoc}
223     */
224    @Override
225    @Nonnull
226    public String validateSystemNameFormat(@Nonnull String systemName, @Nonnull Locale locale) {
227        return validateIntegerSystemNameFormat(systemName, 1, 4096, locale);
228    }
229
230    /**
231     * Get the bit address from the system name.
232     * @param systemName a valid LocoNet-based Turnout System Name
233     * @return the turnout number extracted from the system name
234     */
235    public int getBitFromSystemName(String systemName) {
236        try {
237            validateSystemNameFormat(systemName, Locale.getDefault());
238        } catch (IllegalArgumentException ex) {
239            return 0;
240        }
241        return Integer.parseInt(systemName.substring(getSystemNamePrefix().length()));
242    }
243
244    /**
245     * {@inheritDoc}
246     */
247    @Override
248    public String getEntryToolTip() {
249        return Bundle.getMessage("AddOutputEntryToolTip");
250    }
251
252    public static final String BYPASSBUSHBYBITKEY = "Bypass Bushby Bit";
253    public static final String SENDONANDOFFKEY = "Send ON/OFF";
254
255    /**
256     * {@inheritDoc}
257     */
258    @Override
259    @Nonnull
260    public List<NamedBeanPropertyDescriptor<?>> getKnownBeanProperties() {
261        List<NamedBeanPropertyDescriptor<?>> l = new ArrayList<>();
262        l.add(new BooleanPropertyDescriptor(BYPASSBUSHBYBITKEY, false) {
263            @Override
264            public String getColumnHeaderText() {
265                return Bundle.getMessage("LnByPassBushbyHeader");
266            }
267
268            @Override
269            public boolean isEditable(NamedBean bean) {
270                return bean.getClass().getName().contains("LnTurnout");
271            }
272        });
273        l.add(new BooleanPropertyDescriptor(SENDONANDOFFKEY, !_binaryOutput) {
274            @Override
275            public String getColumnHeaderText() {
276                return Bundle.getMessage("SendOnOffHeader");
277            }
278
279            @Override
280            public boolean isEditable(NamedBean bean) {
281                return bean.getClass().getName().contains("LnTurnout");
282            }
283        });
284        return l;
285    }
286
287    private final static Logger log = LoggerFactory.getLogger(LnTurnoutManager.class);
288
289}