001package jmri.jmrix.can.cbus;
002
003import java.util.ArrayList;
004import java.util.List;
005
006import javax.annotation.Nonnull;
007
008import jmri.*;
009import jmri.jmrix.can.CanListener;
010import jmri.jmrix.can.CanMessage;
011import jmri.jmrix.can.CanMutableFrame;
012import jmri.jmrix.can.CanReply;
013import jmri.jmrix.can.CanSystemConnectionMemo;
014import jmri.managers.AbstractReporterManager;
015
016/**
017 * Implement ReporterManager for CAN CBUS systems.
018 * <p>
019 * System names are "MRnnnnn", where M is the user-configurable system getSystemPrefix(),
020 * nnnnn is the reporter number without padding.
021 * <p>
022 * CBUS Reporters are NOT automatically created.
023 *
024 * @author Mark Riddoch Copyright (C) 2015
025 * @author Steve Young Copyright (C) 2019
026 */
027public class CbusReporterManager extends AbstractReporterManager implements CanListener {
028
029    public CbusReporterManager(CanSystemConnectionMemo memo) {
030        super(memo);
031
032        // At construction, register for messages to auto create CbusReporters
033        addTc(memo.getTrafficController());
034    }
035
036    /**
037     * {@inheritDoc}
038     */
039    @Override
040    @Nonnull
041    public CanSystemConnectionMemo getMemo() {
042        return (CanSystemConnectionMemo) memo;
043    }
044
045    /**
046     * {@inheritDoc}
047     */
048    @Override
049    @Nonnull
050    protected Reporter createNewReporter(@Nonnull String systemName, String userName) throws IllegalArgumentException {
051        log.debug("ReporterManager create new CbusReporter: {}", systemName);
052        String addr = systemName.substring(getSystemNamePrefix().length());
053        Reporter t = new CbusReporter(addr, getMemo());
054        t.setUserName(userName);
055        t.addPropertyChangeListener(this);
056        return t;
057    }
058
059    /**
060     * {@inheritDoc}
061     * Checks for reporter number between 0 and 65535
062     */
063    @Override
064    public NameValidity validSystemNameFormat(@Nonnull String systemName) {
065        // name must be in the MSnnnnn format (M is user configurable); no + or ; or - for Reporter address
066        log.debug("Checking system name: {}", systemName);
067        try {
068            // try to parse the string; success returns true
069            int testnum = Integer.parseInt(systemName.substring(getSystemPrefix().length() + 1, systemName.length()));
070            if ( testnum < 0 ) {
071                log.debug("Number field cannot be negative in system name: {}", systemName);
072                return NameValidity.INVALID;
073            }
074            if ( testnum > 65535  ) {
075                log.debug("Number field cannot be greater than 65535 in system name: {}", systemName);
076                return NameValidity.INVALID;
077            }
078        }
079        catch (NumberFormatException e) {
080            log.debug("Illegal character in number field of system name: {}", systemName);
081            return NameValidity.INVALID;
082        }
083        catch (StringIndexOutOfBoundsException e) {
084            log.debug("Wrong length ( missing MR? ) for system name: {}", systemName);
085            return NameValidity.INVALID;
086        }
087        log.debug("Valid system name: {}", systemName);
088        return NameValidity.VALID;
089    }
090
091    /**
092     * {@inheritDoc}
093     */
094    @Override
095    public boolean allowMultipleAdditions(@Nonnull String systemName) {
096        return true;
097    }
098
099    /**
100     * {@inheritDoc}
101     */
102    @Override
103    public String getEntryToolTip() {
104        return Bundle.getMessage("AddReporterEntryToolTip");
105    }
106
107    /**
108     * Validates to only numeric system names.
109     * {@inheritDoc}
110     */
111    @Override
112    @Nonnull
113    public String validateSystemNameFormat(@Nonnull String name, @Nonnull java.util.Locale locale) throws jmri.NamedBean.BadSystemNameException {
114        return validateSystemNameFormatOnlyNumeric(name,locale);
115    }
116
117    protected final static String CBUS_REPORTER_DESCRIPTOR_KEY = "CBUS Reporter Type"; // NOI18N
118
119    protected final static String CBUS_REPORTER_TYPE_CLASSIC = "Classic RFID"; // NOI18N
120
121    protected final static String CBUS_REPORTER_TYPE_DDES_DESCRIBING = "CANRC522 / CANRCOM"; // NOI18N
122
123    final static String[] CBUS_REPORTER_TYPES = {
124        CBUS_REPORTER_TYPE_CLASSIC,CBUS_REPORTER_TYPE_DDES_DESCRIBING};
125
126    final static String[] CBUS_REPORTER_TYPE_TIPS = {
127        "DDES / ACDAT 5 byte unique tag.","DDES self-describing ( Writeable CANRC522 / Railcom )"}; // NOI18N
128
129    protected final static String CBUS_DEFAULT_REPORTER_TYPE = CBUS_REPORTER_TYPES[0];
130
131    protected final static String CBUS_MAINTAIN_SENSOR_DESCRIPTOR_KEY = "Maintain CBUS Sensor"; // NOI18N
132
133    @Override
134    @Nonnull
135    public List<NamedBeanPropertyDescriptor<?>> getKnownBeanProperties() {
136        List<NamedBeanPropertyDescriptor<?>> l = new ArrayList<>();
137        l.add(new SelectionPropertyDescriptor(
138            CBUS_REPORTER_DESCRIPTOR_KEY, CBUS_REPORTER_TYPES, CBUS_REPORTER_TYPE_TIPS, CBUS_DEFAULT_REPORTER_TYPE) {
139            @Override
140            public String getColumnHeaderText() {
141                return CBUS_REPORTER_DESCRIPTOR_KEY;
142            }
143            @Override
144            public boolean isEditable(NamedBean bean) {
145                return (bean instanceof CbusReporter);
146            }
147        });
148        l.add(new BooleanPropertyDescriptor(
149            CBUS_MAINTAIN_SENSOR_DESCRIPTOR_KEY, false) {
150            @Override
151            public String getColumnHeaderText() {
152                return CBUS_MAINTAIN_SENSOR_DESCRIPTOR_KEY;
153            }
154            @Override
155            public boolean isEditable(NamedBean bean) {
156                return (bean instanceof CbusReporter);
157            }
158        });
159        return l;
160    }
161
162    private int _timeout=2000; // same default as TimeoutReporter
163
164    /**
165     * Set the Reporter timeout.
166     * @param timeout time in milliseconds that CbusReporters stay at IdTag.SEEN after hearing an ID Tag.
167     */
168    public void setTimeout(int timeout){
169        _timeout = timeout;
170    }
171
172    /**
173     * Get the Reporter Timeout.
174     * @return milliseconds for CbusReporters to return to IdTag.UNSEEN
175     */
176    public int getTimeout(){
177        return _timeout;
178    }
179
180    /**
181     * Parse incoming reply to see if it is interesting to a Reporter, 
182     * and if it is make sure that reporter exists.
183     */
184    private CbusReporter processFrame(CanMutableFrame m) {
185        // see if this is a RailCom message from CBus
186        if ( m.extendedOrRtr() ) {
187            return null;
188        }
189        if ( m.getElement(0) != CbusConstants.CBUS_DDES && m.getElement(0) != CbusConstants.CBUS_ACDAT) {
190            return null;
191        }
192        // here an DDES (RFID or RailCom) or ACATmessage, extract device number & check for existing CBusReporter
193        var device = (m.getElement(1) << 8) + m.getElement(2);
194        var systemName = ""+device;
195
196        var reporter = provideReporter(systemName); // create if it doesn't exist
197           
198        // is this the right algorithm?
199        int least_significant_nibble = m.getElement(3) & 0x0F;
200        if ( least_significant_nibble == 0x01 && m.getElement(0) == CbusConstants.CBUS_DDES) {
201            // is a RailCom Reporter, set that as mode
202            reporter.setProperty(CBUS_REPORTER_DESCRIPTOR_KEY,CBUS_REPORTER_TYPE_DDES_DESCRIBING);
203        }
204        
205        return (CbusReporter)reporter;        
206    }
207    
208    @Override
209    public void reply(CanReply m) {
210        var reporter = processFrame(m);
211        if (reporter!= null) {
212            reporter.reply(m);
213        }
214    }
215    
216    @Override  
217    public void message(CanMessage m) {
218        var reporter = processFrame(m);
219        if (reporter!= null) {
220            reporter.message(m);
221        }
222    }
223
224    /**
225     * {@inheritDoc}
226     */
227    @Override
228    public void dispose() {
229        if (getMemo().getTrafficController() != null) { // not all tests provide a TrafficController
230            getMemo().getTrafficController().removeCanListener(this);
231        }
232        super.dispose();
233    }    
234
235    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CbusReporterManager.class);
236
237}