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