001package jmri.jmrix.dccpp;
002
003import static jmri.jmrix.dccpp.DCCppConstants.MAX_TURNOUT_ADDRESS;
004
005import java.util.ArrayDeque;
006import java.util.Locale;
007import java.util.Queue;
008
009import javax.annotation.Nonnull;
010import jmri.Turnout;
011import org.slf4j.Logger;
012import org.slf4j.LoggerFactory;
013
014/**
015 * Implement TurnoutManager for DCC-EX systems.
016 * <p>
017 * System names are "DxppTnnn", where Dx is the system prefix and nnn is the turnout number without padding.
018 *
019 * @author Bob Jacobsen Copyright (C) 2001
020 * @author Paul Bender Copyright (C) 2003-2010
021 * @author Mark Underwood Copyright (C) 2015
022 */
023public class DCCppTurnoutManager extends jmri.managers.AbstractTurnoutManager implements DCCppListener {
024
025    protected DCCppTrafficController tc = null;
026    private final Queue<Integer> _pendingTurnoutIds = new ArrayDeque<>();
027
028    /**
029     * Create a new DCC-EX TurnoutManager.
030     * Has to register for DCC-EX events.
031     *
032     * @param memo the supporting system connection memo
033     */
034    public DCCppTurnoutManager(DCCppSystemConnectionMemo memo) {
035        super(memo);
036        tc = memo.getDCCppTrafficController();
037        // set up listener
038        tc.addDCCppListener(DCCppInterface.FEEDBACK, this);
039        // request list of turnouts
040        tc.sendDCCppMessage(DCCppMessage.makeTurnoutListMsg(), this);
041        // request list of outputs
042        tc.sendDCCppMessage(DCCppMessage.makeOutputListMsg(), this);
043        // request list of Turnout IDs
044        tc.sendDCCppMessage(DCCppMessage.makeTurnoutIDsMsg(), this);
045    }
046
047    /**
048     * {@inheritDoc}
049     */
050    @Override
051    @Nonnull
052    public DCCppSystemConnectionMemo getMemo() {
053        return (DCCppSystemConnectionMemo) memo;
054    }
055
056    // DCCpp-specific methods
057
058    /**
059     * {@inheritDoc}
060     */
061    @Nonnull
062    @Override
063    protected Turnout createNewTurnout(@Nonnull String systemName, String userName) throws IllegalArgumentException {
064        // check if the output bit is available
065        int bitNum = getBitFromSystemName(systemName);
066        if (bitNum < 0) {
067            throw new IllegalArgumentException("Cannot get Bit from System Name " + systemName);
068        }
069        // make the new Turnout object
070        Turnout t = new DCCppTurnout(getSystemPrefix(), bitNum, tc);
071        t.setUserName(userName);
072        return t;
073    }
074
075    /**
076     * {@inheritDoc}
077     * Listen for turnouts, creating them as needed.
078     */
079    @Override
080    public void message(DCCppReply l) {
081        if (l.isTurnoutReply()) {
082            log.debug("received Turnout Reply message: '{}'", l);
083            // parse message type
084            int addr = l.getTOIDInt();
085            if (addr >= 0) {
086                // check to see if the address has been operated before
087                // continuing.
088                log.debug("message has address: {}", addr);
089                // reach here for switch command; make sure we know 
090                // about this one
091                String s = getSystemNamePrefix() + addr;
092                DCCppTurnout found = (DCCppTurnout) getBySystemName(s);
093                if ( found == null) {
094                    // need to create a new one, set some attributes, 
095                    //  and send the message on to the newly created object.
096                    DCCppTurnout t = (DCCppTurnout) provideTurnout(s);
097                    t.setFeedbackMode(Turnout.MONITORING);
098                    t.initmessage(l);
099                } else {
100                    // The turnout exists, forward this message to the 
101                    // turnout
102                    found.message(l);
103                }
104            }
105        } else if (l.isOutputReply()) {
106            log.debug("received Output Reply message: '{}'", l);
107            // parse message type
108            int addr = l.getOutputNumInt();
109            if (addr >= 0) {
110                // check to see if the address has been operated before
111                // continuing.
112                log.debug("message has address: {}", addr);
113                // reach here for switch command; make sure we know 
114                // about this one
115                String s = getSystemNamePrefix() + addr;
116                DCCppTurnout found = (DCCppTurnout) getBySystemName(s);
117                if (found == null) {
118                    // need to create a new one, and send the message on 
119                    // to the newly created object.
120                    DCCppTurnout t = (DCCppTurnout) provideTurnout(s);
121                    t.setFeedbackMode(Turnout.EXACT);
122                    t.initmessage(l);
123                } else {
124                    // The turnout exists, forward this message to the 
125                    // turnout
126                    found.message(l);
127                }
128            }
129        } else if (l.isTurnoutIDsReply()) {
130            log.debug("received Turnout ID List message: '{}'", l);
131            for (Integer id : l.getTurnoutIDList()) {
132                if (!_pendingTurnoutIds.contains(id)) {
133                    _pendingTurnoutIds.add(id);
134                }
135            }
136            requestNextTurnoutDetail();
137        } else if (l.isTurnoutIDReply()) {
138            log.debug("received Turnout ID Detail message: '{}'", l);
139            // parse message type
140            int addr = l.getTOIDInt();
141            if (addr >= 0) {
142                log.debug("message has address: {}", addr);
143                String s = getSystemNamePrefix() + addr;
144                DCCppTurnout found = (DCCppTurnout) getBySystemName(s);
145                if ( found == null) { //create new turnout
146                    DCCppTurnout t = (DCCppTurnout) provideTurnout(s);
147                    t.setFeedbackMode(Turnout.MONITORING);
148                    if (!l.getTurnoutDescString().isEmpty()) {
149                        t.setUserName(l.getTurnoutDescString()); //add username if available
150                    }
151                    t.initmessage(l); //forward message to turnout
152                } else { // The turnout already exists                    
153                    if (!l.getTurnoutDescString().isEmpty() && found.getUserName()==null) {
154                        found.setUserName(l.getTurnoutDescString()); //set userName if needed and available
155                    }
156                    found.message(l); //forward message to turnout
157                }
158            }
159            requestNextTurnoutDetail();
160        }
161    }
162
163    private void requestNextTurnoutDetail() {
164        Integer id = _pendingTurnoutIds.poll();
165        if (id != null) {
166            tc.sendDCCppMessage(DCCppMessage.makeTurnoutIDMsg(id), this);
167        }
168    }
169
170    /**
171     * Get text to be used for the Turnout.CLOSED state in user communication.
172     * Allows text other than "CLOSED" to be use with certain hardware system to
173     * represent the Turnout.CLOSED state.
174     */
175    @Override
176    @Nonnull
177    public String getClosedText() {
178        return Bundle.getMessage("TurnoutStateClosed");
179    }
180
181    /**
182     * Get text to be used for the Turnout.THROWN state in user communication.
183     * Allows text other than "THROWN" to be use with certain hardware system to
184     * represent the Turnout.THROWN state.
185     */
186    @Override
187    @Nonnull
188    public String getThrownText() {
189        return Bundle.getMessage("TurnoutStateThrown");
190    }
191
192    /**
193     * Listen for the outgoing messages (to the command station)
194     */
195    @Override
196    public void message(DCCppMessage l) {
197    }
198
199    // Handle message timeout notification
200    // If the message still has retries available, reduce retries and send it back to the traffic controller.
201    @Override
202    public void notifyTimeout(DCCppMessage msg) {
203        log.debug("Notified of timeout on message '{}' , {} retries available.", msg, msg.getRetries());
204        if (msg.getRetries() > 0) {
205            msg.setRetries(msg.getRetries() - 1);
206            tc.sendDCCppMessage(msg, this);
207        }        
208    }
209
210    /** {@inheritDoc} */
211    @Override
212    public boolean allowMultipleAdditions(@Nonnull String systemName) {
213        return true;
214    }
215
216    /**
217     * {@inheritDoc}
218     */
219    @Override
220    public NameValidity validSystemNameFormat(@Nonnull String systemName) {
221        return (getBitFromSystemName(systemName) != -1) ? NameValidity.VALID : NameValidity.INVALID;
222    }
223
224    /**
225     * {@inheritDoc}
226     */
227    @Override
228    @Nonnull
229    public String validateSystemNameFormat(@Nonnull String systemName, @Nonnull Locale locale) {
230        return validateIntegerSystemNameFormat(systemName, 0, MAX_TURNOUT_ADDRESS, locale);
231    }
232
233    /**
234     * Get the bit address from the system name.
235     *
236     * @param systemName a valid Turnout System Name
237     * @return the turnout number extracted from the system name
238     */
239    public int getBitFromSystemName(String systemName) {
240        try {
241            validateSystemNameFormat(systemName, Locale.getDefault());
242        } catch (IllegalArgumentException ex) {
243            return -1;
244        }
245        return Integer.parseInt(systemName.substring(getSystemNamePrefix().length()));
246    }
247
248    /** {@inheritDoc} */
249    @Override
250    public String getEntryToolTip() {
251        return Bundle.getMessage("AddOutputEntryToolTip");
252    }
253
254    private static final Logger log = LoggerFactory.getLogger(DCCppTurnoutManager.class);
255
256}