001package jmri.jmrix.loconet;
002
003import java.util.HashSet;
004import java.util.regex.Matcher;
005import java.util.regex.Pattern;
006import jmri.DccLocoAddress;
007import jmri.InstanceManager;
008import jmri.IdTag;
009import jmri.LocoAddress;
010import jmri.CollectingReporter;
011import jmri.PhysicalLocationReporter;
012import jmri.implementation.AbstractIdTagReporter;
013import jmri.util.PhysicalLocation;
014import org.slf4j.Logger;
015import org.slf4j.LoggerFactory;
016
017/**
018 * Extend jmri.AbstractIdTagReporter for LocoNet layouts.
019 * <p>
020 * This implementation reports Transponding messages from LocoNet-based "Reporters".
021 * <p>
022 * For LocoNet connections, a "Reporter" represents either a Digitrax "transponding zone", a
023 * Lissy "measurement zone" or a Lissy RFID reader location.
024 * <br>
025 * The messages from these Reporters are handled by this code.
026 * <br>
027 * The LnReporterManager is responsible for decode of appropriate LocoNet messages
028 * and passing only those messages to the Reporter which match its Reporter address.
029 * <br>
030 * Each transponding message creates a new current report. The last report is
031 * always available, and is the same as the contents of the last transponding
032 * message received. Based on the report, for new tags a new Id Tag is created by the LnReporter.
033 * <p>
034 * Reports are Strings, formatted as
035 * <ul>
036 *   <li>NNNN enter - locomotive address NNNN entered the transponding zone. Short
037 *                    vs long address is indicated by the NNNN value
038 *   <li>NNNN exits - locomotive address NNNN left the transponding zone.
039 *   <li>NNNN seen northbound - LISSY measurement
040 *   <li>NNNN seen southbound - LISSY measurement
041 * </ul>
042 *
043 * Some of the message formats used in this class are Copyright Digitrax, Inc.
044 * and used with permission as part of the JMRI project. That permission does
045 * not extend to uses in other software products. If you wish to use this code,
046 * algorithm or these message formats outside of JMRI, please contact Digitrax
047 * Inc for separate permission.
048 *
049 * @author Bob Jacobsen Copyright (C) 2001, 2007
050 */
051public class LnReporter extends AbstractIdTagReporter implements CollectingReporter {
052
053    public LnReporter(int number, LnTrafficController tc, String prefix) {  // a human-readable Reporter number must be specified!
054        super(prefix + "R" + number);  // can't use prefix here, as still in construction
055        log.debug("new Reporter {}", number);
056        _number = number;
057        // At construction, register for messages
058        entrySet = new HashSet<>();
059    }
060
061
062    /**
063      * @return the LocoNet address number for this reporter.
064      */
065    public int getNumber() {
066        return _number;
067    }
068
069    /**
070      * Process LocoNet message handed to us from the LnReporterManager
071      * @param l - a LocoNetMessage.
072      */
073    public void messageFromManager(LocoNetMessage l) {
074        // check message type
075        if (isTranspondingLocationReport(l) || isTranspondingFindReport(l)) {
076            transpondingReport(l);
077        }
078        if (l.getOpCode() == LnConstants.OPC_LISSY_UPDATE) {
079            if (l.getElement(1) == 0x08) {
080                lissyReport(l);
081            } else if (l.getElement(2) == 0x41) {
082                lissyRfidReport(l);
083            }
084        }  // else nothing
085
086    }
087
088    /**
089     * Check if message is a Transponding Location Report message
090     *
091     * A Transponding Location Report message is sent by transponding hardware
092     * when a transponding mobile decoder enters or leaves a transponding zone.
093     *
094     * @param l LocoNet message to check
095     * @return true if message is a Transponding Location Report, else false.
096     */
097    public final boolean isTranspondingLocationReport(LocoNetMessage l) {
098        return ((l.getOpCode() == LnConstants.OPC_MULTI_SENSE)
099            && ((l.getElement(1) & 0xC0) == 0)) ;
100    }
101
102    /**
103     * Check if message is a Transponding Find Report message
104     *
105     * A Transponding Location Report message is sent by transponding hardware
106     * in response to a Transponding Find Request message when the addressed
107     * decoder is within a transponding zone and the decoder is transponding-enabled.
108     *
109     * @param l LocoNet message to check
110     * @return true if message is a Transponding Find Report, else false.
111     */
112    public final boolean isTranspondingFindReport(LocoNetMessage l) {
113        return (l.getOpCode() == LnConstants.OPC_PEER_XFER
114            && l.getElement(1) == 0x09
115            && l.getElement(2) == 0 );
116    }
117
118    /**
119     * Handle transponding message passed to us by the LnReporting Manager
120     *
121     * Assumes that the LocoNet message is a valid transponding message.
122     *
123     * @param l - incoming loconetmessage
124     */
125    void transpondingReport(LocoNetMessage l) {
126        boolean enter;
127        int loco;
128        IdTag idTag;
129        if (l.getOpCode() == LnConstants.OPC_MULTI_SENSE) {
130            enter = ((l.getElement(1) & 0x20) != 0); // get reported direction
131        } else {
132            enter = true; // a response for a find request. Always handled as entry.
133        }
134        loco = getLocoAddrFromTranspondingMsg(l); // get loco address
135
136        log.debug("Transponding Report at {} for {}",_number, loco);
137        notify(null); // set report to null to make sure listeners update
138
139        idTag = InstanceManager.getDefault(TranspondingTagManager.class).provideIdTag("" + loco);
140        idTag.setProperty("entryexit", "enter");
141        if (enter) {
142            idTag.setProperty("entryexit", "enter");
143            if (!entrySet.contains(idTag)) {
144                entrySet.add(idTag);
145            }
146        } else {
147            idTag.setProperty("entryexit", "exits");
148            if (entrySet.contains(idTag)) {
149                entrySet.remove(idTag);
150            }
151        }
152        log.debug("Tag: {} entry {}", idTag, enter);
153        notify(idTag);
154        setState(enter ? loco : -1);
155    }
156
157    /**
158     * extract long or short address from transponding message
159     *
160     * Assumes that the LocoNet message is a valid transponding message.
161     *
162     * @param l LocoNet message
163     * @return loco address
164     */
165    public int getLocoAddrFromTranspondingMsg(LocoNetMessage l) {
166        if (l.getElement(3) == 0x7D) {
167            return l.getElement(4);
168        }
169        return l.getElement(3) * 128 + l.getElement(4);
170
171    }
172
173    /**
174     * Handle LISSY message
175     * @param l Message from which to extract LISSY content
176     */
177    void lissyReport(LocoNetMessage l) {
178
179        // Only report messages where bit 6 is set in element 3,
180        // because these are the only messages with valid loco addresses
181        if ((l.getElement(3) & 0x40) != 0) {
182            int loco = (l.getElement(6) & 0x7F) + 128 * (l.getElement(5) & 0x7F);
183
184            // train category - Perhaps add to idTag as property?
185            int category = l.getElement(2) + 1;
186
187            // get direction
188            // north assumes loco is passing sensors S1->S2
189            boolean north = ((l.getElement(3) & 0x20) == 0);
190
191            notify(null); // set report to null to make sure listeners update
192            // get loco address
193            IdTag idTag = InstanceManager.getDefault(TranspondingTagManager.class).provideIdTag(""+loco+":"+category);
194            if(north) {
195               idTag.setProperty("seen", "seen northbound");
196            } else {
197               idTag.setProperty("seen", "seen southbound");
198            }
199            log.debug("Tag: {}", idTag);
200            notify(idTag);
201            setState(loco);
202        }
203    }
204
205    /**
206     * Handle LISSY RFID-7 and RFID-5 messages
207     * @param l Message from which to extract RFID content (UID)
208     */
209    void lissyRfidReport(LocoNetMessage l) {
210        String tag;
211        StringBuilder tg = new StringBuilder();
212        int max = l.getElement(1) - 2; // GCA51 RFID-7 elem(3) = size = 0x0E; RFID-5 elem(3) = size = 0x0C
213        int rfidHi = l.getElement(max); // MSbits are transmitted via element(max)
214        for (int j = 5; j < max; j++) {
215            int shift = j-5;
216            int hi = 0x0;
217            if(((rfidHi >> shift) & 0x1) == 1) hi = 0x80;
218            tg.append(String.format("%1$02X", l.getElement(j) + hi));
219        }
220        tag = tg.toString();
221
222        int rfidSensorAddress = l.getElement(3) << 7 | l.getElement(4);
223
224        notify(null); // set report to null to make sure listeners update
225        // get rfid tag
226        IdTag idTag = InstanceManager.getDefault(TranspondingTagManager.class).provideIdTag(""+tag);
227        // add info from reader
228        idTag.setProperty("rfid", rfidSensorAddress);
229        log.debug("Tag: {}", idTag);
230        notify(idTag); // sets report of this reporter to tag
231    }
232
233    /**
234     * Provide an int value for use in scripts, etc. This will be the numeric
235     * locomotive address last seen, unless the last message said the loco was
236     * exiting. Note that there may still some other locomotive in the
237     * transponding zone!
238     *
239     * @return -1 if the last message specified exiting
240     */
241    @Override
242    public int getState() {
243        return lastLoco;
244    }
245
246    /**
247      * {@inheritDoc}
248      */
249    @Override
250    public void setState(int s) {
251        lastLoco = s;
252    }
253    int lastLoco = -1;
254
255    /**
256     * Parses out a (possibly old) LnReporter-generated report string to extract info used by
257     * the public PhysicalLocationReporter methods.  Returns a Matcher that, if successful, should
258     * have the following groups defined.
259     * matcher.group(1) : the locomotive address
260     * matcher.group(2) : (enter | exit | seen)
261     * matcher.group(3) | (northbound | southbound) -- Lissy messages only
262     * <p>
263     * NOTE: This code is dependent on the transpondingReport() and lissyReport() methods.
264     * If they change, the regex here must change.
265     */
266    private Matcher parseReport(String rep) {
267        if (rep == null) {
268            return (null);
269        }
270        Pattern ln_p = Pattern.compile("(\\d+) (enter|exits|seen)\\s*(northbound|southbound)?");  // Match a number followed by the word "enter".  This is the LocoNet pattern. // NOI18N
271        Matcher m = ln_p.matcher(rep);
272        return (m);
273    }
274
275    /**
276      * {@inheritDoc}
277      */
278    // Parses out a (possibly old) LnReporter-generated report string to extract the address from the front.
279    // Assumes the LocoReporter format is "NNNN [enter|exit]"
280    @Override
281    public LocoAddress getLocoAddress(String rep) {
282        // Extract the number from the head of the report string
283        log.debug("report string: {}", rep);
284        Matcher m = this.parseReport(rep);
285        if ((m != null) && m.find()) {
286            log.debug("Parsed address: {}", m.group(1));
287            return (new DccLocoAddress(Integer.parseInt(m.group(1)), LocoAddress.Protocol.DCC));
288        } else {
289            return (null);
290        }
291    }
292
293    /**
294      * {@inheritDoc}
295      */
296    // Parses out a (possibly old) LnReporter-generated report string to extract the direction from the end.
297    // Assumes the LocoReporter format is "NNNN [enter|exit]"
298    @Override
299    public PhysicalLocationReporter.Direction getDirection(String rep) {
300        // Extract the direction from the tail of the report string
301        log.debug("report string: {}", rep); // NOI18N
302        Matcher m = this.parseReport(rep);
303        if (m.find()) {
304            log.debug("Parsed direction: {}", m.group(2)); // NOI18N
305            switch (m.group(2)) {
306                case "enter":  // LocoNet Enter message // NOI18N
307                case "seen":   // Lissy message. Treat both as "entry" messages. // NOI18N
308                    return (PhysicalLocationReporter.Direction.ENTER);
309                default:
310                    return (PhysicalLocationReporter.Direction.EXIT);
311            }
312        } else {
313            return (PhysicalLocationReporter.Direction.UNKNOWN);
314        }
315    }
316
317    /**
318      * {@inheritDoc}
319      */
320    @Override
321    public PhysicalLocation getPhysicalLocation() {
322        return (PhysicalLocation.getBeanPhysicalLocation(this));
323    }
324
325    /**
326      * {@inheritDoc}
327      */
328    // Does not use the parameter S.
329    @Override
330    public PhysicalLocation getPhysicalLocation(String s) {
331        return (PhysicalLocation.getBeanPhysicalLocation(this));
332    }
333
334
335    // Collecting Reporter Interface methods
336    /**
337      * {@inheritDoc}
338      */
339     @Override
340     public java.util.Collection<Object> getCollection(){
341        return entrySet;
342     }
343
344    // data members
345    private final int _number;   // LocoNet Reporter number
346    private final HashSet<Object> entrySet;
347
348    private final static Logger log = LoggerFactory.getLogger(LnReporter.class);
349
350}