001package jmri.jmrix.nce;
002
003import org.slf4j.Logger;
004import org.slf4j.LoggerFactory;
005
006import jmri.CommandStation;
007import jmri.JmriException;
008import jmri.NmraPacket;
009import jmri.jmrix.AbstractMRListener;
010import jmri.jmrix.AbstractMRMessage;
011import jmri.jmrix.AbstractMRReply;
012import jmri.jmrix.AbstractMRTrafficController;
013
014/**
015 * Converts Stream-based I/O to/from NCE messages. The "NceInterface" side
016 * sends/receives message objects.
017 * <p>
018 * The connection to a NcePortController is via a pair of *Streams, which then
019 * carry sequences of characters for transmission. Note that this processing is
020 * handled in an independent thread.
021 * <p>
022 * This handles the state transitions, based on the necessary state in each
023 * message.
024 *
025 * @author Bob Jacobsen Copyright (C) 2001
026 * @author Ken Cameron Copyright (C) 2013, 2023
027 */
028public class NceTrafficController extends AbstractMRTrafficController implements NceInterface, CommandStation {
029
030    /**
031     * Create a new NCE SerialTrafficController instance. Simple implementation.
032     */
033    public NceTrafficController() {
034        super();
035    }
036
037    // The methods to implement the NceInterface
038    @Override
039    public synchronized void addNceListener(NceListener l) {
040        this.addListener(l);
041    }
042
043    @Override
044    public synchronized void removeNceListener(NceListener l) {
045        this.removeListener(l);
046    }
047
048    @Override
049    protected int enterProgModeDelayTime() {
050        // we should to wait at least a second after enabling the programming track
051        return 1000;
052    }
053
054    /**
055     * CommandStation implementation
056     */
057    @Override
058    public boolean sendPacket(byte[] packet, int count) {
059        NceMessage m;
060
061        boolean isUsb = ((getUsbSystem() == NceTrafficController.USB_SYSTEM_POWERCAB
062                || getUsbSystem() == NceTrafficController.USB_SYSTEM_SB3
063                || getUsbSystem() == NceTrafficController.USB_SYSTEM_SB5
064                || getUsbSystem() == NceTrafficController.USB_SYSTEM_TWIN));
065
066        if (NmraPacket.isAccSignalDecoderPkt(packet)
067                && (NmraPacket.getAccSignalDecoderPktAddress(packet) > 0)
068                && (NmraPacket.getAccSignalDecoderPktAddress(packet) <= 2044)) {
069            // intercept only those NMRA signal cmds we can handle with NCE binary commands
070            int addr = NmraPacket.getAccSignalDecoderPktAddress(packet);
071            int aspect = packet[2];
072            log.debug("isAccSignalDecoderPkt(packet) sigAddr ={}, aspect ={}", addr, aspect);
073            m = NceMessage.createAccySignalMacroMessage(this, 5, addr, aspect);
074        } else if (isUsb && NmraPacket.isAccDecoderPktOpsMode(packet)) {
075            // intercept NMRA accessory decoder ops programming cmds to USB systems
076            int accyAddr = NmraPacket.getAccDecoderPktOpsModeAddress(packet);
077            int cvAddr = (((0x03 & packet[2]) << 8) | (0xFF & packet[3])) + 1;
078            int cvData = (0xFF & packet[4]);
079            log.debug("isAccDecoderPktOpsMode(packet) accyAddr ={}, cvAddr = {}, cvData ={}", accyAddr, cvAddr, cvData);
080            m = NceMessage.createAccDecoderPktOpsMode(this, accyAddr, cvAddr, cvData);
081        } else if (isUsb && NmraPacket.isAccDecoderPktOpsModeLegacy(packet)) {
082            // intercept NMRA accessory decoder ops programming cmds to USB systems
083            int accyAddr = NmraPacket.getAccDecoderPktOpsModeLegacyAddress(packet);
084            int cvData = (0xFF & packet[3]);
085            int cvAddr = (((0x03 & packet[1]) << 8) | (0xFF & packet[2])) + 1;
086            log.debug("isAccDecoderPktOpsModeLegacy(packet) accyAddr ={}, cvAddr = {}, cvData ={}", accyAddr, cvAddr, cvData);
087            m = NceMessage.createAccDecoderPktOpsMode(this, accyAddr, cvAddr, cvData);
088        } else {
089            m = NceMessage.sendPacketMessage(this, packet);
090            if (m == null) {
091                return false;
092            }
093        }
094        this.sendNceMessage(m, null);
095        return true;
096    }
097
098    /**
099     * Forward a NceMessage to all registered NceInterface listeners.
100     */
101    @Override
102    protected void forwardMessage(AbstractMRListener client, AbstractMRMessage m) {
103        ((NceListener) client).message((NceMessage) m);
104    }
105
106    /**
107     * Forward a NceReply to all registered NceInterface listeners.
108     */
109    @Override
110    protected void forwardReply(AbstractMRListener client, AbstractMRReply r) {
111        ((NceListener) client).reply((NceReply) r);
112    }
113
114    NceSensorManager mSensorManager = null;
115
116    public void setSensorManager(NceSensorManager m) {
117        mSensorManager = m;
118    }
119
120    public NceSensorManager getSensorManager() {
121        return mSensorManager;
122    }
123
124    /**
125     * Create all commands in the ASCII format.
126     */
127    static public final int OPTION_FORCE_ASCII = -1;
128    /**
129     * Create commands compatible with the 1999 EPROM.
130     * <p>
131     * This is binary for everything except service-mode CV programming
132     * operations.
133     */
134    static public final int OPTION_1999 = 0;
135    /**
136     * Create commands compatible with the 2004 EPROM.
137     * <p>
138     * This is binary for everything except service-mode CV programming
139     * operations.
140     */
141    static public final int OPTION_2004 = 10;
142    /**
143     * Create commands compatible with the 2006 EPROM.
144     * <p>
145     * This is binary for everything, including service-mode CV programming
146     * operations.
147     */
148    static public final int OPTION_2006 = 20;
149    /**
150     * Create commands compatible with the 1.28 EPROM.
151     * <p>
152     * For PowerCab/SB3 original pre-Nov 2012
153     */
154    static public final int OPTION_1_28 = 30;
155    /**
156     * Create commands compatible with the 1.65 EPROM.
157     * <p>
158     * For PowerCab/SB5/Twin update post-Nov 2012
159     */
160    static public final int OPTION_1_65 = 40;
161    /**
162     * Create commands compatible with the PH5.
163     * <p>
164     * For PH5
165     */
166    static public final int OPTION_PH5 = 80;
167    /**
168     * Create all commands in the binary format.
169     */
170    static public final int OPTION_FORCE_BINARY = 10000;
171
172    private int commandOptions = OPTION_2006;
173    public boolean commandOptionSet = false;
174    private boolean nceEpromMarch2007 = false; // flag to allow JMRI to be bug for bug compatible
175    private boolean pwrProVer060203orLater = false;
176    private final int[] pwrProVers = new int[3];
177    private boolean simulatorRunning = false; // true if simulator is running
178
179    /**
180     * Return the Power Pro firmware version as user-friendly hex text.
181     *
182     * @return period-separated firmware version
183     */
184    public String getPwrProVersHexText() {
185        StringBuilder sb = new StringBuilder();
186        sb.append(Integer.toHexString(pwrProVers[0] & 0xFF)).append(".");
187        sb.append(Integer.toHexString(pwrProVers[1] & 0xFF)).append(".");
188        sb.append(Integer.toHexString(pwrProVers[2] & 0xFF));
189        return sb.toString();
190    }
191
192    /**
193     * Store the Power Pro firmware version.
194     *
195     * @param VV major version
196     * @param MM intermediate version
197     * @param mm minor version
198     */
199    public void setPwrProVers(byte VV, byte MM, byte mm) {
200        this.pwrProVers[0] = VV & 0xFF;
201        this.pwrProVers[1] = MM & 0xFF;
202        this.pwrProVers[2] = mm & 0xFF;
203    }
204
205    /**
206     * Ask whether Power Pro firmware version is 6.2.3 or later.
207     *
208     * @return {@code true} if it does, otherwise {@code false}
209     */
210    public boolean isPwrProVer060203orLater() {
211        return pwrProVer060203orLater;
212    }
213
214    /**
215     * Specify whether Power Pro firmware version is 6.2.3 or later.
216     *
217     * @param isTrue {@code true} if it does, otherwise {@code false}
218     */
219    public void setPwrProVer060203orLater(boolean isTrue) {
220        pwrProVer060203orLater = isTrue;
221    }
222    
223    public boolean isNceEpromMarch2007() {
224        return nceEpromMarch2007;
225    }
226
227    public void setNceEpromMarch2007(boolean b) {
228        nceEpromMarch2007 = b;
229    }
230    
231    public boolean isSimulatorRunning() {
232        return simulatorRunning;
233    }
234    
235    public void setSimulatorRunning(boolean b) {
236        simulatorRunning = b;
237    }
238
239    /**
240     * Control which command format should be used for various commands: ASCII
241     * or binary.
242     * <p>
243     * The valid argument values are the class "OPTION" constants, which are
244     * interpreted in the various methods to get a particular message.
245     * <ul>
246     * <li>{@link #OPTION_FORCE_ASCII}
247     * <li>{@link #OPTION_1999}
248     * <li>{@link #OPTION_2004}
249     * <li>{@link #OPTION_2006}
250     * <li>{@link #OPTION_1_28}
251     * <li>{@link #OPTION_1_65}
252     * <li>{@link #OPTION_FORCE_BINARY}
253     * </ul>
254     *
255     * @param val command station options
256     *
257     */
258    public void setCommandOptions(int val) {
259        commandOptions = val;
260        if (commandOptionSet) {
261            log.warn("setCommandOptions called more than once");
262        }
263        commandOptionSet = true;
264    }
265
266    /**
267     * Determine which command format should be used for various commands: ASCII
268     * or binary.
269     * <p>
270     * The valid return values are the class "OPTION" constants, which are
271     * interpreted in the various methods to get a particular message.
272     * <ul>
273     * <li>{@link #OPTION_FORCE_ASCII}
274     * <li>{@link #OPTION_1999}
275     * <li>{@link #OPTION_2004}
276     * <li>{@link #OPTION_2006}
277     * <li>{@link #OPTION_1_28}
278     * <li>{@link #OPTION_1_65}
279     * <li>{@link #OPTION_FORCE_BINARY}
280     * </ul>
281     *
282     * @return command station options value
283     *
284     */
285    public int getCommandOptions() {
286        return commandOptions;
287    }
288
289    /**
290     * Default when a NCE USB isn't selected in user system preferences. Also
291     * the case when Serial or Simulator is selected.
292     */
293    public static final int USB_SYSTEM_NONE = 0;
294
295    /**
296     * Create commands compatible with a NCE USB connected to a PowerCab.
297     */
298    public static final int USB_SYSTEM_POWERCAB = 1;
299
300    /**
301     * Create commands compatible with a NCE USB connected to a Smart Booster.
302     */
303    public static final int USB_SYSTEM_SB3 = 2;
304
305    /**
306     * Create commands compatible with a NCE USB connected to a PowerPro.
307     */
308    public static final int USB_SYSTEM_POWERPRO = 3;
309
310    /**
311     * Create commands compatible with a NCE USB with {@literal >=7.*} connected
312     * to a Twin.
313     */
314    public static final int USB_SYSTEM_TWIN = 4;
315
316    /**
317     * Create commands compatible with a NCE USB with SB5.
318     */
319    public static final int USB_SYSTEM_SB5 = 5;
320
321    private int usbSystem = USB_SYSTEM_NONE;
322    private boolean usbSystemSet = false;
323
324    /**
325     * Set the type of system the NCE USB is connected to
326     * <ul>
327     * <li>{@link #USB_SYSTEM_NONE}
328     * <li>{@link #USB_SYSTEM_POWERCAB}
329     * <li>{@link #USB_SYSTEM_SB3}
330     * <li>{@link #USB_SYSTEM_POWERPRO}
331     * <li>{@link #USB_SYSTEM_TWIN}
332     * <li>{@link #USB_SYSTEM_SB5}
333     * </ul>
334     *
335     * @param val usb command station options
336     *
337     */
338    public void setUsbSystem(int val) {
339        usbSystem = val;
340        if (usbSystemSet) {
341            log.warn("setUsbSystem called more than once");
342        }
343        usbSystemSet = true;
344    }
345
346    /**
347     * Get the type of system the NCE USB is connected to
348     * <ul>
349     * <li>{@link #USB_SYSTEM_NONE}
350     * <li>{@link #USB_SYSTEM_POWERCAB}
351     * <li>{@link #USB_SYSTEM_SB3}
352     * <li>{@link #USB_SYSTEM_POWERPRO}
353     * <li>{@link #USB_SYSTEM_TWIN}
354     * <li>{@link #USB_SYSTEM_SB5}
355     * </ul>
356     *
357     * @return usb command station options
358     *
359     */
360    public int getUsbSystem() {
361        return usbSystem;
362    }
363
364    /**
365     * Initializer for supported command groups
366     */
367    static public final long CMDS_NONE = 0;
368
369    /**
370     * Limit max accy decoder to addr 250
371     */
372    static public final long CMDS_ACCYADDR250 = 0x0001;
373
374    /**
375     * Supports programming track and related commands
376     */
377    static public final long CMDS_PROGTRACK = 0x0002;
378
379    /**
380     * Supports read AIU status commands {@code 0x9B}
381     */
382    static public final long CMDS_AUI_READ = 0x004;
383
384    /**
385     * Supports USB read/write memory commands {@code 0xB3 -> 0xB5}
386     */
387    static public final long CMDS_MEM = 0x0008;
388
389    /**
390     * Support Ops Mode Pgm commands {@code 0xAE -> 0xAF}
391     */
392    static public final long CMDS_OPS_PGM = 0x0010;
393
394    /**
395     * Support Clock commands {@code 0x82 -> 0x87}
396     */
397    static public final long CMDS_CLOCK = 0x0020;
398
399    /**
400     * Support USB Interface commands {@code 0xB1}
401     */
402    static public final long CMDS_USB = 0x0040;
403
404    /**
405     * Disable for USB commands
406     */
407    static public final long CMDS_NOT_USB = 0x0080;
408
409    /**
410     * All Connections Support commands
411     */
412    static public final long CMDS_ALL_SYS = 0x0100;
413
414    private long cmdGroups = CMDS_NONE;
415    private boolean cmdGroupsSet = false;
416
417    /**
418     * Set the types of commands valid connected system
419     * <ul>
420     * <li>{@link #CMDS_NONE}
421     * <li>{@link #CMDS_ACCYADDR250}
422     * <li>{@link #CMDS_PROGTRACK}
423     * <li>{@link #CMDS_AUI_READ}
424     * <li>{@link #CMDS_MEM}
425     * <li>{@link #CMDS_OPS_PGM}
426     * <li>{@link #CMDS_CLOCK}
427     * <li>{@link #CMDS_USB}
428     * <li>{@link #CMDS_NOT_USB}
429     * <li>{@link #CMDS_ALL_SYS}
430     * </ul>
431     *
432     * @param val command group supported options
433     *
434     */
435    public void setCmdGroups(long val) {
436        cmdGroups = val;
437        if (cmdGroupsSet) {
438            log.warn("setCmdGroups called more than once");
439        }
440        cmdGroupsSet = true;
441    }
442
443    /**
444     * Get the types of commands valid for the NCE USB and connected system
445     * <ul>
446     * <li>{@link #CMDS_NONE}
447     * <li>{@link #CMDS_ACCYADDR250}
448     * <li>{@link #CMDS_PROGTRACK}
449     * <li>{@link #CMDS_AUI_READ}
450     * <li>{@link #CMDS_MEM}
451     * <li>{@link #CMDS_OPS_PGM}
452     * <li>{@link #CMDS_CLOCK}
453     * <li>{@link #CMDS_USB}
454     * <li>{@link #CMDS_NOT_USB}
455     * <li>{@link #CMDS_ALL_SYS}
456     * </ul>
457     *
458     * @return command group supported options
459     *
460     */
461    public long getCmdGroups() {
462        return cmdGroups;
463    }
464
465    private boolean nceProgMode = false;     // Do not use exit program mode unless active
466
467    /**
468     * Gets the state of the command station
469     *
470     * @return true if in programming mode
471     */
472    public boolean getNceProgMode() {
473        return nceProgMode;
474    }
475
476    /**
477     * Sets the state of the command station
478     *
479     * @param b when true, set programming mode
480     */
481    public void setNceProgMode(boolean b) {
482        nceProgMode = b;
483    }
484
485    /**
486     * Check NCE EPROM and start NCE CS accessory memory poll
487     */
488    @Override
489    protected AbstractMRMessage pollMessage() {
490
491        // Check to see if command options are valid
492        if (commandOptionSet == false) {
493            if (log.isDebugEnabled()) {
494                log.debug("Command options are not valid yet!!");
495            }
496            return null;
497        }
498
499        // Keep checking the state of the communication link by polling
500        // the command station using the EPROM checker
501        NceMessage m = pollEprom.nceEpromPoll();
502        if (m != null) {
503            expectReplyEprom = true;
504            return m;
505        } else {
506            expectReplyEprom = false;
507        }
508
509        // Have we checked to see if AIU broadcasts are enabled?
510        if (pollAiuStatus == null) {
511            // No, do it this time
512            pollAiuStatus = new NceAIUChecker(this);
513            return pollAiuStatus.nceAiuPoll();
514        }
515
516        // Start NCE memory poll for accessory states
517        if (pollHandler == null) {
518            pollHandler = new NceTurnoutMonitor(this);
519        }
520
521        // minimize impact to NCE CS
522        mWaitBeforePoll = NceTurnoutMonitor.POLL_TIME; // default = 25
523
524        return pollHandler.pollMessage();
525
526    }
527
528    NceConnectionStatus pollEprom = new NceConnectionStatus(this);
529    NceAIUChecker pollAiuStatus = null;
530    NceTurnoutMonitor pollHandler = null;
531
532    boolean expectReplyEprom = false;
533
534    @Override
535    protected AbstractMRListener pollReplyHandler() {
536        // First time through, handle reply by checking EPROM revision
537        // Second time through, handle AIU broadcast check
538        if (expectReplyEprom) {
539            return pollEprom;
540        } else if (pollHandler == null) {
541            return pollAiuStatus;
542        } else {
543            return pollHandler;
544        }
545    }
546
547    /**
548     * Forward a preformatted message to the actual interface.
549     */
550    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value = "SLF4J_FORMAT_SHOULD_BE_CONST",
551        justification = "passing exception text")
552    @Override
553    public void sendNceMessage(NceMessage m, NceListener reply) {
554        try {
555            NceMessageCheck.checkMessage(getAdapterMemo(), m);
556        } catch (JmriException e) {
557            log.error(e.getMessage(), e);
558            return;  // don't send bogus message to interface
559        }
560        sendMessage(m, reply);
561    }
562
563    @Override
564    protected void forwardToPort(AbstractMRMessage m, AbstractMRListener reply) {
565        replyBinary = m.isBinary();
566        replyLen = ((NceMessage) m).getReplyLen();
567        super.forwardToPort(m, reply);
568    }
569
570    protected int replyLen;
571    protected boolean replyBinary;
572    protected boolean unsolicitedSensorMessageSeen = false;
573
574    @Override
575    protected AbstractMRMessage enterProgMode() {
576        return NceMessage.getProgMode(this);
577    }
578
579    @Override
580    protected AbstractMRMessage enterNormalMode() {
581        return NceMessage.getExitProgMode(this);
582    }
583
584    /**
585     *
586     * @param adaptermemo the SystemConnectionMemo to associate with this
587     *                    TrafficController
588     */
589    public void setAdapterMemo(NceSystemConnectionMemo adaptermemo) {
590        memo = adaptermemo;
591    }
592
593    public NceSystemConnectionMemo getAdapterMemo() {
594        return memo;
595    }
596
597    private NceSystemConnectionMemo memo = null;
598
599    @Override
600    protected AbstractMRReply newReply() {
601        NceReply reply = new NceReply(this);
602        reply.setBinary(replyBinary);
603        return reply;
604    }
605
606    // pre 2006 EPROMs can't stop AIU broadcasts so we have to accept them
607    @Override
608    protected boolean canReceive() {
609        if (getCommandOptions() < OPTION_2006) {
610            return true;
611        } else if (replyLen > 0) {
612            return true;
613        } else {
614            if (log.isDebugEnabled()) {
615                log.error("unsolicited character received");
616            }
617            return false;
618        }
619    }
620
621    @Override
622    protected boolean endOfMessage(AbstractMRReply msg) {
623        msg.setBinary(replyBinary);
624        // first try boolean
625        if (replyBinary) {
626            // Attempt to detect and correctly forward AIU broadcast from pre
627            // 2006 EPROMS. We'll check for three byte unsolicited message
628            // starting with "A" 0x61. The second byte contains the AIU number +
629            // 0x30. The third byte contains the sensors, 0x41 < s < 0x6F
630            // This code is problematic, it is data sensitive.
631            // We can also incorrectly forward an AIU broadcast to a routine
632            // that is waiting for a reply
633            if (replyLen == 0 && getCommandOptions() < OPTION_2006) {
634                if (msg.getNumDataElements() == 1 && msg.getElement(0) == 0x61) {
635                    return false;
636                }
637                if (msg.getNumDataElements() == 2 && msg.getElement(0) == 0x61
638                        && msg.getElement(1) >= 0x30) {
639                    return false;
640                }
641                if (msg.getNumDataElements() == 3 && msg.getElement(0) == 0x61
642                        && msg.getElement(1) >= 0x30
643                        && msg.getElement(2) >= 0x41
644                        && msg.getElement(2) <= 0x6F) {
645                    return true;
646                }
647            }
648            if (msg.getNumDataElements() >= replyLen) {
649                // reset reply length so we can detect an unsolicited AIU message
650                replyLen = 0;
651                return true;
652            } else {
653                return false;
654            }
655        } else {
656            // detect that the reply buffer ends with "COMMAND: " (note ending
657            // space)
658            int num = msg.getNumDataElements();
659            // ptr is offset of last element in NceReply
660            int ptr = num - 1;
661            if ((num >= 9)
662                    && (msg.getElement(ptr) == ' ')
663                    && (msg.getElement(ptr - 1) == ':')
664                    && (msg.getElement(ptr - 2) == 'D')) {
665                return true;
666            } // this got harder with the new PROM at the beginning of 2005.
667            // It doesn't always send the "COMMAND: " prompt at the end
668            // of each response. Try for the error message:
669            else if ((num >= 19)
670                    && // don't check space,NL at end of buffer
671                    (msg.getElement(ptr - 2) == '*')
672                    && (msg.getElement(ptr - 3) == '*')
673                    && (msg.getElement(ptr - 4) == '*')
674                    && (msg.getElement(ptr - 5) == '*')
675                    && (msg.getElement(ptr - 6) == ' ')
676                    && (msg.getElement(ptr - 7) == 'D')
677                    && (msg.getElement(ptr - 8) == 'O')
678                    && (msg.getElement(ptr - 9) == 'O')
679                    && (msg.getElement(ptr - 10) == 'T')
680                    && (msg.getElement(ptr - 11) == 'S')
681                    && (msg.getElement(ptr - 12) == 'R')) {
682                return true;
683            }
684
685            // otherwise, it's not the end
686            return false;
687        }
688    }
689
690    @Override
691    public String getUserName() {
692        if (memo == null) {
693            return "NCE";
694        }
695        return memo.getUserName();
696    }
697
698    @Override
699    public String getSystemPrefix() {
700        if (memo == null) {
701            return "N";
702        }
703        return memo.getSystemPrefix();
704    }
705    
706    /*
707     * the command station memory object
708     */
709    public NceCmdStationMemory csm;
710    
711    private final static Logger log = LoggerFactory.getLogger(NceTrafficController.class);
712
713}