001package jmri.jmrix.nce;
002
003import java.util.ArrayList;
004
005import org.slf4j.Logger;
006import org.slf4j.LoggerFactory;
007
008import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
009import jmri.Consist;
010import jmri.ConsistListener;
011import jmri.DccLocoAddress;
012import jmri.implementation.DccConsist;
013
014/**
015 * The Consist definition for a consist on an NCE system. It uses the NCE
016 * specific commands to build a consist.
017 *
018 * @author Paul Bender Copyright (C) 2011
019 * @author Daniel Boudreau Copyright (C) 2012
020 * @author Ken Cameron Copyright (C) 2023
021 */
022public class NceConsist extends jmri.implementation.DccConsist implements jmri.jmrix.nce.NceListener {
023
024    public static final int CONSIST_MIN = 1;    // NCE doesn't use consist 0
025    public static final int CONSIST_MAX = 127;
026    private NceTrafficController tc = null;
027    private boolean _valid = false;
028
029    // state machine stuff
030    private int _busy = 0;
031    private int _replyLen = 0;      // expected byte length
032    private static final int REPLY_1 = 1;  // reply length of 16 bytes expected
033    private byte _consistNum = 0;    // consist number (short address of consist)
034
035    // Initialize a consist for the specific address
036    // the Default consist type is an advanced consist 
037    public NceConsist(int address, NceSystemConnectionMemo m) {
038        super(address);
039        tc = m.getNceTrafficController();
040        loadConsist(address);
041    }
042
043    // Initialize a consist for the specific address
044    // the Default consist type is an advanced consist 
045    public NceConsist(DccLocoAddress locoAddress, NceSystemConnectionMemo m) {
046        super(locoAddress);
047        tc = m.getNceTrafficController();
048        loadConsist(locoAddress.getNumber());
049    }
050
051    // Clean Up local storage
052    @Override
053    public void dispose() {
054        if(consistList == null) {
055           // already disposed;
056           return;
057        }
058        if (consistList.size() > 0) {
059            // kill this consist
060            DccLocoAddress locoAddress = consistList.get(0);
061            killConsist(locoAddress.getNumber(), locoAddress.isLongAddress());
062        }
063        stopReadNCEconsistThread();
064        super.dispose();
065        consistList = null;
066    }
067
068    // Set the Consist Type
069    @Override
070    public void setConsistType(int consist_type) {
071        if (consist_type == Consist.ADVANCED_CONSIST) {
072            consistType = consist_type;
073        } else {
074            log.error("Consist Type Not Supported");
075            notifyConsistListeners(new DccLocoAddress(0, false), ConsistListener.NotImplemented);
076        }
077    }
078
079    /* is there a size limit for this consist?
080     */
081    @Override
082    public int sizeLimit() {
083        return 6;
084    }
085
086    /**
087     * Add a Locomotive to a Consist
088     *
089     * @param locoAddress     is the Locomotive address to add to the consist
090     * @param directionNormal is True if the locomotive is traveling the same
091     *                        direction as the consist, or false otherwise.
092     */
093    @Override
094    public synchronized void add(DccLocoAddress locoAddress, boolean directionNormal) {
095        if (!contains(locoAddress)) {
096            // NCE has 6 commands for adding a loco to a consist, lead, rear, and mid, plus direction
097            // First loco to consist?
098            if (consistList.size() == 0) {
099                // add lead loco
100                byte command = NceMessage.LOCO_CMD_FWD_CONSIST_LEAD;
101                if (!directionNormal) {
102                    command = NceMessage.LOCO_CMD_REV_CONSIST_LEAD;
103                }
104                addLocoToConsist(locoAddress.getNumber(), locoAddress.isLongAddress(), command);
105                consistPosition.put(locoAddress, DccConsist.POSITION_LEAD);
106            } // Second loco to consist?
107            else if (consistList.size() == 1) {
108                // add rear loco
109                byte command = NceMessage.LOCO_CMD_FWD_CONSIST_REAR;
110                if (!directionNormal) {
111                    command = NceMessage.LOCO_CMD_REV_CONSIST_REAR;
112                }
113                addLocoToConsist(locoAddress.getNumber(), locoAddress.isLongAddress(), command);
114                consistPosition.put(locoAddress, DccConsist.POSITION_TRAIL);
115            } else {
116                // add mid loco
117                byte command = NceMessage.LOCO_CMD_FWD_CONSIST_MID;
118                if (!directionNormal) {
119                    command = NceMessage.LOCO_CMD_REV_CONSIST_MID;
120                }
121                addLocoToConsist(locoAddress.getNumber(), locoAddress.isLongAddress(), command);
122                consistPosition.put(locoAddress, consistPosition.size());
123            }
124            // add loco to lists
125            consistList.add(locoAddress);
126            consistDir.put(locoAddress, Boolean.valueOf(directionNormal));
127        } else {
128            log.error("Loco {} is already part of this consist {}", locoAddress, getConsistAddress());
129        }
130
131    }
132
133    public void restore(DccLocoAddress locoAddress, boolean directionNormal, int position) {
134        consistPosition.put(locoAddress, position);
135        super.restore(locoAddress, directionNormal);
136        //notifyConsistListeners(locoAddress, ConsistListener.OPERATION_SUCCESS);
137    }
138
139    /**
140     * Remove a locomotive from this consist
141     *
142     * @param locoAddress is the locomotive address to remove from this consist
143     */
144    @Override
145    public synchronized void remove(DccLocoAddress locoAddress) {
146        if (contains(locoAddress)) {
147            // can not delete the lead or rear loco from a NCE consist
148            int position = getPosition(locoAddress);
149            if (position == DccConsist.POSITION_LEAD || position == DccConsist.POSITION_TRAIL) {
150                log.info("Can not delete lead or rear loco from a NCE consist!");
151                notifyConsistListeners(locoAddress, ConsistListener.DELETE_ERROR);
152                return;
153            }
154            // send remove loco from consist to NCE command station
155            removeLocoFromConsist(locoAddress.getNumber(), locoAddress.isLongAddress());
156            //reset the value in the roster entry for CV19
157            resetRosterEntryCVValue(locoAddress);
158
159            // remove from lists
160            consistRoster.remove(locoAddress);
161            consistPosition.remove(locoAddress);
162            consistDir.remove(locoAddress);
163            consistList.remove(locoAddress);
164            notifyConsistListeners(locoAddress, ConsistListener.OPERATION_SUCCESS);
165        } else {
166            log.error("Loco {} is not part of this consist {}", locoAddress, getConsistAddress());
167        }
168    }
169
170    private void loadConsist(int consistNum) {
171        if (consistNum > CONSIST_MAX || consistNum < CONSIST_MIN) {
172            log.error("Requesting consist {} out of range", consistNum);
173            return;
174        }
175        _consistNum = (byte) consistNum;
176        startReadNCEconsistThread(false);
177    }
178
179    public void checkConsist() {
180        if (!isValid()) {
181            return; // already checking the consist
182        }
183        setValid(false);
184        startReadNCEconsistThread(true);
185    }
186
187    private NceReadConsist mb = null;
188
189    private synchronized void startReadNCEconsistThread(boolean check) {
190        // read command station memory to get the current consist (can't be a USB, only PH)
191        if (tc.getUsbSystem() == NceTrafficController.USB_SYSTEM_NONE) {
192            mb = new NceReadConsist();
193            mb.setName("Read Consist " + _consistNum);
194            mb.setConsist(_consistNum);
195            mb.setCheck(check);
196            mb.start();
197        }
198    }
199
200    private synchronized void stopReadNCEconsistThread() {
201        if (mb != null) {
202            try {
203                mb.interrupt();
204                mb.join();
205            } catch (InterruptedException ex) {
206                log.warn("stopReadNCEconsistThread interrupted");
207            } catch (Throwable t) {
208                log.error("stopReadNCEconsistThread caught ", t);
209                throw t;
210            } finally {
211                mb = null;
212            }
213        }
214    }
215
216    public DccLocoAddress getLocoAddressByPosition(int position) {
217        DccLocoAddress locoAddress;
218        ArrayList<DccLocoAddress> list = getConsistList();
219        for (int i = 0; i < list.size(); i++) {
220            locoAddress = list.get(i);
221            if (getPosition(locoAddress) == position) {
222                return locoAddress;
223            }
224        }
225        return null;
226    }
227
228    /**
229     * Used to determine if consist has been initialized properly.
230     *
231     * @return true if command station memory has been read for this consist
232     *         number.
233     */
234    public boolean isValid() {
235        return _valid;
236    }
237
238    private void setValid(boolean valid) {
239        _valid = valid;
240    }
241
242    /**
243     * Adds a loco to the consist
244     *
245     * @param address The address of the loco to be added
246     * @param command There are six NCE commands to add a loco to a consist. Add
247     *                Lead, Rear, Mid, and the loco direction 3x2 = 6 commands.
248     */
249    private void addLocoToConsist(int address, boolean isLong, byte command) {
250        if (isLong) {
251            address += 0xC000; // set the upper two bits for long addresses
252        }
253        sendNceBinaryCommand(address, command, _consistNum);
254    }
255
256    /**
257     * Remove a loco from any consist. The consist number is not supplied to
258     * NCE.
259     *
260     * @param address The address of the loco to be removed
261     * @param isLong  true if long address
262     */
263    private void removeLocoFromConsist(int address, boolean isLong) {
264        if (isLong) {
265            address += 0xC000; // set the upper two bits for long addresses
266        }
267        sendNceBinaryCommand(address, NceMessage.LOCO_CMD_DELETE_LOCO_CONSIST, (byte) 0);
268    }
269
270    /**
271     * Kills consist using lead loco address
272     * @param address loco address
273     * @param isLong true if long address
274     */
275    void killConsist(int address, boolean isLong) {
276        if (isLong) {
277            address += 0xC000; // set the upper two bits for long addresses
278        }
279        sendNceBinaryCommand(address, NceMessage.LOCO_CMD_KILL_CONSIST, (byte) 0);
280    }
281
282    private void sendNceBinaryCommand(int nceAddress, byte nceLocoCmd, byte consistNumber) {
283        byte[] bl = NceBinaryCommand.nceLocoCmd(nceAddress, nceLocoCmd, consistNumber);
284        sendNceMessage(bl, REPLY_1);
285    }
286
287    private void sendNceMessage(byte[] b, int replyLength) {
288        NceMessage m = NceMessage.createBinaryMessage(tc, b, replyLength);
289        _busy++;
290        _replyLen = replyLength; // Expect n byte response
291        tc.sendNceMessage(m, this);
292    }
293
294    @Override
295    public void message(NceMessage m) {
296        // not used
297    }
298
299    @Override
300    public void reply(NceReply r) {
301        if (_busy == 0) {
302            log.debug("Consist {} read reply not for this consist", _consistNum);
303            return;
304        }
305        if (r.getNumDataElements() != _replyLen) {
306            log.error("reply length error, expecting: {} got: {}", _replyLen, r.getNumDataElements());
307            return;
308        }
309        if (_replyLen == 1 && r.getElement(0) == NceMessage.NCE_OKAY) {
310            log.debug("Command complete okay for consist {}", getConsistAddress());
311        } else {
312            log.error("Error, command failed for consist {}", getConsistAddress());
313        }
314    }
315
316    public class NceReadConsist extends Thread implements jmri.jmrix.nce.NceListener {
317
318        // state machine stuff
319        private int _consistNum = 0;
320        private int _busy = 0;
321        private boolean _validConsist = false;    // true when there's a lead and rear loco in the consist
322        private boolean _check = false;    // when true update consist to match NCE CS
323
324        private int _replyLen = 0;      // expected byte length
325        private static final int REPLY_16 = 16;  // reply length of 16 bytes expected
326
327        private int _locoNum = LEAD;     // which loco, 0 = lead, 1 = rear, 2 = mid
328        private static final int LEAD = 0;
329        private static final int REAR = 1;
330        private static final int MID = 2;
331
332        public void setConsist(int number) {
333            _consistNum = number;
334        }
335
336        public void setCheck(boolean check) {
337            _check = check;
338        }
339
340        // load up the consist lists by lead, rear, and then mid
341        @Override
342        public void run() {
343            try{
344                readConsistMemory(_consistNum, LEAD);
345                readConsistMemory(_consistNum, REAR);
346                readConsistMemory(_consistNum, MID);
347                setValid(true);
348            } catch (InterruptedException e) {
349                return; // we're done!
350            } catch (Throwable t) {
351                if ( ! (t instanceof java.lang.ThreadDeath) ) {
352                    log.error("NceReadConsist.run caught ", t);
353                }
354                throw t;
355            }
356        }
357
358        /**
359         * Reads 16 bytes of NCE consist memory based on consist number and loco
360         * number 0=lead 1=rear 2=mid
361         */
362        private void readConsistMemory(int consistNum, int eNum) throws InterruptedException { // throw interrupt upward
363            if (consistNum > CONSIST_MAX || consistNum < CONSIST_MIN) {
364                log.error("Requesting consist {} out of range", consistNum);
365                return;
366            }
367            // if busy wait
368            if (!readWait()) {
369                log.error("Time out reading NCE command station consist memory");
370                return;
371            }
372            _locoNum = eNum;
373            int nceMemAddr = (consistNum * 2) + tc.csm.getConsistHeadAddr();
374            if (eNum == REAR) {
375                nceMemAddr = (consistNum * 2) + tc.csm.getConsistTailAddr();
376            }
377            if (eNum == MID) {
378                nceMemAddr = (consistNum * 8) + tc.csm.getConsistMidAddr();
379            }
380            if (eNum == LEAD || _validConsist) {
381                byte[] bl = NceBinaryCommand.accMemoryRead(nceMemAddr);
382                sendNceMessage(bl, REPLY_16);
383            }
384        }
385
386        private void sendNceMessage(byte[] b, int replyLength) {
387            NceMessage m = NceMessage.createBinaryMessage(tc, b, replyLength);
388            _busy++;
389            _replyLen = replyLength; // Expect n byte response
390            tc.sendNceMessage(m, this);
391        }
392
393        // wait up to 30 sec per read
394        private boolean readWait() throws InterruptedException { // throw interrupt upward
395            int waitcount = 30;
396            while (_busy > 0) {
397                synchronized (this) {
398                    wait(1000);
399                }
400                if (waitcount-- < 0) {
401                    log.error("read timeout");
402                    return false;
403                }
404            }
405            return true;
406        }
407
408        @Override
409        public void message(NceMessage m) {
410            // not used
411        }
412
413        @SuppressFBWarnings(value = "NN_NAKED_NOTIFY") // notify not naked
414        @Override
415        public void reply(NceReply r) {
416            if (_busy == 0) {
417                log.debug("Consist {} read reply not for this consist", _consistNum);
418                return;
419            }
420            log.debug("Consist {} read reply number {}", _consistNum, _locoNum);
421            if (r.getNumDataElements() != _replyLen) {
422                log.error("reply length error, expecting: {} got: {}", _replyLen, r.getNumDataElements());
423                return;
424            }
425
426            // are we checking to see if the consist matches CS memory?
427            if (_check) {
428                log.debug("Checking {}", _consistNum);
429                if (_locoNum == LEAD) {
430                    _validConsist = checkLocoConsist(r, 0, DccConsist.POSITION_LEAD); // consist is valid if there's at least a lead & rear loco
431                }
432                if (_validConsist && _locoNum == REAR) {
433                    _validConsist = checkLocoConsist(r, 0, DccConsist.POSITION_TRAIL);
434                }
435
436                if (_validConsist && _locoNum == MID) {
437                    for (int index = 0; index < 8; index = index + 2) {
438                        checkLocoConsist(r, index, consistPosition.size());
439                    }
440                }
441
442            } else {
443                if (_locoNum == LEAD) {
444                    _validConsist = addLocoConsist(r, 0, DccConsist.POSITION_LEAD); // consist is valid if there's at least a lead & rear loco
445                }
446                if (_validConsist && _locoNum == REAR) {
447                    _validConsist = addLocoConsist(r, 0, DccConsist.POSITION_TRAIL);
448                }
449
450                if (_validConsist && _locoNum == MID) {
451                    for (int index = 0; index < 8; index = index + 2) {
452                        addLocoConsist(r, index, consistPosition.size());
453                    }
454                }
455            }
456
457            _busy--;
458
459            // wake up thread
460            synchronized (this) {
461                notify();
462            }
463        }
464
465        /*
466         * Returns true if loco added to consist
467         */
468        private boolean addLocoConsist(NceReply r, int index, int position) {
469            int address = getLocoAddrText(r, index);
470            boolean isLong = getLocoAddressType(r, index); // Long (true) or short (false) address?   
471            if (address != 0) {
472                log.debug("Add loco address {} to consist {}", address, _consistNum);
473                restore(new DccLocoAddress(address, isLong), true, position); // we don't know the direction of the loco
474                return true;
475            }
476            return false;
477        }
478
479        private boolean checkLocoConsist(NceReply r, int index, int position) {
480            int address = getLocoAddrText(r, index);
481            boolean isLong = getLocoAddressType(r, index); // Long (true) or short (false) address?
482            DccLocoAddress locoAddress = new DccLocoAddress(address, isLong);
483            if (contains(locoAddress)) {
484                log.debug("Loco address {} found match for consist {}", locoAddress, _consistNum);
485            } else if (address != 0) {
486                log.debug("New loco address {} found for consist {}", locoAddress, _consistNum);
487                restore(locoAddress, true, position); // we don't know the direction of the loco
488            } else {
489                log.debug("Found loco address 0 for consist {} index {} position {}", _consistNum, index, position);
490                // remove loco by position in consist
491                locoAddress = getLocoAddressByPosition(position);
492                if (locoAddress != null) {
493                    remove(locoAddress);
494                }
495            }
496            return true;
497        }
498
499        private int getLocoAddrText(NceReply r, int index) {
500            int rC = r.getElement(index++);
501            rC = (rC << 8) & 0x3F00;  // Mask off upper two bits
502            int rC_l = r.getElement(index);
503            rC_l = rC_l & 0xFF;
504            rC = rC + rC_l;
505            return rC;
506        }
507
508        // get loco address type, returns true if long
509        private boolean getLocoAddressType(NceReply r, int index) {
510            int rC = r.getElement(index);
511            rC = rC & 0xC0; // long address if 2 msb are set
512            if (rC == 0xC0) {
513                return true;
514            } else {
515                return false;
516            }
517        }
518    }
519
520    private final static Logger log = LoggerFactory.getLogger(NceConsist.class);
521
522}