001package jmri.jmrix.roco.z21;
002
003import jmri.jmrix.AbstractMRReply;
004import jmri.DccLocoAddress;
005import org.reflections.Reflections;
006import org.slf4j.Logger;
007import org.slf4j.LoggerFactory;
008
009import java.lang.reflect.Constructor;
010import java.lang.reflect.InvocationTargetException;
011import java.util.ArrayList;
012import java.util.List;
013import java.util.Set;
014
015/**
016 * Class for replies in the z21/Z21 protocol.
017 * <p>
018 * Replies are of the format: 2 bytes length 2 bytes opcode n bytes data
019 * <p>
020 * numeric data is sent in little endian format.
021 *
022 * @author Bob Jacobsen Copyright (C) 2003
023 * @author Paul Bender Copyright (C) 2014
024 */
025public class Z21Reply extends AbstractMRReply {
026
027    private static final String WRONG_REPLY_TYPE = "Wrong Reply Type";
028
029    /**
030     *  Create a new one.
031     */
032    public Z21Reply() {
033        super();
034        setBinary(true);
035    }
036
037    /**
038     * This ctor interprets the byte array as a sequence of characters to send.
039     *
040     * @param a Array of bytes to send.
041     * @param l length of reply.
042     */
043    public Z21Reply(byte[] a, int l) {
044        super();
045        _nDataChars = l;
046        setBinary(true);
047        for (int i = 0; i < _nDataChars; i++) {
048            _dataChars[i] = a[i];
049        }
050    }
051
052    // keep track of length
053    @Override
054    public void setElement(int n, int v) {
055        _dataChars[n] = (char) v;
056        _nDataChars = Math.max(_nDataChars, n + 1);
057    }
058
059    /**
060     * Get an integer representation of a BCD value.
061     *
062     * @param n byte in message to convert
063     * @return Integer value of BCD byte.
064     */
065    public Integer getElementBCD(int n) {
066        return Integer.decode(Integer.toHexString(getElement(n)));
067    }
068
069    @Override
070    public void setOpCode(int i) {
071        _dataChars[2] = (char) (i & 0x00ff);
072        _dataChars[3] = (char) ((i & 0xff00) >> 8);
073        _nDataChars = Math.max(_nDataChars, 4);  //smallest reply is of length 4.
074    }
075
076    @Override
077    public int getOpCode() {
078        return (0xff&_dataChars[2]) + ((0xff&_dataChars[3]) << 8);
079    }
080
081    public void setLength(int i) {
082        _dataChars[0] = (char) (i & 0x00ff);
083        _dataChars[1] = (char) ((i & 0xff00) >> 8);
084        _nDataChars = Math.max(_nDataChars, i);
085    }
086
087    public int getLength() {
088        return (0xff & _dataChars[0] ) + ((0xff & _dataChars[1]) << 8);
089    }
090
091    @Override
092    protected int skipPrefix(int index) {
093        return 0;
094    }
095
096    private static List<Z21MessageFormatter> formatterList = new ArrayList<>();
097
098    @Override
099    public String toMonitorString() {
100        if(formatterList.isEmpty()) {
101            try {
102                Reflections reflections = new Reflections("jmri.jmrix.roco.z21.messageformatters");
103                Set<Class<? extends Z21MessageFormatter>> f = reflections.getSubTypesOf(Z21MessageFormatter.class);
104                for (Class<?> c : f) {
105                    log.debug("Found formatter: {}", f.getClass().getName());
106                    Constructor<?> ctor = c.getConstructor();
107                    formatterList.add((Z21MessageFormatter) ctor.newInstance());
108                }
109            } catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException |
110                     IllegalArgumentException | InvocationTargetException e) {
111                log.error("Error instantiating formatter", e);
112            }
113        }
114
115        return formatterList.stream()
116                .filter(f -> f.handlesMessage(this))
117                .findFirst().map(f -> f.formatMessage(this))
118                .orElse(this.toString());
119    }
120
121    // handle XpressNet replies tunneled in Z21 messages
122    boolean isXPressNetTunnelMessage() {
123        return (getOpCode() == 0x0040);
124    }
125
126    public Z21XNetReply getXNetReply() {
127        Z21XNetReply xnr = null;
128        if (isXPressNetTunnelMessage()) {
129            int i = 4;
130            xnr = new Z21XNetReply();
131            for (; i < getLength(); i++) {
132                xnr.setElement(i - 4, getElement(i));
133            }
134            if(( xnr.getElement(0) & 0x0F ) > ( xnr.getNumDataElements()+2) ){
135               // there is at least one message from the Z21 that can be sent 
136               // with fewer bytes than the XpressNet payload indicates it
137               // should have.  Pad those messages with 0x00 bytes.
138               for(i=i-4;i<((xnr.getElement(0)&0x0F)+2);i++){
139                  xnr.setElement(i,0x00);
140               }
141            }
142        }
143        return xnr;
144    }
145   
146    // handle RailCom data replies
147    boolean isRailComDataChangedMessage(){
148        return (getOpCode() == 0x0088);
149    }
150
151    /**
152     * @return the number of RailCom entries in this message.
153     *         the returned value is in the 0 to 19 range.
154     */
155    public int getNumRailComDataEntries(){
156        if(!this.isRailComDataChangedMessage()){
157           return 0; // this isn't a RailCom message, so there are no entries.
158        }
159        // if this is a RailCom message, the length field is
160        // then the entries are n=(len-4)/13, per the Z21 protocol 
161        // manual, section 8.1.  Also, 0<=n<=19
162        return ((getLength() - 4)/13);
163    } 
164
165    /**
166     * Get a locomotive address from an entry in a railcom message.
167     *
168     * @param n the entry to get the address from.
169     * @return the locomotive address for the specified entry.
170     */
171    public DccLocoAddress getRailComLocoAddress(int n){
172         int offset = 4+(n*13);  // +4 to get past header
173         int address = Z21MessageUtils.integer16BitFromOffeset(_dataChars,offset);
174         return new DccLocoAddress(address,address>=100);
175    }
176
177    /**
178     * Get the receive counter from an entry in a railcom message.
179     *
180     * @param n the entry to get the address from.
181     * @return the receive counter for the specified entry.
182     */
183    public int getRailComRcvCount(int n){
184         int offset = 6+(n*13); // +6 to get header and address.
185         return ((0xff&getElement(offset+3))<<24) +
186                       ((0xff&(getElement(offset+2))<<16) + 
187                       ((0xff&getElement(offset+1))<<8) + 
188                       (0xff&(getElement(offset))));
189    }
190
191    /**
192     * Get the error counter from an entry in a railcom message.
193     *
194     * @param n the entry to get the address from.
195     * @return the error counter for the specified entry.
196     */
197    public int getRailComErrCount(int n){
198         int offset = 10+(n*13); // +10 to get past header, address,and rcv count.
199         return Z21MessageUtils.integer16BitFromOffeset(_dataChars,offset);
200    }
201
202    /**
203     * Get the speed value from an entry in a railcom message.
204     *
205     * @param n the entry to get the address from.
206     * @return the error counter for the specified entry.
207     */
208    public int getRailComSpeed(int n){
209         int options = getRailComOptions(n);
210         if(((options & 0x01) == 0x01) || ((options & 0x02) == 0x02)) { 
211            int offset = 14+(n*13); //+14 to get past the options, 
212                                    // and everything before the options.
213            return (0xff&(getElement(offset)));
214         } else {
215            return 0;
216         }
217    }
218
219    /**
220     * Get the options value from an entry in a railcom message.
221     *
222     * @param n the entry to get the address from.
223     * @return the options for the specified entry.
224     */
225    public int getRailComOptions(int n){
226         int offset = 13+(n*13); //+13 to get past the header, address, rcv 
227                                 // counter, and reserved byte.
228         return (0xff&(getElement(offset)));
229    }
230
231    /**
232     * Get the Quality of Service value from an entry in a railcom message.
233     *
234     * @param n the entry to get the address from.
235     * @return the Quality of Service value for the specified entry.
236     */
237    public int getRailComQos(int n){
238         if((getRailComOptions(n) & 0x04) == 0x04 ) { 
239            int offset = 15+(n*13); //+15 to get past the speed, 
240                                    // and everything before the speed.
241            return (0xff&(getElement(offset)));
242         } else {
243            return 0; // if the QOS bit isn't set, there is no QOS attribute.
244         }
245    }
246
247    // handle System data replies
248    boolean isSystemDataChangedReply(){
249        return (getOpCode() == 0x0084);
250    }
251
252    private void checkSystemDataChangeReply(){
253        if(!isSystemDataChangedReply()){
254            throw new IllegalArgumentException(WRONG_REPLY_TYPE);
255        }
256    }
257
258    /**
259     * Get the Main Track Current from the SystemStateDataChanged 
260     * message.
261     *
262     * @return the current in mA.
263     */
264    public int getSystemDataMainCurrent(){
265         checkSystemDataChangeReply();
266         int offset = 4; //skip the headers
267         return Z21MessageUtils.integer16BitFromOffeset(_dataChars,offset);
268    }
269
270    /**
271     * Get the Programming Track Current from the SystemStateDataChanged 
272     * message.
273     *
274     * @return the current in mA.
275     */
276    public int getSystemDataProgCurrent(){
277         checkSystemDataChangeReply();
278         int offset = 6; //skip the headers
279         return Z21MessageUtils.integer16BitFromOffeset(_dataChars,offset);
280    }
281
282    /**
283     * Get the Filtered Main Track Current from the SystemStateDataChanged 
284     * message.
285     *
286     * @return the current in mA.
287     */
288    public int getSystemDataFilteredMainCurrent(){
289         checkSystemDataChangeReply();
290         int offset = 8; //skip the headers
291         return Z21MessageUtils.integer16BitFromOffeset(_dataChars,offset);
292    }
293
294    /**
295     * Get the Temperature from the SystemStateDataChanged 
296     * message.
297     *
298     * @return the current in degrees C.
299     */
300    public int getSystemDataTemperature(){
301         checkSystemDataChangeReply();
302         int offset = 10; //skip the headers
303         return Z21MessageUtils.integer16BitFromOffeset(_dataChars,offset);
304    }
305
306    /**
307     * Get the Supply Voltage from the SystemStateDataChanged 
308     * message.
309     *
310     * @return the current in mV.
311     */
312    public int getSystemDataSupplyVoltage(){
313         checkSystemDataChangeReply();
314         int offset = 12; //skip the headers
315         return Z21MessageUtils.integer16BitFromOffeset(_dataChars,offset);
316    }
317
318    /**
319     * Get the VCC (and track) Voltage from the SystemStateDataChanged 
320     * message.
321     *
322     * @return the current in mV.
323     */
324    public int getSystemDataVCCVoltage(){
325         checkSystemDataChangeReply();
326         int offset = 14; //skip the headers
327         return Z21MessageUtils.integer16BitFromOffeset(_dataChars,offset);
328    }
329
330    // handle LocoNet replies tunneled in Z21 messages
331    boolean isLocoNetTunnelMessage() {
332        switch (getOpCode()){
333          case 0xA0: // LAN_LOCONET_Z21_RX
334          case 0xA1: // LAN_LOCONET_Z21_TX
335          case 0xA2: // LAN_LOCONET_FROM_LAN
336             return true;
337          default:
338             return false;
339        }
340    }
341
342    boolean isLocoNetDispatchMessage() {
343       return (getOpCode() == 0xA3);
344    }
345
346    boolean isLocoNetDetectorMessage() {
347       return (getOpCode() == 0xA4);
348    }
349
350    public jmri.jmrix.loconet.LocoNetMessage getLocoNetMessage() {
351        jmri.jmrix.loconet.LocoNetMessage lnr = null;
352        if (isLocoNetTunnelMessage()) {
353            int i = 4;
354            lnr = new jmri.jmrix.loconet.LocoNetMessage(getLength()-4);
355            for (; i < getLength(); i++) {
356                lnr.setElement(i - 4, getElement(i));
357            }
358        }
359        return lnr;
360    }
361
362    // handle RMBus data replies
363    boolean isRMBusDataChangedReply(){
364        return (getOpCode() == 0x0080);
365    }
366
367    // handle CAN Feedback/Railcom replies
368    boolean isCanDetectorMessage() {
369        return (getOpCode() == 0x00C4);
370    }
371
372
373
374    /**
375     * @return the can Detector Message type or -1 if not a can detector message.
376     */
377   public int canDetectorMessageType() {
378        if(isCanDetectorMessage()){
379            return getElement(9) & 0xFF;
380        }
381        return -1;
382   }
383
384    /**
385      * @return true if the reply is for a CAN detector and the type is 0x01
386     */
387   public boolean isCanSensorMessage(){
388        return isCanDetectorMessage() && canDetectorMessageType() == 0x01;
389   }
390
391    /**
392     * @return true if the reply is for a CAN detector and the type is 0x01
393     */
394    public boolean isCanReporterMessage(){
395        int type = canDetectorMessageType();
396        return isCanDetectorMessage() && type >= 0x11 && type<= 0x1f;
397    }
398
399    private static final Logger log = LoggerFactory.getLogger(Z21Reply.class);
400}