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