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