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            // consistDir.get(address) returns a Boolean, auto-unboxed to
160            // primitive boolean here.
161            return consistDir.getOrDefault(address, false);
162        } else {
163            log.error(CONSIST_TYPE_NOT_SUPPORTED);
164            notifyConsistListeners(address, ConsistListener.NotImplemented);
165        }
166        return false;
167    }
168
169    /**
170     * Add an Address to the internal consist list object.
171     *
172     * @param locoAddress     {@link jmri.DccLocoAddress address} of the
173     *                        locomotive to add.
174     * @param directionNormal true for normal direction, false for reverse.
175     */
176    private synchronized void addToConsistList(DccLocoAddress locoAddress, boolean directionNormal) {
177        if (!(consistList.contains(locoAddress))) {
178            consistList.add(locoAddress);
179        }
180        consistDir.put(locoAddress, directionNormal);
181        if (consistType == CS_CONSIST && consistList.size() == 2) {
182            notifyConsistListeners(locoAddress,
183                    ConsistListener.OPERATION_SUCCESS
184                    | ConsistListener.CONSIST_FULL);
185        } else {
186            notifyConsistListeners(locoAddress,
187                    ConsistListener.OPERATION_SUCCESS);
188        }
189    }
190
191    /**
192     * Remove an address from the internal consist list object.
193     *
194     * @param locoAddress {@link jmri.DccLocoAddress address} of the locomotive
195     *                    to remove.
196     */
197    private synchronized void removeFromConsistList(DccLocoAddress locoAddress) {
198        if (consistList.contains(locoAddress)) {
199            consistDir.remove(locoAddress);
200            consistList.remove(locoAddress);
201        }
202        notifyConsistListeners(locoAddress, ConsistListener.OPERATION_SUCCESS);
203    }
204
205    /**
206     * Add a Locomotive to a Consist.
207     *
208     * @param locoAddress     the Locomotive address to add to the locomotive
209     * @param directionNormal is True if the locomotive is traveling the same
210     *                        direction as the consist, or false otherwise
211     */
212    @Override
213    public synchronized void add(DccLocoAddress locoAddress, boolean directionNormal) {
214        switch (consistType) {
215            case ADVANCED_CONSIST:
216                addToAdvancedConsist(locoAddress, directionNormal);
217                // save the address for the check after we get a response
218                // from the command station
219                _locoAddress = locoAddress;
220                _directionNormal = directionNormal;
221                break;
222            case CS_CONSIST:
223                if (consistList.size() < 2) {
224                    // Lenz Double Headers require exactly 2 locomotives, so
225                    // wait for the second locomotive to be added to start
226                    if (consistList.size() == 1 && !consistList.contains(locoAddress)) {
227                        addToCSConsist(locoAddress, directionNormal);
228                        // save the address for the check after we get a response
229                        // from the command station
230                        _locoAddress = locoAddress;
231                        _directionNormal = directionNormal;
232                    } else if (consistList.isEmpty()) {
233                        // we're going to just add this directly, since we
234                        // can't form the consist yet.
235                        addToConsistList(locoAddress, directionNormal);
236                    } else {
237                        // we must have gotten here because we tried to add
238                        // a locomotive already in this consist.
239                        notifyConsistListeners(locoAddress,
240                                ConsistListener.CONSIST_ERROR
241                                | ConsistListener.ALREADY_CONSISTED);
242                    }
243                } else {
244                    // The only way it is valid for us to do something
245                    // here is if the locomotive we're adding is
246                    // already in the consist and we want to change
247                    // its direction
248                    if (consistList.size() == 2
249                            && consistList.contains(locoAddress)) {
250                        addToCSConsist(locoAddress, directionNormal);
251                        // save the address for the check after we get aresponse
252                        // from the command station
253                        _locoAddress = locoAddress;
254                        _directionNormal = directionNormal;
255                    } else {
256                        notifyConsistListeners(locoAddress,
257                                ConsistListener.CONSIST_ERROR
258                                | ConsistListener.CONSIST_FULL);
259                    }
260                }
261                break;
262            default:
263                log.error(CONSIST_TYPE_NOT_SUPPORTED);
264                notifyConsistListeners(locoAddress, ConsistListener.NotImplemented);
265                break;
266        }
267    }
268
269    /**
270     * Restore a Locomotive to an Advanced Consist, but don't write to the
271     * command station.
272     * <p>
273     * This is used for restoring the consist from a file or adding a consist
274     * read from the command station.
275     *
276     * @param locoAddress     the Locomotive address to add to the locomotive
277     * @param directionNormal True if the locomotive is traveling the same
278     *                        direction as the Consist, or false otherwise.
279     */
280    @Override
281    public synchronized void restore(DccLocoAddress locoAddress, boolean directionNormal) {
282        switch (consistType) {
283            case ADVANCED_CONSIST:
284            case CS_CONSIST:
285                addToConsistList(locoAddress, directionNormal);
286                break;
287            default:
288                log.error(CONSIST_TYPE_NOT_SUPPORTED);
289                notifyConsistListeners(locoAddress, ConsistListener.NotImplemented);
290                break;
291        }
292    }
293
294    /**
295     * Remove a Locomotive from this Consist.
296     *
297     * @param locoAddress the Locomotive address to add to the Consist
298     */
299    @Override
300    public synchronized void remove(DccLocoAddress locoAddress) {
301        log.debug("Consist {}: remove called for address {}", consistAddress, locoAddress);
302        switch (consistType) {
303            case ADVANCED_CONSIST:
304                // save the address for the check after we get a response
305                // from the command station
306                _locoAddress = locoAddress;
307                removeFromAdvancedConsist(locoAddress);
308                break;
309            case CS_CONSIST:
310                // Lenz Double Headers must be formed with EXACTLY 2
311                // addresses, so if there are two addresses in the list,
312                // we'll actually send the commands to remove the consist
313                if (consistList.size() == 2
314                        && _state != REMOVEREQUESTSENTSTATE) {
315                    // save the address for the check after we get a response
316                    // from the command station
317                    _locoAddress = locoAddress;
318                    removeFromCSConsist(locoAddress);
319                } else {
320                    // we just want to remove this from the list.
321                    if (_state != REMOVEREQUESTSENTSTATE
322                            || _locoAddress != locoAddress) {
323                        removeFromConsistList(locoAddress);
324                    }
325                }
326                break;
327            default:
328                log.error(CONSIST_TYPE_NOT_SUPPORTED);
329                notifyConsistListeners(locoAddress, ConsistListener.NotImplemented);
330                break;
331        }
332    }
333
334    /**
335     * Add a Locomotive to an Advanced Consist.
336     *
337     * @param locoAddress     the Locomotive address to add to the locomotive
338     * @param directionNormal is True if the locomotive is traveling the same
339     *                        direction as the consist, or false otherwise.
340     */
341    @Override
342    protected synchronized void addToAdvancedConsist(DccLocoAddress locoAddress, boolean directionNormal) {
343        log.debug("Adding locomotive {} to consist {}", locoAddress.getNumber(), consistAddress.getNumber());
344        // First, check to see if the locomotive is in the consist already
345        if (this.contains(locoAddress)) {
346            // we want to remove the locomotive from the consist
347            // before we re-add it. (we might just be switching
348            // the direction of the locomotive in the consist)
349            removeFromAdvancedConsist(locoAddress);
350        }
351        // set the speed of the locomotive to zero, to make sure we have
352        // control over it.
353        sendDirection(locoAddress, directionNormal);
354
355        // All we have to do here is create an apropriate XNetMessage,
356        // and send it.
357        XNetMessage msg = XNetMessage.getAddLocoToConsistMsg(consistAddress.getNumber(), 
358            locoAddress.getNumber(), directionNormal);
359        tc.sendXNetMessage(msg, this);
360        _state = ADDREQUESTSENTSTATE;
361    }
362
363    /**
364     * Remove a Locomotive from an Advanced Consist.
365     *
366     * @param locoAddress the Locomotive address to add to the locomotive
367     */
368    @Override
369    protected synchronized void removeFromAdvancedConsist(DccLocoAddress locoAddress) {
370        // set the speed of the locomotive to zero, to make sure we
371        // have control over it.
372        sendDirection(locoAddress, getLocoDirection(locoAddress));
373        // All we have to do here is create an apropriate XNetMessage,
374        // and send it.
375        XNetMessage msg = XNetMessage.getRemoveLocoFromConsistMsg(consistAddress.getNumber(), locoAddress.getNumber());
376        tc.sendXNetMessage(msg, this);
377        _state = REMOVEREQUESTSENTSTATE;
378    }
379
380    /**
381     * Add a Locomotive to a Lenz Double Header
382     *
383     * @param locoAddress     the Locomotive address to add to the locomotive
384     * @param directionNormal is True if the locomotive is traveling the same
385     *                        direction as the consist, or false otherwise.
386     */
387    private synchronized void addToCSConsist(DccLocoAddress locoAddress, boolean directionNormal) {
388
389        if (consistAddress.equals(locoAddress)) {
390            // Something went wrong here, we are trying to add a
391            // trailing locomotive to the consist with the same
392            // address as the lead locomotive.  This isn't supposed to
393            // happen.
394            log.error("Attempted to add {} to consist {}", locoAddress, consistAddress);
395            _state = IDLESTATE;
396            notifyConsistListeners(_locoAddress,
397                    ConsistListener.CONSIST_ERROR
398                    | ConsistListener.ALREADY_CONSISTED);
399            return;
400        }
401
402        // If the consist already contains the locomotive in
403        // question, we need to disolve the consist
404        if (consistList.size() == 2
405                && consistList.contains(locoAddress)) {
406            XNetMessage msg = XNetMessage.getDisolveDoubleHeaderMsg(
407                    consistList.get(0).getNumber());
408            tc.sendXNetMessage(msg, this);
409        }
410
411        // We need to set the speed and direction of both
412        // locomotives to establish control.
413        DccLocoAddress address = consistList.get(0);
414        Boolean direction = consistDir.get(address);
415        sendDirection(address, direction);
416        sendDirection(locoAddress, directionNormal);
417
418        // All we have to do here is create an apropriate XNetMessage,
419        // and send it.
420        XNetMessage msg = XNetMessage.getBuildDoubleHeaderMsg(address.getNumber(), locoAddress.getNumber());
421        tc.sendXNetMessage(msg, this);
422        _state = ADDREQUESTSENTSTATE;
423
424    }
425
426    /**
427     * Remove a Locomotive from a Lenz Double Header.locoAddress
428     * @param locoAddress is the Locomotive address, unused here.
429     */
430    public synchronized void removeFromCSConsist(DccLocoAddress locoAddress) {
431        // All we have to do here is create an apropriate XNetMessage,
432        // and send it.
433        XNetMessage msg = XNetMessage.getDisolveDoubleHeaderMsg(consistList.get(0).getNumber());
434        tc.sendXNetMessage(msg, this);
435        _state = REMOVEREQUESTSENTSTATE;
436    }
437
438    /**
439     * Listeners for messages from the command station.
440     */
441    @Override
442    public synchronized void message(XNetReply l) {
443        // remove() for ADVANCED_CONSIST is fire-and-forget: it sends the
444        // XNet message and returns immediately, with the actual list/state
445        // update happening later in this callback. dispose() does not wait
446        // for those replies before nulling out consistList, so a reply for
447        // an already-disposed consist must be ignored here rather than
448        // acting on torn-down state.
449        if (consistList == null) {
450            log.debug("Ignoring reply for consist {} -- already disposed", consistAddress);
451            return;
452        }
453        if (_state != IDLESTATE) {
454            // we're waiting for a reply, so examine what we received
455            if (l.isOkMessage()) {
456                if (_state == ADDREQUESTSENTSTATE) {
457                    addToConsistList(_locoAddress, _directionNormal);
458                    if (consistType == ADVANCED_CONSIST) {
459                       //set the value in the roster entry for CV19
460                       setRosterEntryCVValue(_locoAddress);
461                    }
462                } else if (_state == REMOVEREQUESTSENTSTATE) {
463                    if (consistType == ADVANCED_CONSIST) {
464                       //reset the value in the roster entry for CV19
465                       resetRosterEntryCVValue(_locoAddress);
466                    }
467                    removeFromConsistList(_locoAddress);
468                }
469                _state = IDLESTATE;
470            } else if (l.getElement(0) == XNetConstants.LOCO_MU_DH_ERROR) {
471                String text;
472                switch (l.getElement(1)) {
473                    case 0x81:
474                        text = "Selected Locomotive has not been operated by this XpressNet device or address 0 selected";
475                        _state = IDLESTATE;
476                        notifyConsistListeners(_locoAddress,
477                                ConsistListener.CONSIST_ERROR
478                                | ConsistListener.LOCO_NOT_OPERATED);
479                        break;
480                    case 0x82:
481                        text = "Selected Locomotive is being operated by another XpressNet device";
482                        _state = IDLESTATE;
483                        notifyConsistListeners(_locoAddress,
484                                ConsistListener.CONSIST_ERROR
485                                | ConsistListener.LOCO_NOT_OPERATED);
486                        break;
487                    case 0x83:
488                        text = "Selected Locomotive already in MU or DH";
489                        _state = IDLESTATE;
490                        notifyConsistListeners(_locoAddress,
491                                ConsistListener.CONSIST_ERROR
492                                | ConsistListener.ALREADY_CONSISTED);
493                        break;
494                    case 0x84:
495                        text = "Unit selected for MU or DH has speed setting other than 0";
496                        _state = IDLESTATE;
497                        notifyConsistListeners(_locoAddress,
498                                ConsistListener.CONSIST_ERROR
499                                | ConsistListener.NONZERO_SPEED);
500                        break;
501                    case 0x85:
502                        text = "Locomotive not in a MU";
503                        _state = IDLESTATE;
504                        notifyConsistListeners(_locoAddress,
505                                ConsistListener.CONSIST_ERROR
506                                | ConsistListener.NOT_CONSISTED);
507                        break;
508                    case 0x86:
509                        text = "Locomotive address not a multi-unit base address";
510                        _state = IDLESTATE;
511                        notifyConsistListeners(_locoAddress,
512                                ConsistListener.CONSIST_ERROR
513                                | ConsistListener.NOT_CONSIST_ADDR);
514
515                        break;
516                    case 0x87:
517                        text = "It is not possible to delete the locomotive";
518                        _state = IDLESTATE;
519                        notifyConsistListeners(_locoAddress,
520                                ConsistListener.CONSIST_ERROR
521                                | ConsistListener.DELETE_ERROR);
522                        break;
523                    case 0x88:
524                        text = "The Command Station Stack is Full";
525                        _state = IDLESTATE;
526                        notifyConsistListeners(_locoAddress,
527                                ConsistListener.CONSIST_ERROR
528                                | ConsistListener.STACK_FULL);
529                        break;
530                    default:
531                        text = "Unknown";
532                        _state = IDLESTATE;
533                        notifyConsistListeners(_locoAddress,
534                                ConsistListener.CONSIST_ERROR);
535                }
536                log.error("XpressNet MU+DH error: {}",text);
537            }
538        }
539    }
540
541    @Override
542    public void message(XNetMessage l) {
543        // we don't care about outgoing messages
544    }
545
546   // Handle a timeout notification
547    @Override
548    public void notifyTimeout(XNetMessage msg) {
549        log.debug("Notified of timeout on message{}", msg);
550    }
551
552    /**
553     * Set the speed and direction of a locomotive; bypassing the commands in
554     * the throttle, since they don't work for this application.
555     * <p>
556     * For this application, we also set the speed setting to 0, which also
557     * establishes control over the locomotive in the consist.
558     *
559     * @param address   the DccLocoAddress of the locomotive.
560     * @param isForward the boolean value representing the desired direction
561     */
562    private void sendDirection(DccLocoAddress address, boolean isForward) {
563        XNetMessage msg = XNetMessage.getSpeedAndDirectionMsg(address.getNumber(),
564                jmri.SpeedStepMode.NMRA_DCC_28,
565                (float) 0.0,
566                isForward);
567        // now, we send the message to the command station
568        tc.sendXNetMessage(msg, this);
569    }
570
571    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(XNetConsist.class);
572
573}