001package jmri.jmrix.loconet;
002
003import java.util.ArrayList;
004
005import jmri.Consist;
006import jmri.ConsistListener;
007import jmri.LocoAddress;
008import jmri.DccLocoAddress;
009import jmri.ThrottleListener;
010
011/**
012 * LocoNetConsist.java
013 * This is the Consist definition for a consist on a LocoNet system.
014 * It uses the LocoNet specific commands to build a consist.
015 * @author Paul Bender Copyright (C) 2011
016 */
017public class LocoNetConsist extends jmri.implementation.DccConsist implements SlotListener, ThrottleListener {
018
019    private SlotManager slotManager = null;
020    private LnTrafficController trafficController = null;
021    private jmri.jmrix.AbstractThrottleManager throttleManager = null;
022    private LocoNetSlot leadSlot = null;
023
024    private ArrayList<DccLocoAddress> needToWrite = null;
025
026    // State Machine states
027    static final int IDLESTATE = 0;
028    static final int LEADREQUESTSTATE = 1;
029    static final int LINKSTAGEONESTATE = 2;
030    static final int LINKSTAGETWOSTATE = 4;
031    static final int LINKSTAGETHREESTATE = 8;
032    static final int UNLINKSTAGEONESTATE = 16;
033
034    private int consistRequestState = IDLESTATE;
035
036    // Initialize a consist for the specific address
037    // the Default consist type for LocoNet is a Command
038    // Station Consist.
039    public LocoNetConsist(int address, LocoNetSystemConnectionMemo lm) {
040        super(address);
041        this.slotManager = lm.getSlotManager();
042        this.trafficController = lm.getLnTrafficController();
043        this.throttleManager = (jmri.jmrix.AbstractThrottleManager) lm.getThrottleManager();
044        consistRequestState = LEADREQUESTSTATE;
045        consistType = Consist.CS_CONSIST;
046        needToWrite = new ArrayList<>();
047        throttleManager.requestThrottle(consistAddress, LocoNetConsist.this, false);
048    }
049
050    // Initialize a consist for the specific address
051    // the Default consist type for LocoNet is a Command
052    // Station Consist.
053    public LocoNetConsist(DccLocoAddress address, LocoNetSystemConnectionMemo lm) {
054        super(address);
055        this.slotManager = lm.getSlotManager();
056        this.trafficController = lm.getLnTrafficController();
057        this.throttleManager = (jmri.jmrix.AbstractThrottleManager) lm.getThrottleManager();
058        consistRequestState = LEADREQUESTSTATE;
059        consistType = Consist.CS_CONSIST;
060        needToWrite = new ArrayList<>();
061        throttleManager.requestThrottle(consistAddress, LocoNetConsist.this, false);
062    }
063
064    // Set the Consist Type
065    @Override
066    public void setConsistType(int type) {
067        switch (type) {
068            case Consist.ADVANCED_CONSIST:
069            case Consist.CS_CONSIST:
070                consistType = type;
071                break;
072            default:
073                log.error("Consist Type Not Supported");
074                notifyConsistListeners(new DccLocoAddress(0, false), ConsistListener.NotImplemented);
075                break;
076        }
077    }
078
079    /**
080     * Is this address allowed?
081     * On LocoNet systems, All addresses can be used in a Universal Consist
082     * and only 0 is not allowed in Advanced Consists.
083     * {@inheritDoc}
084     */
085    @Override
086    public boolean isAddressAllowed(DccLocoAddress address) {
087        return consistType == Consist.CS_CONSIST || (address.getNumber() != 0);
088    }
089
090    /** 
091     * Is there a size limit for this consist?
092     * @return -1 (no limit) for
093     * both CS and Advanced Consists,
094     * 0 for any other consist type.
095     */
096    @Override
097    public int sizeLimit() {
098        switch (consistType) {
099            case ADVANCED_CONSIST:
100            case CS_CONSIST:
101                return -1;
102            default:
103                return 0;
104        }
105    }
106
107    // does the consist contain the specified address?
108    @Override
109    public boolean contains(DccLocoAddress address) {
110        if (consistType == ADVANCED_CONSIST || consistType == CS_CONSIST) {
111            return consistList.contains(address);
112        } else {
113            log.error("Consist Type Not Supported");
114            notifyConsistListeners(address, ConsistListener.NotImplemented);
115        }
116        return false;
117    }
118
119    // get the relative direction setting for a specific
120    // locomotive in the consist
121    @Override
122    public boolean getLocoDirection(DccLocoAddress address) {
123        log.debug("consist {} obtaining direction for {} Consist List Size {}", 
124            consistAddress, address, consistList.size());
125        if (consistType == ADVANCED_CONSIST || consistType == CS_CONSIST) {
126            if (address == consistAddress) {
127                return true;
128            }
129            if (consistList.contains(address)) {
130                return consistDir.getOrDefault(address, false);
131            } else {
132                return (true);
133            }
134        } else {
135            log.error("Consist Type Not Supported");
136            notifyConsistListeners(address, ConsistListener.NotImplemented);
137        }
138        return false;
139    }
140
141    /**
142     * Add an Address to the internal Consist list object.
143     */
144    private synchronized void addToConsistList(DccLocoAddress locoAddress, boolean directionNormal) {
145        if (!(consistList.contains(locoAddress))) {
146            consistList.add(locoAddress);
147        }
148        if (consistDir.containsKey(locoAddress)) {
149            consistDir.remove(locoAddress);
150        }
151        consistDir.put(locoAddress, directionNormal);
152    }
153
154    /**
155     * Remove an address from the internal Consist list object.
156     */
157    private synchronized void removeFromConsistList(DccLocoAddress locoAddress) {
158        consistDir.remove(locoAddress);
159        consistList.remove(locoAddress);
160    }
161
162    /**
163     * Add a Locomotive to a Consist.
164     *
165     * @param locoAddress     the Locomotive address to add to the locomotive
166     * @param directionNormal if the locomotive is traveling
167     *        the same direction as the consist, false otherwise
168     */
169    @Override
170    public synchronized void add(DccLocoAddress locoAddress, boolean directionNormal) {
171        if (locoAddress == consistAddress) {
172            // this is required for command station consists on LocoNet.
173            addToConsistList(locoAddress, directionNormal);
174            notifyConsistListeners(locoAddress, ConsistListener.OPERATION_SUCCESS);
175        } else if (consistType == ADVANCED_CONSIST) {
176            if (consistList.contains(locoAddress)) {
177                // we are changing the direction, so remove first,
178                // then add
179                removeFromAdvancedConsist(locoAddress);
180            }
181            addToConsistList(locoAddress, directionNormal);
182            if (leadSlot == null || consistRequestState != IDLESTATE) {
183                needToWrite.add(locoAddress);
184            } else {
185                addToAdvancedConsist(locoAddress, directionNormal);
186            }
187        } else if (consistType == CS_CONSIST) {
188            if (consistList.contains(locoAddress)) {
189                // we are changing the direction, so remove first,
190                // then add
191                removeFromCSConsist(locoAddress);
192            }
193            addToConsistList(locoAddress, directionNormal);
194            if (leadSlot == null || consistRequestState != IDLESTATE) {
195                needToWrite.add(locoAddress);
196            } else {
197                addToCSConsist(locoAddress, directionNormal);
198            }
199        } else {
200            log.error("Consist Type Not Supported");
201            notifyConsistListeners(locoAddress, ConsistListener.NotImplemented);
202        }
203    }
204
205    private synchronized void delayedAdd() {
206        DccLocoAddress locoAddress = needToWrite.get(0);
207        if (consistType == ADVANCED_CONSIST) {
208            addToAdvancedConsist(locoAddress, getLocoDirection(locoAddress));
209        } else if (consistType == CS_CONSIST) {
210            addToCSConsist(locoAddress, getLocoDirection(locoAddress));
211        }
212        needToWrite.remove(locoAddress);
213    }
214
215    /**
216     * Restore a Locomotive to a Consist, but don't write to
217     * the command station.
218     * This is used for restoring the consist
219     * from a file or adding a consist read from the command station.
220     *
221     * @param locoAddress     the Locomotive address to add to the locomotive
222     * @param directionNormal True if the locomotive is traveling
223     *        the same direction as the consist, false otherwise
224     */
225    @Override
226    public synchronized void restore(DccLocoAddress locoAddress, boolean directionNormal) {
227        switch (consistType) {
228            case ADVANCED_CONSIST:
229            case CS_CONSIST:
230                addToConsistList(locoAddress, directionNormal);
231                break;
232            default:
233                log.error("Consist Type Not Supported");
234                notifyConsistListeners(locoAddress, ConsistListener.NotImplemented);
235                break;
236        }
237    }
238
239    /**
240     *  Remove a Locomotive from this Consist.
241     *
242     *  @param locoAddress is the Locomotive address to add to the locomotive
243     */
244    @Override
245    public synchronized void remove(DccLocoAddress locoAddress) {
246        switch (consistType) {
247            case ADVANCED_CONSIST:
248                removeFromAdvancedConsist(locoAddress);
249                removeFromConsistList(locoAddress);
250                break;
251            case CS_CONSIST:
252                removeFromCSConsist(locoAddress);
253                removeFromConsistList(locoAddress);
254                break;
255            default:
256                log.error("Consist Type Not Supported");
257                notifyConsistListeners(locoAddress, ConsistListener.NotImplemented);
258                break;
259        }
260    }
261
262    /**
263     *  Add a Locomotive to an Advanced Consist.
264     *
265     *  @param locoAddress     the Locomotive address to add to the locomotive
266     *  @param directionNormal True if the locomotive is traveling
267     *        the same direction as the consist, false otherwise
268     */
269    @Override
270    protected synchronized void addToAdvancedConsist(DccLocoAddress locoAddress, boolean directionNormal) {
271        log.debug("Add Locomotive {} to advanced consist {} With Direction Normal {}.",
272            locoAddress, consistAddress, directionNormal);
273        //set the value in the roster entry for CV19
274        setRosterEntryCVValue(locoAddress);
275        consistRequestState = LINKSTAGEONESTATE;
276        throttleManager.requestThrottle(locoAddress, this, false);
277    }
278
279    /**
280     *  Remove a Locomotive from an Advanced Consist
281     *  @param locoAddress is the Locomotive address to add to the locomotive
282     */
283    @Override
284    protected synchronized void removeFromAdvancedConsist(DccLocoAddress locoAddress) {
285        log.debug(" Remove Locomotive {} from advanced consist {}", locoAddress, consistAddress);
286        //reset the value in the roster entry for CV19
287        resetRosterEntryCVValue(locoAddress);
288        slotManager.slotFromLocoAddress(locoAddress.getNumber(), this);
289        consistRequestState = UNLINKSTAGEONESTATE;
290    }
291
292    /**
293     *  Add a Locomotive to a LocoNet Universal Consist.
294     *  @param locoAddress is the Locomotive address to add to the locomotive
295     *  @param directionNormal is True if the locomotive is traveling
296     *        the same direction as the consist, or false otherwise.
297     */
298    private synchronized void addToCSConsist(DccLocoAddress locoAddress, boolean directionNormal) {
299        log.debug("Add Locomotive {} to Standard Consist {} With Direction Normal {}.", 
300            locoAddress, consistAddress, directionNormal);
301        if(consistList.size()<=1 && locoAddress.equals(consistAddress)){
302          // there is only one address in this consist, no reason to link.
303          notifyConsistListeners(locoAddress,ConsistListener.OPERATION_SUCCESS);
304          return;
305        }
306        throttleManager.requestThrottle(locoAddress, this, false);
307        // skip right to stage 2, we do not need to status edit.
308        consistRequestState = LINKSTAGETWOSTATE;
309    }
310
311    /**
312     *  Remove a Locomotive from a LocoNet Universal Consist.
313     *  @param locoAddress is the Locomotive address to add to the locomotive.
314     */
315    public synchronized void removeFromCSConsist(DccLocoAddress locoAddress) {
316        log.debug("Remove Locomotive {} from Standard Consist {}.", locoAddress, consistAddress);
317        if(consistList.size()==1 && locoAddress.equals(consistAddress)){
318          // there is only one address in this consist, no reason to link.
319          notifyConsistListeners(locoAddress,ConsistListener.OPERATION_SUCCESS);
320          return;
321        }
322        slotManager.slotFromLocoAddress(locoAddress.getNumber(), this);
323        consistRequestState = UNLINKSTAGEONESTATE;
324    }
325
326    /**
327     * create and send a message to link two slots
328     * @param lead is the slot which is the leader
329     * @param follow is the slot which will follow the leader
330     */
331    private void linkSlots(LocoNetSlot lead, LocoNetSlot follow) {
332        LocoNetMessage msg;
333        if (lead != follow) {
334            if (slotManager.getLoconetProtocol() == LnConstants.LOCONETPROTOCOL_TWO) {
335                msg = new LocoNetMessage(6);
336                int dest1 = follow.getSlot() / 128;
337                int dest2 = follow.getSlot() % 128;
338                int src1 = lead.getSlot() / 128;
339                int src2 = lead.getSlot() % 128;
340                msg.setOpCode(LnConstants.OPC_EXP_SLOT_MOVE_RE_OPC_IB2_SPECIAL);
341                msg.setElement(1, dest1 | 0b00111000);
342                msg.setElement(2, dest2 & 0x7F);
343                msg.setElement(3, src1  | 0b01000000);
344                msg.setElement(4, src2 & 0x7F);
345            } else {
346                msg = new LocoNetMessage(4);
347                msg.setOpCode(LnConstants.OPC_LINK_SLOTS);
348                msg.setElement(1, follow.getSlot());
349                msg.setElement(2, lead.getSlot());
350            }
351            trafficController.sendLocoNetMessage(msg);
352        } else {
353          // lead == follow
354          // this is an error, notify the consist listeners.
355          follow.removeSlotListener(this);
356          notifyConsistListeners(new DccLocoAddress(follow.locoAddr(),
357                throttleManager.canBeLongAddress(follow.locoAddr())),
358                ConsistListener.CONSIST_ERROR);
359        }
360        consistRequestState = IDLESTATE;
361        if (!needToWrite.isEmpty()) {
362            delayedAdd();
363        }
364    }
365
366    /**
367     * create and send a message to unlink two slots
368     * @param lead is the slot which is the leader
369     * @param follow is the slot which was following the leader
370     */
371    private void unlinkSlots(LocoNetSlot lead, LocoNetSlot follow) {
372        LocoNetMessage msg;
373        if (lead != follow) {
374            if (slotManager.getLoconetProtocol() == LnConstants.LOCONETPROTOCOL_TWO) {
375                msg = new LocoNetMessage(6);
376                int src1 = lead.getSlot() / 128;
377                int src2 = lead.getSlot() % 128;
378                int dest1 = follow.getSlot() / 128;
379                int dest2 = follow.getSlot() % 128;
380                msg.setOpCode(LnConstants.OPC_EXP_SLOT_MOVE_RE_OPC_IB2_SPECIAL);
381                msg.setElement(3, src1 | 0b01010000);
382                msg.setElement(4, src2 & 0x7F);
383                msg.setElement(1, dest1 | 0b00111000);
384                msg.setElement(2, dest2 & 0x7F);
385            } else {
386                msg = new LocoNetMessage(4);
387                msg.setOpCode(LnConstants.OPC_UNLINK_SLOTS);
388                msg.setElement(1, follow.getSlot());
389                msg.setElement(2, lead.getSlot());
390            }
391            trafficController.sendLocoNetMessage(msg);
392        } else {
393          // lead == follow
394          // this is an error, notify the consist listeners.
395          follow.removeSlotListener(this);
396          notifyConsistListeners(new DccLocoAddress(follow.locoAddr(),
397                throttleManager.canBeLongAddress(follow.locoAddr())),
398                ConsistListener.CONSIST_ERROR | ConsistListener.DELETE_ERROR );
399        }
400        consistRequestState = IDLESTATE;
401        if (!needToWrite.isEmpty()) {
402            delayedAdd();
403        }
404    }
405
406    private void setDirection(LocoNetThrottle t) {
407        log.debug("consist {} set direction for {}", consistAddress, t.getLocoAddress());
408        // send a command to set the direction
409        // of the locomotive in the slot.
410        boolean directionNormal = getLocoDirection((DccLocoAddress) t.getLocoAddress());
411        if (directionNormal) {
412            t.setIsForward(leadSlot.isForward());
413        } else {
414            t.setIsForward(!leadSlot.isForward());
415        }
416
417        consistRequestState = LINKSTAGETWOSTATE;
418    }
419
420    private void setSlotModeAdvanced(LocoNetSlot s) {
421        // set the slot so that it can be an advanced consist
422        int oldstatus = s.slotStatus();
423        int newstatus = oldstatus | LnConstants.STAT1_SL_SPDEX;
424        trafficController.sendLocoNetMessage(s.writeStatus(newstatus));
425    }
426
427    // slot listener interface functions
428    @Override
429    public void notifyChangedSlot(LocoNetSlot s) {
430        log.debug("Notified slot {} changed with mode {} slot consist state: {}", 
431            s.getSlot(), consistRequestState, LnConstants.CONSIST_STAT(s.consistStatus()));
432        switch (consistRequestState) {
433            case LEADREQUESTSTATE:
434                leadSlot = s;
435                consistRequestState = IDLESTATE;
436                break;
437            case LINKSTAGEONESTATE:
438                s.addSlotListener(this);
439                setSlotModeAdvanced(s);
440                consistRequestState = LINKSTAGETWOSTATE;
441                break;
442            case LINKSTAGETWOSTATE:
443                linkSlots(leadSlot, s);
444                break;
445            case UNLINKSTAGEONESTATE:
446                unlinkSlots(leadSlot, s);
447                break;
448            default:
449                s.removeSlotListener(this);
450                notifyConsistListeners(new DccLocoAddress(s.locoAddr(),
451                        throttleManager.canBeLongAddress(s.locoAddr())),
452                        ConsistListener.OPERATION_SUCCESS);
453                if (!needToWrite.isEmpty()) {
454                    delayedAdd();
455                } else {
456                    consistRequestState = IDLESTATE;
457                }
458        }
459    }
460
461    // Throttle listener interface functions
462    @Override
463    public void notifyThrottleFound(jmri.DccThrottle t) {
464        log.debug("notified Throttle {} found with mode {}", t.getLocoAddress(), consistRequestState);
465        try {
466            if (consistRequestState == LEADREQUESTSTATE) {
467                ((LocoNetThrottle) t).setIsForward(true);
468                leadSlot = ((LocoNetThrottle) t).getLocoNetSlot();
469                consistRequestState = IDLESTATE;
470                if (!needToWrite.isEmpty()) {
471                    delayedAdd();
472                }
473            } else {
474                LocoNetSlot tempSlot = ((LocoNetThrottle) t).getLocoNetSlot();
475                if (tempSlot != null) {
476                    tempSlot.addSlotListener(this);
477                    if (consistRequestState == LINKSTAGEONESTATE) {
478                        notifyChangedSlot(tempSlot);
479                        setDirection(((LocoNetThrottle) t));
480                        consistRequestState = LINKSTAGETWOSTATE;
481                    } else {
482                        setDirection(((LocoNetThrottle) t));
483                    }
484                } else {
485                    log.error("Cannot notify a throttle's slot if the slot is null!");
486                }
487            }
488        } catch (java.lang.ClassCastException cce) {
489            // if the simulator is in use, we will
490            // get a ClassCastException.
491            if (consistRequestState == LEADREQUESTSTATE) {
492                t.setIsForward(true);
493                consistRequestState = IDLESTATE;
494                if (!needToWrite.isEmpty()) {
495                    delayedAdd();
496                }
497            } else {
498                if (t instanceof LocoNetThrottle) {
499                    LocoNetThrottle lt = (LocoNetThrottle)t;
500                    setDirection(lt);
501                }
502            }
503        }
504    }
505
506    @Override
507    public void notifyFailedThrottleRequest(LocoAddress address, String reason) {
508        if (! (address instanceof DccLocoAddress)) {
509            throw new IllegalArgumentException("address is not a DccLocoAddress object");
510        }
511        notifyConsistListeners((DccLocoAddress) address,
512                ConsistListener.CONSIST_ERROR);
513        removeFromConsistList((DccLocoAddress) address);
514        consistRequestState = IDLESTATE;
515    }
516
517    /**
518     * No steal or share decisions made locally
519     * <p>
520     * {@inheritDoc}
521     */
522    @Override
523    public void notifyDecisionRequired(jmri.LocoAddress address, DecisionType question) {
524        log.debug("notifydecisionrequired {} {}", address, question);
525    }
526
527    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LocoNetConsist.class);
528
529}