001package jmri.jmrix.zimo;
002
003import org.slf4j.Logger;
004import org.slf4j.LoggerFactory;
005
006/**
007 * Represents a single command or response to the Zimo Binary Protocol.
008 * <p>
009 * Content is represented with ints to avoid the problems with sign-extension
010 * that bytes have, and because a Java char is actually a variable number of
011 * bytes in Unicode.
012 *
013 * @author Kevin Dickerson Copyright (C) 2014
014 *
015 * Adapted by Sip Bosch for use with zimo MX-1
016 *
017 */
018public class Mx1Message extends jmri.jmrix.NetMessage {
019
020    public Mx1Message(int len) {
021        this(len, Mx1Packetizer.ASCII);
022    }
023
024    /**
025     * Create a new object, representing a specific-length message.
026     *
027     * @param len      Total bytes in message, including opcode and
028     *                 error-detection byte.
029     * @param protocol one of {@link Mx1Packetizer#ASCII} or
030     *                 {@link Mx1Packetizer#BINARY}
031     */
032    public Mx1Message(int len, boolean protocol) {
033        super(len);
034        this.protocol = protocol;
035        if (!protocol) {
036            if (len > 15 || len < 0) {
037                log.error("Invalid length in ctor: {}", len);
038            }
039        }
040    }
041
042    boolean protocol = Mx1Packetizer.ASCII;
043
044    //Version 5 and above allow for a message size greater than 15 bytes
045    public Mx1Message(Integer[] contents) {
046        super(contents.length);
047        protocol = Mx1Packetizer.BINARY;
048        for (int i = 0; i < contents.length; i++) {
049            this.setElement(i, contents[i]);
050        }
051    }
052
053    //Version 5 and above allow for a message size greater than 15 bytes
054    public Mx1Message(byte[] contents) {
055        super(contents.length);
056        protocol = Mx1Packetizer.BINARY;
057        for (int i = 0; i < contents.length; i++) {
058            this.setElement(i, (contents[i] & 0xff));
059        }
060    }
061
062    public boolean getLongMessage() {
063        if (protocol == Mx1Packetizer.BINARY) {
064            if ((getElement(1) & 0x80) == 0x80) {
065                return true;
066            }
067        }
068        return false;
069    }
070
071    final static int PRIMARY = 0x00;
072    final static int ACKREP1 = 0x40;
073    final static int REPLY2 = 0x20;
074    final static int ACK2 = 0x60;
075
076    /**
077     * Indicates where the message is to/from in the header byte.
078     * <p>
079     * Up to JMRI 4.3.5, this was doing {@code ((mod & MX1) == MX1)} for the
080     * first test, which is really 0 == 0 and always true. At that point it was
081     * changed to just check the bottom two bits.
082     *
083     * @return one of {@link #MX1}, {@link #MX8}, {@link #MX9} or 0x0F
084     */
085    public int getModule() {
086        int mod = getElement(1) & 0x0F;
087        if ((mod & 0x03) == MX1) {
088            return MX1;
089        }
090        if ((mod & 0x03) == MX8) {
091            return MX8;
092        }
093        if ((mod & 0x03) == MX9) {
094            return MX9;
095        }
096        return mod;
097    }
098
099    public int getMessageType() {
100        return getElement(1) & 0x60;
101    }
102
103    public int getPrimaryMessage() {
104        return getElement(2);
105    }
106
107    /**
108     * Message to/from Command Station MX1
109     */
110    static final int MX1 = 0x00;
111
112    /**
113     * Message to/from Accessory module MX8
114     */
115    static final int MX8 = 0x01;
116
117    /**
118     * Message to/from Track Section module MX9
119     */
120    static final int MX9 = 0x02;
121
122    /**
123     * Indicates the message source is a command station. {@value #CS}
124     *
125     * @see #messageSource()
126     */
127    final static boolean CS = true;
128    /**
129     * Indicates the message source is a command station. {@value #PC}
130     *
131     * @see #messageSource()
132     */
133    final static boolean PC = false;
134
135    /**
136     * Indicates the source of the message.
137     *
138     * @return {@link #PC} or {@link #CS}
139     */
140    public boolean messageSource() {
141        if ((getElement(0) & 0x08) == 0x08) {
142            return PC;
143        }
144        return CS;
145    }
146
147    long timeStamp = 0L;
148
149    protected long getTimeStamp() {
150        return timeStamp;
151    }
152
153    protected void setTimeStamp(long ts) {
154        timeStamp = ts;
155    }
156
157    int retries = 3;
158
159    public int getRetry() {
160        return retries;
161    }
162
163    public void setRetries(int i) {
164        retries = i;
165    }
166
167    //byte sequenceNo = 0x00;
168    public boolean replyL1Expected() {
169        return true;
170    }
171
172    byte[] rawPacket;
173
174    public void setRawPacket(byte[] b) {
175        rawPacket = b;
176    }
177
178    protected byte[] getRawPacket() {
179        return rawPacket;
180    }
181
182    public void setSequenceNo(byte s) {
183        setElement(0, (s & 0xff));
184    }
185
186    public int getSequenceNo() {
187        return (getElement(0) & 0xff);
188    }
189
190    boolean crcError = false;
191
192    public void setCRCError() {
193        crcError = true;
194    }
195
196    public boolean isCRCError() {
197        return crcError;
198    }
199
200    /**
201     * Check if the message has a valid parity (actually check for CR or LF as
202     * end of message).
203     *
204     * @return true if message has correct parity bit
205     */
206    @Override
207    public boolean checkParity() {
208        //javax.swing.JOptionPane.showMessageDialog(null, "A-Programma komt tot hier!");
209        int len = getNumDataElements();
210        return (getElement(len - 1) == (0x0D | 0x0A));
211    }
212    // programma komt hier volgens mij nooit
213    // in fact set CR as end of message
214
215    @Override
216    public void setParity() {
217        javax.swing.JOptionPane.showMessageDialog(null, "B-Programma komt tot hier!");
218        int len = getNumDataElements();
219        setElement(len - 1, 0x0D);
220    }
221
222    // decode messages of a particular form
223    public String getStringMsg() {
224        StringBuilder txt = new StringBuilder();
225        if (protocol == Mx1Packetizer.BINARY) {
226            if (isCRCError()) {
227                txt.append(" === CRC ERROR === ");
228            }
229            if (getNumDataElements() <= 3) {
230                txt.append("Short Packet ");
231                return txt.toString();
232            }
233            if ((getElement(1) & 0x10) == 0x10) {
234                txt.append("From PC");
235            } else {
236                txt.append("From CS");
237            }
238            txt.append(" Seq ").append(getElement(0) & 0xff);
239            if (getLongMessage()) {
240                txt.append(" (L)");
241            } else {
242                txt.append(" (S)");
243            }
244            int offset;
245            switch (getMessageType()) {
246                case PRIMARY:
247                    txt.append(" Prim");
248                    break;
249                case ACKREP1:
250                    txt.append(" Ack/Reply 1");
251                    break;
252                case REPLY2:
253                    txt.append(" Reply 2");
254                    break;
255                case ACK2:
256                    txt.append(" Ack 2");
257                    break;
258                default:
259                    txt.append(" Unknown msg");
260                    break;
261            }
262            if (getModule() == MX1) {  //was (getElement(1)&0x00) == 0x00
263                txt.append(" to/from CS (MX1)");
264                switch (getPrimaryMessage()) {  //was getElement(2)
265                    case TRACKCTL:
266                        offset = 0;
267                        if (getMessageType() == ACKREP1) {
268                            offset++;
269                        }
270                        txt.append(" Track Control ");
271                        if ((getElement(3 + offset) & 0x03) == 0x03) {
272                            txt.append(" Query Track Status ");
273                        } else if ((getElement(3 + offset) & 0x01) == 0x01) {
274                            txt.append(" Turn Track Off ");
275                        } else if ((getElement(3 + offset) & 0x02) == 0x02) {
276                            txt.append(" Turn Track On ");
277                        } else {
278                            txt.append(" Stop All Locos ");
279                        }
280                        break;
281                    case 3:
282                        txt.append(" Loco Control : ");
283                        if (getMessageType() == PRIMARY) {
284                            txt.append(getLocoAddress(getElement((3)), getElement(4)));
285                            txt.append(((getElement(6) & 0x20) == 0x20) ? " Fwd " : " Rev ");
286                            txt.append(((getElement(6) & 0x10) == 0x10) ? " F0: On " : " F0: Off ");
287                            txt.append(decodeFunctionStates(getElement(7), getElement(8)));
288                        }
289                        break;
290                    case 4:
291                        txt.append(" Loco Funct ");
292                        break;
293                    case 5:
294                        txt.append(" Loco Acc/Dec ");
295                        break;
296                    case 6:
297                        txt.append(" Shuttle ");
298                        break;
299                    case 7:
300                        txt.append(" Accessory ");
301                        if (getMessageType() == PRIMARY) {
302                            txt.append(getLocoAddress(getElement((3)), getElement(4)));
303                            txt.append(((getElement(5) & 0x04) == 0x04) ? " Thrown " : " Closed ");
304                        }
305                        break;
306                    case 8:
307                        txt.append(" Loco Status ");
308                        break;
309                    case 9:
310                        txt.append(" Acc Status ");
311                        break;
312                    case 10:
313                        txt.append(" Address Control ");
314                        break;
315                    case 11:
316                        txt.append(" CS State ");
317                        break;
318                    case 12:
319                        txt.append(" Read/Write CS CV ");
320                        break;
321                    case 13:
322                        txt.append(" CS Equip Query ");
323                        break;
324                    case 17:
325                        txt.append(" Tool Type ");
326                        break;
327                    case PROGCMD:
328                        offset = 0;
329                        if (getMessageType() == ACKREP1) {
330                            txt.append(" Prog CV ");
331                            break;
332                        }
333                        if (getMessageType() == REPLY2) {
334                            offset++;
335                        }
336                        if (getMessageType() == ACK2) {
337                            txt.append("Ack to CS Message");
338                            break;
339                        }
340                        if (getNumDataElements() == 7 && getMessageType() == ACKREP1) {
341                            txt.append(" Error Occured ");
342                            txt.append(getErrorCode(getElement(6)));
343                            txt.append(" Loco: ").append(getLocoAddress(getElement((3 + offset)), getElement(4 + offset)));
344                            break;
345                        }
346                        /*if(getNumDataElements()<7){
347                         txt.append(" Ack L1 ");
348                         break;
349                         }*/
350                        if ((getMessageType() == PRIMARY && getNumDataElements() == 8)) {
351                            txt.append(" Write CV ");
352                        } else {
353                            txt.append(" Read CV ");
354                        }
355                        txt.append("Loco: ").append(getLocoAddress(getElement((3 + offset)), getElement(4 + offset)));
356                        if ((getElement(3 + offset) & 0x80) == 0x80) {
357                            txt.append(" DCC");
358                        }
359                        int cv = (((getElement(5 + offset) & 0xff) << 8) + (getElement(6 + offset) & 0xff));
360                        txt.append(" CV: ").append(cv);
361                        if (getNumDataElements() >= (8 + offset)) {  //Version 61.26 and later includes an extra error bit at the end of the packet
362                            txt.append(" Set To: ").append(getElement(7 + offset) & 0xff);
363                        }
364                        break;
365                    case 254:
366                        txt.append(" Cur Acc Memory ");
367                        break;
368                    case 255:
369                        txt.append(" Cur Loco Memory ");
370                        break;
371                    default:
372                        txt.append(" Unknown ");
373                }
374            } else if ((getElement(1) & 0x01) == 0x01) {
375                txt.append(" to/from Accessory Mod (MX8)");
376            } else if ((getElement(1) & 0x02) == 0x02) {
377                txt.append(" to/from Track Section (MX9)");
378            } else {
379                txt.append(" unknown");
380            }
381        }
382        //int type = getElement(2);
383
384        return txt.toString();
385    }
386
387    private String decodeFunctionStates(int cData2, int cData3) {
388        StringBuilder txt = new StringBuilder();
389
390        txt.append(((cData2 & 0x1) == 0x1) ? " F1: On " : " F1: Off ");
391        txt.append(((cData2 & 0x2) == 0x2) ? " F2: On " : " F2: Off ");
392        txt.append(((cData2 & 0x4) == 0x4) ? " F3: On " : " F3: Off ");
393        txt.append(((cData2 & 0x8) == 0x8) ? " F4: On " : " F4: Off ");
394        txt.append(((cData2 & 0x10) == 0x10) ? " F5: On " : " F5: Off ");
395        txt.append(((cData2 & 0x20) == 0x20) ? " F6: On " : " F6: Off ");
396        txt.append(((cData2 & 0x40) == 0x40) ? " F7: On " : " F7: Off ");
397        txt.append(((cData2 & 0x80) == 0x80) ? " F8: On " : " F8: Off ");
398
399        txt.append(((cData3 & 0x1) == 0x1) ? " F9: On " : " F9: Off ");
400        txt.append(((cData3 & 0x2) == 0x2) ? " F10: On " : " F10: Off ");
401        txt.append(((cData3 & 0x4) == 0x4) ? " F11: On " : " F11: Off ");
402        txt.append(((cData3 & 0x8) == 0x8) ? " F12: On " : " F12: Off ");
403
404        return txt.toString();
405    }
406
407    public int getLocoAddress() {
408        int offset = 0;
409        if (getMessageType() == REPLY2) {
410            offset++;
411        } else if (getMessageType() == ACKREP1) {
412            offset = +2;
413        }
414        if (getNumDataElements() == (4 + offset)) {
415            return getLocoAddress(getElement(3 + offset), getElement(4 + offset));
416        }
417        return -1;
418    }
419
420    public int getCvValue() {
421        int offset = 0;
422        if (getMessageType() == REPLY2) {
423            offset++;
424        } else if (getMessageType() == ACKREP1) {
425            offset = +2;
426        }
427        if (getNumDataElements() >= (8 + offset)) { //Version 61.26 and later includes an extra error bit at the end of the packet
428            return (getElement(7 + offset) & 0xff);
429        }
430        return -1;
431    }
432
433    int getLocoAddress(int hi, int lo) {
434        hi = hi & 0x3F;
435        if (hi == 0) {
436            return lo;
437        } else {
438            hi = (((hi & 255) - 192) << 8);
439            hi = hi + (lo & 255);
440            return hi;
441        }
442    }
443
444    String getErrorCode(int i) {
445        switch (i) {
446            case NO_ERROR:
447                return "No Error";
448            case ERR_ADDRESS:
449                return "Invalid Address";
450            case ERR_INDEX:
451                return "Invalid Index";
452            case ERR_FORWARD:
453                return "Could not be Forwarded";
454            case ERR_BUSY:
455                return "CMD Busy";
456            case ERR_NO_MOT:
457                return "Motorola Jump Off";
458            case ERR_NO_DCC:
459                return "DCC Jump Off";
460            case ERR_CV_ADDRESS:
461                return "Invalid CV";
462            case ERR_SECTION:
463                return "Invalid Section";
464            case ERR_NO_MODUL:
465                return "No Module Found";
466            case ERR_MESSAGE:
467                return "Error in Message";
468            case ERR_SPEED:
469                return "Invalid Speed";
470            default:
471                return "Unknown Error";
472        }
473    }
474
475    final static int NO_ERROR = 0x00;
476    final static int ERR_ADDRESS = 0x01;
477    final static int ERR_INDEX = 0x02;
478    final static int ERR_FORWARD = 0x03;
479    final static int ERR_BUSY = 0x04;
480    final static int ERR_NO_MOT = 0x05;
481    final static int ERR_NO_DCC = 0x06;
482    final static int ERR_CV_ADDRESS = 0x07;
483    final static int ERR_SECTION = 0x08;
484    final static int ERR_NO_MODUL = 0x09;
485    final static int ERR_MESSAGE = 0x0a;
486    final static int ERR_SPEED = 0x0b;
487
488    final static int TRACKCTL = 0x02;
489    final static int PROGCMD = 0x13;
490    final static int LOCOCMD = 0x03;
491    final static int ACCCMD = 0x07;
492
493    static public Mx1Message getCmdStnDetails() {
494        Mx1Message m = new Mx1Message(4);
495        m.setElement(1, 0x10);
496        m.setElement(2, 0x13);
497        m.setElement(3, 0x00);
498        return m;
499    }
500
501    /**
502     * Set Track Power Off/Emergency Stop
503     *
504     * @return MrcMessage
505     */
506    static public Mx1Message setPowerOff() {
507        Mx1Message m = new Mx1Message(4, Mx1Packetizer.BINARY);
508        m.setElement(1, 0x10); // PC control short message
509
510        m.setElement(2, TRACKCTL);
511        m.setElement(3, 0x01);
512        return m;
513    }
514
515    static public Mx1Message setPowerOn() {
516        Mx1Message m = new Mx1Message(4, Mx1Packetizer.BINARY);
517        m.setElement(1, 0x10); // PC control short message
518
519        m.setElement(2, TRACKCTL);
520        m.setElement(3, 0x02);
521        return m;
522    }
523
524    static public Mx1Message getTrackStatus() {
525        Mx1Message m = new Mx1Message(4, Mx1Packetizer.BINARY);
526        m.setElement(1, 0x10); // PC control short message
527
528        m.setElement(2, TRACKCTL);
529        m.setElement(3, 0x03);
530        return m;
531    }
532
533    /**
534     * Create a message to read or write a CV.
535     *
536     * @param locoAddress address of the loco
537     * @param cv          CV to read or write
538     * @param value       value to write to CV, if -1 CV is read
539     * @param dcc         true if decoder is DCC; false if decoder is Motorola
540     * @return a message to read or write a CV
541     */
542    // javadoc did indicate locoAddress could be blank to use programming track, but that's not possible with an int
543    static public Mx1Message getDecProgCmd(int locoAddress, int cv, int value, boolean dcc) {
544        Mx1Message m;
545        if (value == -1) {
546            m = new Mx1Message(7, Mx1Packetizer.BINARY);
547        } else {
548            m = new Mx1Message(8, Mx1Packetizer.BINARY);
549        }
550        m.setElement(0, 0x00);
551        m.setElement(1, 0x10); // PC control short message
552
553        m.setElement(2, PROGCMD);
554        int locoHi = locoAddress >> 8;
555        if (dcc) {
556            locoHi = locoHi + 128;
557        } else {
558            locoHi = locoHi + 64;
559        }
560        m.setElement(3, (locoHi));
561        m.setElement(4, (locoAddress & 0xff));
562        m.setElement(5, cv >> 8);
563        m.setElement(6, cv & 0xff);
564        if (value != -1) {
565            m.setElement(7, value);
566        }
567        return m;
568    }
569
570    /**
571     * Create a locomotive control message.
572     *
573     * @param locoAddress address of the loco
574     * @param speed       Speed Step in the actual Speed Step System
575     * @param dcc         true if decoder is DCC; false if decoder is Motorola
576     * @param cData1      ???
577     * @param cData2      functions output 0-7
578     * @param cData3      functions output 9-12
579     * @return message controlling a locomotive
580     */
581    static public Mx1Message getLocoControl(int locoAddress, int speed, boolean dcc, int cData1, int cData2, int cData3) {
582        Mx1Message m = new Mx1Message(9, Mx1Packetizer.BINARY);
583        m.setElement(0, 0x00);
584        m.setElement(1, 0x10); // PC control short message
585
586        m.setElement(2, LOCOCMD);
587        //High add 80 to indicate DCC
588        int locoHi = locoAddress >> 8;
589        if (dcc) {
590            locoHi = locoHi + 128;
591        } else {
592            locoHi = locoHi + 64;
593        }
594        m.setElement(3, (locoHi));
595        m.setElement(4, (locoAddress & 0xff));
596        m.setElement(5, speed);
597        m.setElement(6, cData1);
598        m.setElement(7, cData2);
599        m.setElement(8, cData3);
600        return m;
601    }
602
603    static public Mx1Message getSwitchMsg(int accAddress, int setting, boolean dcc) {
604        Mx1Message m = new Mx1Message(6, Mx1Packetizer.BINARY);
605        m.setElement(0, 0x00);
606        m.setElement(1, 0x10); // PC control short message
607
608        m.setElement(2, ACCCMD);
609        //High add 80 to indicate DCC
610        int accHi = accAddress >> 8;
611        if (dcc) {
612            accHi = accHi + 128;
613        } else {
614            accHi = accHi + 64;
615        }
616        m.setElement(3, (accHi));
617        m.setElement(4, (accAddress & 0xff));
618        m.setElement(5, 0x00);
619        if (setting == jmri.Turnout.THROWN) {
620            m.setElement(5, 0x04);
621        }
622        return m;
623    }
624    // initialize logging
625    private final static Logger log = LoggerFactory.getLogger(Mx1Message.class);
626
627}