001package jmri.jmrix.lenz;
002
003import jmri.Consist;
004import jmri.ConsistListener;
005import jmri.DccLocoAddress;
006
007/**
008 * XNetConsist.java
009 *
010 * This is the Consist definition for a consist on an XPresNet system. it uses
011 * the XpressNet specific commands to build a consist.
012 *
013 * @author Paul Bender Copyright (C) 2004-2010
014 */
015public class XNetConsist extends jmri.implementation.DccConsist implements XNetListener {
016
017    // We need to wait for replies before completing consist
018    // operations
019    private static final int IDLESTATE = 0;
020    private static final int ADDREQUESTSENTSTATE = 1;
021    private static final int REMOVEREQUESTSENTSTATE = 2;
022    private static final String CONSIST_TYPE_NOT_SUPPORTED = "Consist Type Not Supported";
023
024    private int _state = IDLESTATE;
025
026    private DccLocoAddress _locoAddress = null; // address for the last request
027    private boolean _directionNormal = false; // direction of the last request
028
029    protected XNetTrafficController tc; // hold the traffic controller associated with this consist.
030
031    /**
032     * Initialize a consist for the specific address.
033     * Default consist type is an advanced consist.
034     * @param address loco address.
035     * @param controller system connection traffic controller.
036     * @param systemMemo system connection.
037     */
038    public XNetConsist(int address, XNetTrafficController controller, XNetSystemConnectionMemo systemMemo) {
039        super(address);
040        tc = controller;
041        this.systemMemo = systemMemo;
042        // At construction, register for messages
043        tc.addXNetListener(XNetInterface.COMMINFO
044                | XNetInterface.CONSIST,
045                XNetConsist.this);
046    }
047
048    /**
049     * Initialize a consist for the specific address.
050     * Default consist type is an advanced consist.
051     * @param address loco address.
052     * @param controller system connection traffic controller.
053     * @param systemMemo system connection.
054     */
055    public XNetConsist(DccLocoAddress address, XNetTrafficController controller, XNetSystemConnectionMemo systemMemo) {
056        super(address);
057        tc = controller;
058        this.systemMemo = systemMemo;
059        // At construction, register for messages
060        tc.addXNetListener(XNetInterface.COMMINFO
061                | XNetInterface.CONSIST,
062                XNetConsist.this);
063    }
064
065    final XNetSystemConnectionMemo systemMemo;
066
067    /**
068     * Clean Up local storage, and remove the XNetListener.
069     */
070    @Override
071    public synchronized void dispose() {
072        super.dispose();
073        tc.removeXNetListener(
074                XNetInterface.COMMINFO
075                | XNetInterface.CONSIST,
076                this);
077    }
078
079    /**
080     * Set the Consist Type.
081     *
082     * @param consistType An integer, should be either
083     *                     jmri.Consist.ADVANCED_CONSIST or
084     *                     jmri.Consist.CS_CONSIST.
085     */
086    @Override
087    public void setConsistType(int consistType) {
088        switch (consistType) {
089            case Consist.ADVANCED_CONSIST:
090            case Consist.CS_CONSIST:
091                this.consistType = consistType;
092                break;
093            default:
094                log.error(CONSIST_TYPE_NOT_SUPPORTED);
095                notifyConsistListeners(new DccLocoAddress(0, false), ConsistListener.NotImplemented);
096                break;
097        }
098    }
099
100    /**
101     * Is this address allowed?
102     * <p>
103     * On Lenz systems, All addresses but 0 can be used in a consist (Either and
104     * Advanced Consist or a Double Header).
105     * {@inheritDoc}
106     * @param address {@link jmri.DccLocoAddress DccLocoAddress} object to
107     *                check.
108     */
109    @Override
110    public boolean isAddressAllowed(DccLocoAddress address) {
111        return address.getNumber() != 0;
112    }
113
114    /**
115     * Is there a size limit for this consist?
116     *
117     * @return 2 For Lenz double headers. -1 (no limit) For Decoder Assisted
118     *         Consists. 0 for any other consist type.
119     */
120    @Override
121    public int sizeLimit() {
122        switch (consistType) {
123            case ADVANCED_CONSIST:
124                return -1;
125            case CS_CONSIST:
126                return 2;
127            default:
128                return 0;
129        }
130    }
131
132    /**
133     * Does the consist contain the specified address?
134     * {@inheritDoc}
135     * @param address {@link jmri.DccLocoAddress DccLocoAddress} object to
136     *                check.
137     */
138    @Override
139    public boolean contains(DccLocoAddress address) {
140        if (consistType == ADVANCED_CONSIST || consistType == CS_CONSIST) {
141            return (consistList.contains(address));
142        } else {
143            log.error(CONSIST_TYPE_NOT_SUPPORTED);
144            notifyConsistListeners(address, ConsistListener.NotImplemented);
145        }
146        return false;
147    }
148
149    /**
150     * Get the relative direction setting for a specific locomotive in the
151     * consist.
152     *
153     * @param address {@link jmri.DccLocoAddress DccLocoAddress} object to check
154     * @return true means forward, false means backwards.
155     */
156    @Override
157    public boolean getLocoDirection(DccLocoAddress address) {
158        if (consistType == ADVANCED_CONSIST || consistType == CS_CONSIST) {
159            return consistDir.get(address);
160        } else {
161            log.error(CONSIST_TYPE_NOT_SUPPORTED);
162            notifyConsistListeners(address, ConsistListener.NotImplemented);
163        }
164        return false;
165    }
166
167    /**
168     * Add an Address to the internal consist list object.
169     *
170     * @param locoAddress     {@link jmri.DccLocoAddress address} of the
171     *                        locomotive to add.
172     * @param directionNormal true for normal direction, false for reverse.
173     */
174    private synchronized void addToConsistList(DccLocoAddress locoAddress, boolean directionNormal) {
175        if (!(consistList.contains(locoAddress))) {
176            consistList.add(locoAddress);
177        }
178        consistDir.put(locoAddress, directionNormal);
179        if (consistType == CS_CONSIST && consistList.size() == 2) {
180            notifyConsistListeners(locoAddress,
181                    ConsistListener.OPERATION_SUCCESS
182                    | ConsistListener.CONSIST_FULL);
183        } else {
184            notifyConsistListeners(locoAddress,
185                    ConsistListener.OPERATION_SUCCESS);
186        }
187    }
188
189    /**
190     * Remove an address from the internal consist list object.
191     *
192     * @param locoAddress {@link jmri.DccLocoAddress address} of the locomotive
193     *                    to remove.
194     */
195    private synchronized void removeFromConsistList(DccLocoAddress locoAddress) {
196        if (consistList.contains(locoAddress)) {
197            consistDir.remove(locoAddress);
198            consistList.remove(locoAddress);
199        }
200        notifyConsistListeners(locoAddress, ConsistListener.OPERATION_SUCCESS);
201    }
202
203    /**
204     * Add a Locomotive to a Consist.
205     *
206     * @param locoAddress     the Locomotive address to add to the locomotive
207     * @param directionNormal is True if the locomotive is traveling the same
208     *                        direction as the consist, or false otherwise
209     */
210    @Override
211    public synchronized void add(DccLocoAddress locoAddress, boolean directionNormal) {
212        switch (consistType) {
213            case ADVANCED_CONSIST:
214                addToAdvancedConsist(locoAddress, directionNormal);
215                // save the address for the check after we get a response
216                // from the command station
217                _locoAddress = locoAddress;
218                _directionNormal = directionNormal;
219                break;
220            case CS_CONSIST:
221                if (consistList.size() < 2) {
222                    // Lenz Double Headers require exactly 2 locomotives, so
223                    // wait for the second locomotive to be added to start
224                    if (consistList.size() == 1 && !consistList.contains(locoAddress)) {
225                        addToCSConsist(locoAddress, directionNormal);
226                        // save the address for the check after we get a response
227                        // from the command station
228                        _locoAddress = locoAddress;
229                        _directionNormal = directionNormal;
230                    } else if (consistList.isEmpty()) {
231                        // we're going to just add this directly, since we
232                        // can't form the consist yet.
233                        addToConsistList(locoAddress, directionNormal);
234                    } else {
235                        // we must have gotten here because we tried to add
236                        // a locomotive already in this consist.
237                        notifyConsistListeners(locoAddress,
238                                ConsistListener.CONSIST_ERROR
239                                | ConsistListener.ALREADY_CONSISTED);
240                    }
241                } else {
242                    // The only way it is valid for us to do something
243                    // here is if the locomotive we're adding is
244                    // already in the consist and we want to change
245                    // its direction
246                    if (consistList.size() == 2
247                            && consistList.contains(locoAddress)) {
248                        addToCSConsist(locoAddress, directionNormal);
249                        // save the address for the check after we get aresponse
250                        // from the command station
251                        _locoAddress = locoAddress;
252                        _directionNormal = directionNormal;
253                    } else {
254                        notifyConsistListeners(locoAddress,
255                                ConsistListener.CONSIST_ERROR
256                                | ConsistListener.CONSIST_FULL);
257                    }
258                }
259                break;
260            default:
261                log.error(CONSIST_TYPE_NOT_SUPPORTED);
262                notifyConsistListeners(locoAddress, ConsistListener.NotImplemented);
263                break;
264        }
265    }
266
267    /**
268     * Restore a Locomotive to an Advanced Consist, but don't write to the
269     * command station.
270     * <p>
271     * This is used for restoring the consist from a file or adding a consist
272     * read from the command station.
273     *
274     * @param locoAddress     the Locomotive address to add to the locomotive
275     * @param directionNormal True if the locomotive is traveling the same
276     *                        direction as the Consist, or false otherwise.
277     */
278    @Override
279    public synchronized void restore(DccLocoAddress locoAddress, boolean directionNormal) {
280        switch (consistType) {
281            case ADVANCED_CONSIST:
282            case CS_CONSIST:
283                addToConsistList(locoAddress, directionNormal);
284                break;
285            default:
286                log.error(CONSIST_TYPE_NOT_SUPPORTED);
287                notifyConsistListeners(locoAddress, ConsistListener.NotImplemented);
288                break;
289        }
290    }
291
292    /**
293     * Remove a Locomotive from this Consist.
294     *
295     * @param locoAddress the Locomotive address to add to the Consist
296     */
297    @Override
298    public synchronized void remove(DccLocoAddress locoAddress) {
299        log.debug("Consist {}: remove called for address {}", consistAddress, locoAddress);
300        switch (consistType) {
301            case ADVANCED_CONSIST:
302                // save the address for the check after we get a response
303                // from the command station
304                _locoAddress = locoAddress;
305                removeFromAdvancedConsist(locoAddress);
306                break;
307            case CS_CONSIST:
308                // Lenz Double Headers must be formed with EXACTLY 2
309                // addresses, so if there are two addresses in the list,
310                // we'll actually send the commands to remove the consist
311                if (consistList.size() == 2
312                        && _state != REMOVEREQUESTSENTSTATE) {
313                    // save the address for the check after we get a response
314                    // from the command station
315                    _locoAddress = locoAddress;
316                    removeFromCSConsist(locoAddress);
317                } else {
318                    // we just want to remove this from the list.
319                    if (_state != REMOVEREQUESTSENTSTATE
320                            || _locoAddress != locoAddress) {
321                        removeFromConsistList(locoAddress);
322                    }
323                }
324                break;
325            default:
326                log.error(CONSIST_TYPE_NOT_SUPPORTED);
327                notifyConsistListeners(locoAddress, ConsistListener.NotImplemented);
328                break;
329        }
330    }
331
332    /**
333     * Add a Locomotive to an Advanced Consist.
334     *
335     * @param locoAddress     the Locomotive address to add to the locomotive
336     * @param directionNormal is True if the locomotive is traveling the same
337     *                        direction as the consist, or false otherwise.
338     */
339    @Override
340    protected synchronized void addToAdvancedConsist(DccLocoAddress locoAddress, boolean directionNormal) {
341        log.debug("Adding locomotive {} to consist {}", locoAddress.getNumber(), consistAddress.getNumber());
342        // First, check to see if the locomotive is in the consist already
343        if (this.contains(locoAddress)) {
344            // we want to remove the locomotive from the consist
345            // before we re-add it. (we might just be switching
346            // the direction of the locomotive in the consist)
347            removeFromAdvancedConsist(locoAddress);
348        }
349        // set the speed of the locomotive to zero, to make sure we have
350        // control over it.
351        sendDirection(locoAddress, directionNormal);
352
353        // All we have to do here is create an apropriate XNetMessage,
354        // and send it.
355        XNetMessage msg = XNetMessage.getAddLocoToConsistMsg(consistAddress.getNumber(), 
356            locoAddress.getNumber(), directionNormal);
357        tc.sendXNetMessage(msg, this);
358        _state = ADDREQUESTSENTSTATE;
359    }
360
361    /**
362     * Remove a Locomotive from an Advanced Consist.
363     *
364     * @param locoAddress the Locomotive address to add to the locomotive
365     */
366    @Override
367    protected synchronized void removeFromAdvancedConsist(DccLocoAddress locoAddress) {
368        // set the speed of the locomotive to zero, to make sure we
369        // have control over it.
370        sendDirection(locoAddress, getLocoDirection(locoAddress));
371        // All we have to do here is create an apropriate XNetMessage,
372        // and send it.
373        XNetMessage msg = XNetMessage.getRemoveLocoFromConsistMsg(consistAddress.getNumber(), locoAddress.getNumber());
374        tc.sendXNetMessage(msg, this);
375        _state = REMOVEREQUESTSENTSTATE;
376    }
377
378    /**
379     * Add a Locomotive to a Lenz Double Header
380     *
381     * @param locoAddress     the Locomotive address to add to the locomotive
382     * @param directionNormal is True if the locomotive is traveling the same
383     *                        direction as the consist, or false otherwise.
384     */
385    private synchronized void addToCSConsist(DccLocoAddress locoAddress, boolean directionNormal) {
386
387        if (consistAddress.equals(locoAddress)) {
388            // Something went wrong here, we are trying to add a
389            // trailing locomotive to the consist with the same
390            // address as the lead locomotive.  This isn't supposed to
391            // happen.
392            log.error("Attempted to add {} to consist {}", locoAddress, consistAddress);
393            _state = IDLESTATE;
394            notifyConsistListeners(_locoAddress,
395                    ConsistListener.CONSIST_ERROR
396                    | ConsistListener.ALREADY_CONSISTED);
397            return;
398        }
399
400        // If the consist already contains the locomotive in
401        // question, we need to disolve the consist
402        if (consistList.size() == 2
403                && consistList.contains(locoAddress)) {
404            XNetMessage msg = XNetMessage.getDisolveDoubleHeaderMsg(
405                    consistList.get(0).getNumber());
406            tc.sendXNetMessage(msg, this);
407        }
408
409        // We need to set the speed and direction of both
410        // locomotives to establish control.
411        DccLocoAddress address = consistList.get(0);
412        Boolean direction = consistDir.get(address);
413        sendDirection(address, direction);
414        sendDirection(locoAddress, directionNormal);
415
416        // All we have to do here is create an apropriate XNetMessage,
417        // and send it.
418        XNetMessage msg = XNetMessage.getBuildDoubleHeaderMsg(address.getNumber(), locoAddress.getNumber());
419        tc.sendXNetMessage(msg, this);
420        _state = ADDREQUESTSENTSTATE;
421
422    }
423
424    /**
425     * Remove a Locomotive from a Lenz Double Header.locoAddress
426     * @param locoAddress is the Locomotive address, unused here.
427     */
428    public synchronized void removeFromCSConsist(DccLocoAddress locoAddress) {
429        // All we have to do here is create an apropriate XNetMessage,
430        // and send it.
431        XNetMessage msg = XNetMessage.getDisolveDoubleHeaderMsg(consistList.get(0).getNumber());
432        tc.sendXNetMessage(msg, this);
433        _state = REMOVEREQUESTSENTSTATE;
434    }
435
436    /**
437     * Listeners for messages from the command station.
438     */
439    @Override
440    public synchronized void message(XNetReply l) {
441        if (_state != IDLESTATE) {
442            // we're waiting for a reply, so examine what we received
443            if (l.isOkMessage()) {
444                if (_state == ADDREQUESTSENTSTATE) {
445                    addToConsistList(_locoAddress, _directionNormal);
446                    if (consistType == ADVANCED_CONSIST) {
447                       //set the value in the roster entry for CV19
448                       setRosterEntryCVValue(_locoAddress);
449                    }
450                } else if (_state == REMOVEREQUESTSENTSTATE) {
451                    if (consistType == ADVANCED_CONSIST) {
452                       //reset the value in the roster entry for CV19
453                       resetRosterEntryCVValue(_locoAddress);
454                    }
455                    removeFromConsistList(_locoAddress);
456                }
457                _state = IDLESTATE;
458            } else if (l.getElement(0) == XNetConstants.LOCO_MU_DH_ERROR) {
459                String text;
460                switch (l.getElement(1)) {
461                    case 0x81:
462                        text = "Selected Locomotive has not been operated by this XpressNet device or address 0 selected";
463                        _state = IDLESTATE;
464                        notifyConsistListeners(_locoAddress,
465                                ConsistListener.CONSIST_ERROR
466                                | ConsistListener.LOCO_NOT_OPERATED);
467                        break;
468                    case 0x82:
469                        text = "Selected Locomotive is being operated by another XpressNet device";
470                        _state = IDLESTATE;
471                        notifyConsistListeners(_locoAddress,
472                                ConsistListener.CONSIST_ERROR
473                                | ConsistListener.LOCO_NOT_OPERATED);
474                        break;
475                    case 0x83:
476                        text = "Selected Locomotive already in MU or DH";
477                        _state = IDLESTATE;
478                        notifyConsistListeners(_locoAddress,
479                                ConsistListener.CONSIST_ERROR
480                                | ConsistListener.ALREADY_CONSISTED);
481                        break;
482                    case 0x84:
483                        text = "Unit selected for MU or DH has speed setting other than 0";
484                        _state = IDLESTATE;
485                        notifyConsistListeners(_locoAddress,
486                                ConsistListener.CONSIST_ERROR
487                                | ConsistListener.NONZERO_SPEED);
488                        break;
489                    case 0x85:
490                        text = "Locomotive not in a MU";
491                        _state = IDLESTATE;
492                        notifyConsistListeners(_locoAddress,
493                                ConsistListener.CONSIST_ERROR
494                                | ConsistListener.NOT_CONSISTED);
495                        break;
496                    case 0x86:
497                        text = "Locomotive address not a multi-unit base address";
498                        _state = IDLESTATE;
499                        notifyConsistListeners(_locoAddress,
500                                ConsistListener.CONSIST_ERROR
501                                | ConsistListener.NOT_CONSIST_ADDR);
502
503                        break;
504                    case 0x87:
505                        text = "It is not possible to delete the locomotive";
506                        _state = IDLESTATE;
507                        notifyConsistListeners(_locoAddress,
508                                ConsistListener.CONSIST_ERROR
509                                | ConsistListener.DELETE_ERROR);
510                        break;
511                    case 0x88:
512                        text = "The Command Station Stack is Full";
513                        _state = IDLESTATE;
514                        notifyConsistListeners(_locoAddress,
515                                ConsistListener.CONSIST_ERROR
516                                | ConsistListener.STACK_FULL);
517                        break;
518                    default:
519                        text = "Unknown";
520                        _state = IDLESTATE;
521                        notifyConsistListeners(_locoAddress,
522                                ConsistListener.CONSIST_ERROR);
523                }
524                log.error("XpressNet MU+DH error: {}",text);
525            }
526        }
527    }
528
529    @Override
530    public void message(XNetMessage l) {
531        // we don't care about outgoing messages
532    }
533
534   // Handle a timeout notification
535    @Override
536    public void notifyTimeout(XNetMessage msg) {
537        log.debug("Notified of timeout on message{}", msg);
538    }
539
540    /**
541     * Set the speed and direction of a locomotive; bypassing the commands in
542     * the throttle, since they don't work for this application.
543     * <p>
544     * For this application, we also set the speed setting to 0, which also
545     * establishes control over the locomotive in the consist.
546     *
547     * @param address   the DccLocoAddress of the locomotive.
548     * @param isForward the boolean value representing the desired direction
549     */
550    private void sendDirection(DccLocoAddress address, boolean isForward) {
551        XNetMessage msg = XNetMessage.getSpeedAndDirectionMsg(address.getNumber(),
552                jmri.SpeedStepMode.NMRA_DCC_28,
553                (float) 0.0,
554                isForward);
555        // now, we send the message to the command station
556        tc.sendXNetMessage(msg, this);
557    }
558
559    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(XNetConsist.class);
560
561}