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    static private 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                classicRFIDReport(m);
126            }
127        }
128    }
129
130    private void ddesReport(CanReply m) {
131        int least_significant_bit = m.getElement(3) & 1;
132        if ( least_significant_bit == 0 ) {
133            canRc522Report(m);
134        } else {
135            canRcomReport(m);
136        }
137    }
138
139    private void classicRFIDReport(CanReply m) {
140        String buf = toClassicTag(m.getElement(3), m.getElement(4), m.getElement(5), m.getElement(6), m.getElement(7));
141        log.debug("Reporter {} {} RFID tag read of tag: {}", this,getCbusReporterType(),buf);
142        IdTag tag = InstanceManager.getDefault(IdTagManager.class).provideIdTag(buf);
143        notify(tag);
144        startTimeout(tag);
145    }
146
147    // no DCC address correction to allow full 0-65535 range of tags on rolling stock
148    private void canRc522Report(CanReply m){
149        String tagId = String.valueOf((m.getElement(4)<<8)+ m.getElement(5));
150        log.debug("Reporter {} RFID tag read of tag: {}",this, tagId);
151        IdTag tag = InstanceManager.getDefault(IdTagManager.class).provideIdTag("ID"+tagId);
152        tag.setProperty("DDES Dat3", m.getElement(6));
153        tag.setProperty("DDES Dat4", m.getElement(7));
154        notify(tag);
155        startTimeout(tag);
156    }
157
158
159
160    private void canRcomReport(CanReply m) {
161
162        var locoAddress = parseAddress(m);
163        
164        int speed = m.getElement(6)&0x7F;
165        if ((m.getElement(6)&0x80) == 0) {
166            speed = -1;  // data unavailable
167        }
168        
169        int flags = m.getElement(7);
170        
171        RailCom.Orientation orientation;
172        switch (flags&0x03) {
173            case 2:
174                orientation = RailCom.Orientation.EAST;
175                break;
176            case 1:
177                orientation = RailCom.Orientation.WEST;
178                break;
179            case 0:
180                orientation = RailCom.Orientation.UNKNOWN;
181                break;
182            default:
183                log.warn("Unexpected orientation code 3");
184                orientation = RailCom.Orientation.UNKNOWN;
185                break;
186        }
187        
188        RailCom.Direction direction;
189        switch ((flags>>2)&0x03) {
190            case 1:
191                direction = RailCom.Direction.FORWARD;
192                break;
193            case 2:
194                direction = RailCom.Direction.BACKWARD;
195                break;
196            case 0:
197                direction = RailCom.Direction.UNKNOWN;
198                break;
199            default:
200                log.warn("Unexpected direction code 3");
201                direction = RailCom.Direction.UNKNOWN;
202                break;
203        }
204        
205        RailCom.Motion motion;
206        switch ((flags>>4)&0x03) {
207            case 1:
208                motion = RailCom.Motion.STATIONARY;
209                log.debug("Setting speed to zero because known to be not moving");
210                speed = 0;
211                break;
212            case 2:
213                motion = RailCom.Motion.MOVING;
214                break;
215            case 0:
216                motion = RailCom.Motion.UNKNOWN;
217                break;
218            default:
219                log.warn("Unexpected motion code 3");
220                motion = RailCom.Motion.UNKNOWN;
221                break;
222        }
223        
224        RailCom.QoS qos;
225        switch ((flags>>6)&0x03) {
226            case 1:
227                qos = RailCom.QoS.POOR;
228                break;
229            case 2:
230                qos = RailCom.QoS.GOOD;
231                break;
232            case 0:
233                qos = RailCom.QoS.UNKNOWN;
234                break;
235            default:
236                log.warn("Unexpected QoS code 3");
237                qos = RailCom.QoS.UNKNOWN;
238                break;
239        }
240        
241        var idTag = railComManager.provideIdTag(""+locoAddress.getNumber());
242        var tag = (RailCom)idTag;
243        
244        tag.setDccAddress(locoAddress);
245        tag.setActualSpeed(speed);
246        tag.setOrientation(orientation);
247        tag.setDirection(direction);
248        tag.setMotion(motion);
249        tag.setQoS(qos);
250
251        notify(tag);
252        startTimeout(tag);
253    }
254
255    DccLocoAddress parseAddress(CanReply m) {  // package access for testing
256        int dccTypeInt = m.getElement(4)&0xC0;
257        int dccNumber;
258        int b4 = m.getElement(4) & 0x3F;  // excludes high "type" bits
259        int b5 = m.getElement(5);
260        LocoAddress.Protocol dccType;
261        switch (dccTypeInt) {
262            default:
263            case 0xC0:
264                dccNumber = (b4<<8) | b5;
265                dccType = LocoAddress.Protocol.DCC_LONG;
266                break;
267            case 0x00:
268                dccNumber = b5;
269                dccType = LocoAddress.Protocol.DCC_SHORT;
270                break;
271            case 0x40:
272                dccNumber = b5&0x7F;  // remove direction bit
273                dccType = LocoAddress.Protocol.DCC_CONSIST;
274                break;
275            case 0x80:
276                int decUpper = (b4 << 1) | ((b5>>7)&0x01); // BCD upper value
277                dccNumber = decUpper*100 + (b5&0x7F);
278                dccType = LocoAddress.Protocol.DCC_EXTENDED_CONSIST;
279                
280        }
281        return new DccLocoAddress(dccNumber, dccType);
282    }
283    
284    private String toClassicTag(int b1, int b2, int b3, int b4, int b5) {
285        return String.format("%02X", b1) + String.format("%02X", b2) + String.format("%02X", b3)
286            + String.format("%02X", b4) + String.format("%02X", b5);
287    }
288
289    /**
290     * Get the Reporter Listener format type.
291     * <p>
292     * Defaults to Classic RfID, 5 byte unique.
293     * @return reporter format type.
294     */
295    @Nonnull
296    public String getCbusReporterType() {
297        Object returnVal = getProperty(CbusReporterManager.CBUS_REPORTER_DESCRIPTOR_KEY);
298        return (returnVal==null ? CbusReporterManager.CBUS_DEFAULT_REPORTER_TYPE : returnVal.toString());
299    }
300
301    /**
302     * Get if the Reporter should provide / update a CBUS Sensor, following Reporter Status.
303     * <p>
304     * Defaults to false.
305     * @return true if the reporter should maintain the Sensor.
306     */
307    public boolean getMaintainSensor() {
308        Boolean returnVal = (Boolean) getProperty(CbusReporterManager.CBUS_MAINTAIN_SENSOR_DESCRIPTOR_KEY);
309        return (returnVal==null ? false : returnVal);
310    }
311
312    // delay can be set to non-null memo when older constructor fully deprecated.
313    private void startTimeout(IdTag tag){
314        // only timeout when enabled
315        if (! eraseOnTimeoutAll && ! eraseOnTimeoutThisReporter) return;
316
317        int delay = (_memo==null ? 2000 : ((CbusReporterManager)_memo.get(jmri.ReporterManager.class)).getTimeout() );
318        ThreadingUtil.runOnLayoutDelayed( () -> {
319            if (!disposed && getCurrentReport() == tag) {
320                notify(null);
321            }
322        },delay);
323    }
324
325    private boolean disposed = false;
326
327    /**
328     * {@inheritDoc}
329     */
330    @Override
331    public void dispose() {
332        disposed = true;
333        super.dispose();
334    }
335
336    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CbusReporter.class);
337}