001package jmri.jmrix.can.cbus;
002
003import javax.annotation.Nonnull;
004
005import jmri.*;
006import jmri.implementation.AbstractRailComReporter;
007import jmri.jmrix.can.*;
008import jmri.util.ThreadingUtil;
009
010/**
011 * Extend jmri.AbstractRailComReporter for CBUS controls.
012 * <hr>
013 * This file is part of JMRI.
014 * <p>
015 * JMRI is free software; you can redistribute it and/or modify it under the
016 * terms of version 2 of the GNU General Public License as published by the Free
017 * Software Foundation. See the "COPYING" file for a copy of this license.
018 * <p>
019 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY
020 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
021 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
022 * <p>
023 *
024 * CBUS Reporters can accept
025 * 5-byte unique Classic RFID on DDES or ACDAT OPCs,
026 * CANRC522 / CANRCOM DDES OPCs.
027 *
028 * @author Mark Riddoch Copyright (C) 2015
029 * @author Steve Young Copyright (c) 2019, 2020
030 *
031 */
032public class CbusReporter extends AbstractRailComReporter implements CanListener {
033
034    private final int _number;
035    private final CanSystemConnectionMemo _memo;
036
037
038    /**
039     * Should all CbusReporters clear themselves after a timeout?
040     * <p>
041     * Default behavior is to not timeout; this is public access
042     * so it can be updated from a script
043     */
044    public static boolean eraseOnTimeoutAll = false;
045
046    /**
047     * Should this CbusReporter clear itself after a timeout?
048     * <p>
049     * Default behavior is to not timeout; this is public access
050     * so it can be updated from a script
051     */
052    public boolean eraseOnTimeoutThisReporter = false;
053
054    /**
055     * Create a new CbusReporter.
056     *
057     *
058     * @param address Reporter address, currently in String number format. No system prefix or type letter.
059     * @param memo System connection.
060     */
061    public CbusReporter(String address, CanSystemConnectionMemo memo) {  // a human-readable Reporter number must be specified!
062        super(memo.getSystemPrefix() + "R" + address);  // can't use prefix here, as still in construction
063        _number = Integer.parseInt(  address);
064        _memo = memo;
065        // At construction, don't register for messages; they're sent via the CbusReporterManager
066        // tc = memo.getTrafficController(); // can be removed when former constructor removed
067        // addTc(memo.getTrafficController());
068        log.debug("Added new reporter {}R{}", memo.getSystemPrefix(), address);
069    }
070
071    /**
072     * {@inheritDoc}
073     * <p>
074     * CBUS hardware does not send exit reports; it relies on timeout clearing.
075     */
076    @Override
077    public boolean hasExitReports() {
078        return false;
079    }
080
081    /**
082     * Set the CbusReporter State.
083     *
084     * May also provide / update a CBUS Sensor State, depending on property.
085     * {@inheritDoc}
086     */
087    @Override
088    public void setState(int s) {
089        super.setState(s);
090        if ( getMaintainSensor() ) {
091            SensorManager sm = _memo.get(SensorManager.class);
092            sm.provide("+"+_number).setCommandedState( s==IdTag.SEEN ? Sensor.ACTIVE : Sensor.INACTIVE );
093        }
094    }
095
096    /**
097     * {@inheritDoc}
098     * Resets report briefly back to null so Sensor Listeners are updated.
099     */
100    @Override
101    public void notify(IdTag id){
102        if ( this.getCurrentReport()!=null && id!=null ){
103            super.notify(null); //
104        }
105        super.notify(id);
106    }
107
108    /**
109     * {@inheritDoc}
110     * CBUS Reporters can respond to ACDAT or DDES OPC's.
111     */
112    @Override
113    public void message(CanMessage m) {
114        reply(new CanReply(m));
115    }
116
117    /**
118     * {@inheritDoc}
119     * CBUS Reporters can respond to ACDAT or DDES OPC's
120     */
121    @Override
122    public void reply(CanReply m) {
123        if ( m.extendedOrRtr() ) {
124            return;
125        }
126        if ( m.getOpCode() != CbusConstants.CBUS_DDES && m.getOpCode() != CbusConstants.CBUS_ACDAT) {
127            return;
128        }
129
130        if (((m.getElement(1) << 8) + m.getElement(2)) == _number) { // correct reporter number, for us
131            if (getCbusReporterType().equals(CbusReporterManager.CBUS_REPORTER_TYPE_CLASSIC)) {
132                // Classic 5 byte unique RFID tag on DDES or ACDAT, the tag
133                // bytes are opaque data and must not be inspected for a type.
134                classicRFIDReport(m);
135            } else if (m.getOpCode() == CbusConstants.CBUS_DDES) {
136                // self-describing DDES, CANRC522 or CANRCOM
137                ddesReport(m);
138            } else {
139                // ACDAT is always a 5 byte tag
140                classicRFIDReport(m);
141            }
142        }
143    }
144
145    /**
146     * Process a self-describing DDES from a CANRC522 or CANRCOM.
147     * The least significant bit of the first data byte selects the format.
148     * @param m the DDES frame
149     */
150    private void ddesReport(CanReply m) {
151        int least_significant_bit = m.getElement(3) & 1;
152        if ( least_significant_bit == 0 ) {
153            canRc522Report(m);
154        } else {
155            canRcomReport(m);
156        }
157    }
158
159    private void classicRFIDReport(CanReply m) {
160        String buf = toClassicTag(m.getElement(3), m.getElement(4), m.getElement(5), m.getElement(6), m.getElement(7));
161        log.debug("Reporter {} {} RFID tag read of tag: {}", this,getCbusReporterType(),buf);
162        IdTag tag = InstanceManager.getDefault(IdTagManager.class).provideIdTag(buf);
163        notify(tag);
164        startTimeout(tag);
165    }
166
167    // no DCC address correction to allow full 0-65535 range of tags on rolling stock
168    private void canRc522Report(CanReply m){
169        String tagId = String.valueOf((m.getElement(4)<<8)+ m.getElement(5));
170        log.debug("Reporter {} RFID tag read of tag: {}",this, tagId);
171        IdTag tag = InstanceManager.getDefault(IdTagManager.class).provideIdTag("ID"+tagId);
172        tag.setProperty("DDES Dat3", m.getElement(6));
173        tag.setProperty("DDES Dat4", m.getElement(7));
174        notify(tag);
175        startTimeout(tag);
176    }
177
178
179
180    private void canRcomReport(CanReply m) {
181
182        var locoAddress = parseAddress(m);
183        
184        int speed = m.getElement(6)&0x7F;
185        if ((m.getElement(6)&0x80) == 0) {
186            speed = -1;  // data unavailable
187        }
188        
189        int flags = m.getElement(7);
190        
191        RailCom.Orientation orientation;
192        switch (flags&0x03) {
193            case 2:
194                orientation = RailCom.Orientation.EAST;
195                break;
196            case 1:
197                orientation = RailCom.Orientation.WEST;
198                break;
199            case 0:
200                orientation = RailCom.Orientation.UNKNOWN;
201                break;
202            default:
203                log.warn("Unexpected orientation code 3");
204                orientation = RailCom.Orientation.UNKNOWN;
205                break;
206        }
207        
208        RailCom.Direction direction;
209        switch ((flags>>2)&0x03) {
210            case 1:
211                direction = RailCom.Direction.FORWARD;
212                break;
213            case 2:
214                direction = RailCom.Direction.BACKWARD;
215                break;
216            case 0:
217                direction = RailCom.Direction.UNKNOWN;
218                break;
219            default:
220                log.warn("Unexpected direction code 3");
221                direction = RailCom.Direction.UNKNOWN;
222                break;
223        }
224        
225        RailCom.Motion motion;
226        switch ((flags>>4)&0x03) {
227            case 1:
228                motion = RailCom.Motion.STATIONARY;
229                log.debug("Setting speed to zero because known to be not moving");
230                speed = 0;
231                break;
232            case 2:
233                motion = RailCom.Motion.MOVING;
234                break;
235            case 0:
236                motion = RailCom.Motion.UNKNOWN;
237                break;
238            default:
239                log.warn("Unexpected motion code 3");
240                motion = RailCom.Motion.UNKNOWN;
241                break;
242        }
243        
244        RailCom.QoS qos;
245        switch ((flags>>6)&0x03) {
246            case 1:
247                qos = RailCom.QoS.POOR;
248                break;
249            case 2:
250                qos = RailCom.QoS.GOOD;
251                break;
252            case 0:
253                qos = RailCom.QoS.UNKNOWN;
254                break;
255            default:
256                log.warn("Unexpected QoS code 3");
257                qos = RailCom.QoS.UNKNOWN;
258                break;
259        }
260        
261        // RailComManager looked up when needed, not cached in a static field at class load,
262        // which could refer to a manager no longer registered with the InstanceManager.
263        var idTag = InstanceManager.getDefault(RailComManager.class).provideIdTag(""+locoAddress.getNumber());
264        var tag = (RailCom)idTag;
265        
266        tag.setDccAddress(locoAddress);
267        tag.setActualSpeed(speed);
268        tag.setOrientation(orientation);
269        tag.setDirection(direction);
270        tag.setMotion(motion);
271        tag.setQoS(qos);
272
273        notify(tag);
274        startTimeout(tag);
275    }
276
277    DccLocoAddress parseAddress(CanReply m) {  // package access for testing
278        int dccTypeInt = m.getElement(4)&0xC0;
279        int dccNumber;
280        int b4 = m.getElement(4) & 0x3F;  // excludes high "type" bits
281        int b5 = m.getElement(5);
282        LocoAddress.Protocol dccType;
283        boolean isConsist;
284        switch (dccTypeInt) {
285            default:
286            case 0xC0:
287                dccNumber = (b4<<8) | b5;
288                dccType = LocoAddress.Protocol.DCC_LONG;
289                isConsist = false;
290                break;
291            case 0x00:
292                dccNumber = b5;
293                dccType = LocoAddress.Protocol.DCC_SHORT;
294                isConsist = false;
295                break;
296            case 0x40:
297                dccNumber = b5&0x7F;  // remove direction bit
298                dccType = LocoAddress.Protocol.DCC_SHORT;
299                isConsist = true;
300                break;
301            case 0x80:
302                int decUpper = (b4 << 1) | ((b5>>7)&0x01); // BCD upper value
303                dccNumber = decUpper*100 + (b5&0x7F);
304                dccType = LocoAddress.Protocol.DCC_LONG;
305                isConsist = true;
306                break;                
307        }
308        return new DccLocoAddress(dccNumber, dccType, isConsist);
309    }
310    
311    private String toClassicTag(int b1, int b2, int b3, int b4, int b5) {
312        return String.format("%02X", b1) + String.format("%02X", b2) + String.format("%02X", b3)
313            + String.format("%02X", b4) + String.format("%02X", b5);
314    }
315
316    /**
317     * Get the Reporter Listener format type.
318     * <p>
319     * Defaults to Classic RfID, 5 byte unique.
320     * @return reporter format type.
321     */
322    @Nonnull
323    public String getCbusReporterType() {
324        Object returnVal = getProperty(CbusReporterManager.CBUS_REPORTER_DESCRIPTOR_KEY);
325        return (returnVal==null ? CbusReporterManager.CBUS_DEFAULT_REPORTER_TYPE : returnVal.toString());
326    }
327
328    /**
329     * Get if the Reporter should provide / update a CBUS Sensor, following Reporter Status.
330     * <p>
331     * Defaults to false.
332     * @return true if the reporter should maintain the Sensor.
333     */
334    public boolean getMaintainSensor() {
335        Boolean returnVal = (Boolean) getProperty(CbusReporterManager.CBUS_MAINTAIN_SENSOR_DESCRIPTOR_KEY);
336        return (returnVal==null ? false : returnVal);
337    }
338
339    // delay can be set to non-null memo when older constructor fully deprecated.
340    private void startTimeout(IdTag tag){
341        // only timeout when enabled
342        if (! eraseOnTimeoutAll && ! eraseOnTimeoutThisReporter) return;
343
344        int delay = (_memo==null ? 2000 : ((CbusReporterManager)_memo.get(jmri.ReporterManager.class)).getTimeout() );
345        // a later read restarts the timeout, also when it is the same tag again
346        final int generation = ++timeoutGeneration;
347        ThreadingUtil.runOnLayoutDelayed( () -> {
348            if (!disposed && generation == timeoutGeneration && getCurrentReport() == tag) {
349                notify(null);
350            }
351        },delay);
352    }
353
354    /**
355     * Counts the reads that started a timeout; only the timeout of the last
356     * read can clear the report. Used on the layout thread.
357     */
358    private int timeoutGeneration = 0;
359
360    private boolean disposed = false;
361
362    /**
363     * {@inheritDoc}
364     */
365    @Override
366    public void dispose() {
367        disposed = true;
368        super.dispose();
369    }
370
371    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CbusReporter.class);
372}