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    private static final RailComManager railComManager = InstanceManager.getDefault(RailComManager.class);
038
039    /**
040     * Should all CbusReporters clear themselves after a timeout?
041     * <p>
042     * Default behavior is to not timeout; this is public access
043     * so it can be updated from a script
044     */
045    public static boolean eraseOnTimeoutAll = false;
046
047    /**
048     * Should this CbusReporter clear itself after a timeout?
049     * <p>
050     * Default behavior is to not timeout; this is public access
051     * so it can be updated from a script
052     */
053    public boolean eraseOnTimeoutThisReporter = false;
054
055    /**
056     * Create a new CbusReporter.
057     *
058     *
059     * @param address Reporter address, currently in String number format. No system prefix or type letter.
060     * @param memo System connection.
061     */
062    public CbusReporter(String address, CanSystemConnectionMemo memo) {  // a human-readable Reporter number must be specified!
063        super(memo.getSystemPrefix() + "R" + address);  // can't use prefix here, as still in construction
064        _number = Integer.parseInt(  address);
065        _memo = memo;
066        // At construction, don't register for messages; they're sent via the CbusReporterManager
067        // tc = memo.getTrafficController(); // can be removed when former constructor removed
068        // addTc(memo.getTrafficController());
069        log.debug("Added new reporter {}R{}", memo.getSystemPrefix(), address);
070    }
071
072    /**
073     * Set the CbusReporter State.
074     *
075     * May also provide / update a CBUS Sensor State, depending on property.
076     * {@inheritDoc}
077     */
078    @Override
079    public void setState(int s) {
080        super.setState(s);
081        if ( getMaintainSensor() ) {
082            SensorManager sm = _memo.get(SensorManager.class);
083            sm.provide("+"+_number).setCommandedState( s==IdTag.SEEN ? Sensor.ACTIVE : Sensor.INACTIVE );
084        }
085    }
086
087    /**
088     * {@inheritDoc}
089     * Resets report briefly back to null so Sensor Listeners are updated.
090     */
091    @Override
092    public void notify(IdTag id){
093        if ( this.getCurrentReport()!=null && id!=null ){
094            super.notify(null); //
095        }
096        super.notify(id);
097    }
098
099    /**
100     * {@inheritDoc}
101     * CBUS Reporters can respond to ACDAT or DDES OPC's.
102     */
103    @Override
104    public void message(CanMessage m) {
105        reply(new CanReply(m));
106    }
107
108    /**
109     * {@inheritDoc}
110     * CBUS Reporters can respond to ACDAT or DDES OPC's
111     */
112    @Override
113    public void reply(CanReply m) {
114        if ( m.extendedOrRtr() ) {
115            return;
116        }
117        if ( m.getOpCode() != CbusConstants.CBUS_DDES && m.getOpCode() != CbusConstants.CBUS_ACDAT) {
118            return;
119        }
120
121        if (((m.getElement(1) << 8) + m.getElement(2)) == _number) { // correct reporter number, for us
122            if (m.getOpCode() == CbusConstants.CBUS_DDES && !getCbusReporterType().equals(CbusReporterManager.CBUS_REPORTER_TYPE_CLASSIC)  ) {
123                ddesReport(m);
124            } else {
125                int least_significant_bit = m.getElement(3) & 1;
126                if ( least_significant_bit == 0 ) {
127                    classicRFIDReport(m);
128                } else {
129                    canRcomReport(m);
130                }
131            }
132        }
133    }
134
135    private void ddesReport(CanReply m) {
136        int least_significant_bit = m.getElement(3) & 1;
137        if ( least_significant_bit == 0 ) {
138            canRc522Report(m);
139        } else {
140            canRcomReport(m);
141        }
142    }
143
144    private void classicRFIDReport(CanReply m) {
145        String buf = toClassicTag(m.getElement(3), m.getElement(4), m.getElement(5), m.getElement(6), m.getElement(7));
146        log.debug("Reporter {} {} RFID tag read of tag: {}", this,getCbusReporterType(),buf);
147        IdTag tag = InstanceManager.getDefault(IdTagManager.class).provideIdTag(buf);
148        notify(tag);
149        startTimeout(tag);
150    }
151
152    // no DCC address correction to allow full 0-65535 range of tags on rolling stock
153    private void canRc522Report(CanReply m){
154        String tagId = String.valueOf((m.getElement(4)<<8)+ m.getElement(5));
155        log.debug("Reporter {} RFID tag read of tag: {}",this, tagId);
156        IdTag tag = InstanceManager.getDefault(IdTagManager.class).provideIdTag("ID"+tagId);
157        tag.setProperty("DDES Dat3", m.getElement(6));
158        tag.setProperty("DDES Dat4", m.getElement(7));
159        notify(tag);
160        startTimeout(tag);
161    }
162
163
164
165    private void canRcomReport(CanReply m) {
166
167        var locoAddress = parseAddress(m);
168        
169        int speed = m.getElement(6)&0x7F;
170        if ((m.getElement(6)&0x80) == 0) {
171            speed = -1;  // data unavailable
172        }
173        
174        int flags = m.getElement(7);
175        
176        RailCom.Orientation orientation;
177        switch (flags&0x03) {
178            case 2:
179                orientation = RailCom.Orientation.EAST;
180                break;
181            case 1:
182                orientation = RailCom.Orientation.WEST;
183                break;
184            case 0:
185                orientation = RailCom.Orientation.UNKNOWN;
186                break;
187            default:
188                log.warn("Unexpected orientation code 3");
189                orientation = RailCom.Orientation.UNKNOWN;
190                break;
191        }
192        
193        RailCom.Direction direction;
194        switch ((flags>>2)&0x03) {
195            case 1:
196                direction = RailCom.Direction.FORWARD;
197                break;
198            case 2:
199                direction = RailCom.Direction.BACKWARD;
200                break;
201            case 0:
202                direction = RailCom.Direction.UNKNOWN;
203                break;
204            default:
205                log.warn("Unexpected direction code 3");
206                direction = RailCom.Direction.UNKNOWN;
207                break;
208        }
209        
210        RailCom.Motion motion;
211        switch ((flags>>4)&0x03) {
212            case 1:
213                motion = RailCom.Motion.STATIONARY;
214                log.debug("Setting speed to zero because known to be not moving");
215                speed = 0;
216                break;
217            case 2:
218                motion = RailCom.Motion.MOVING;
219                break;
220            case 0:
221                motion = RailCom.Motion.UNKNOWN;
222                break;
223            default:
224                log.warn("Unexpected motion code 3");
225                motion = RailCom.Motion.UNKNOWN;
226                break;
227        }
228        
229        RailCom.QoS qos;
230        switch ((flags>>6)&0x03) {
231            case 1:
232                qos = RailCom.QoS.POOR;
233                break;
234            case 2:
235                qos = RailCom.QoS.GOOD;
236                break;
237            case 0:
238                qos = RailCom.QoS.UNKNOWN;
239                break;
240            default:
241                log.warn("Unexpected QoS code 3");
242                qos = RailCom.QoS.UNKNOWN;
243                break;
244        }
245        
246        var idTag = railComManager.provideIdTag(""+locoAddress.getNumber());
247        var tag = (RailCom)idTag;
248        
249        tag.setDccAddress(locoAddress);
250        tag.setActualSpeed(speed);
251        tag.setOrientation(orientation);
252        tag.setDirection(direction);
253        tag.setMotion(motion);
254        tag.setQoS(qos);
255
256        notify(tag);
257        startTimeout(tag);
258    }
259
260    DccLocoAddress parseAddress(CanReply m) {  // package access for testing
261        int dccTypeInt = m.getElement(4)&0xC0;
262        int dccNumber;
263        int b4 = m.getElement(4) & 0x3F;  // excludes high "type" bits
264        int b5 = m.getElement(5);
265        LocoAddress.Protocol dccType;
266        boolean isConsist;
267        switch (dccTypeInt) {
268            default:
269            case 0xC0:
270                dccNumber = (b4<<8) | b5;
271                dccType = LocoAddress.Protocol.DCC_LONG;
272                isConsist = false;
273                break;
274            case 0x00:
275                dccNumber = b5;
276                dccType = LocoAddress.Protocol.DCC_SHORT;
277                isConsist = false;
278                break;
279            case 0x40:
280                dccNumber = b5&0x7F;  // remove direction bit
281                dccType = LocoAddress.Protocol.DCC_SHORT;
282                isConsist = true;
283                break;
284            case 0x80:
285                int decUpper = (b4 << 1) | ((b5>>7)&0x01); // BCD upper value
286                dccNumber = decUpper*100 + (b5&0x7F);
287                dccType = LocoAddress.Protocol.DCC_LONG;
288                isConsist = true;
289                break;                
290        }
291        return new DccLocoAddress(dccNumber, dccType, isConsist);
292    }
293    
294    private String toClassicTag(int b1, int b2, int b3, int b4, int b5) {
295        return String.format("%02X", b1) + String.format("%02X", b2) + String.format("%02X", b3)
296            + String.format("%02X", b4) + String.format("%02X", b5);
297    }
298
299    /**
300     * Get the Reporter Listener format type.
301     * <p>
302     * Defaults to Classic RfID, 5 byte unique.
303     * @return reporter format type.
304     */
305    @Nonnull
306    public String getCbusReporterType() {
307        Object returnVal = getProperty(CbusReporterManager.CBUS_REPORTER_DESCRIPTOR_KEY);
308        return (returnVal==null ? CbusReporterManager.CBUS_DEFAULT_REPORTER_TYPE : returnVal.toString());
309    }
310
311    /**
312     * Get if the Reporter should provide / update a CBUS Sensor, following Reporter Status.
313     * <p>
314     * Defaults to false.
315     * @return true if the reporter should maintain the Sensor.
316     */
317    public boolean getMaintainSensor() {
318        Boolean returnVal = (Boolean) getProperty(CbusReporterManager.CBUS_MAINTAIN_SENSOR_DESCRIPTOR_KEY);
319        return (returnVal==null ? false : returnVal);
320    }
321
322    // delay can be set to non-null memo when older constructor fully deprecated.
323    private void startTimeout(IdTag tag){
324        // only timeout when enabled
325        if (! eraseOnTimeoutAll && ! eraseOnTimeoutThisReporter) return;
326
327        int delay = (_memo==null ? 2000 : ((CbusReporterManager)_memo.get(jmri.ReporterManager.class)).getTimeout() );
328        ThreadingUtil.runOnLayoutDelayed( () -> {
329            if (!disposed && getCurrentReport() == tag) {
330                notify(null);
331            }
332        },delay);
333    }
334
335    private boolean disposed = false;
336
337    /**
338     * {@inheritDoc}
339     */
340    @Override
341    public void dispose() {
342        disposed = true;
343        super.dispose();
344    }
345
346    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CbusReporter.class);
347}