001package jmri.jmrix.openlcb;
002
003import jmri.CollectingReporter;
004import jmri.DccLocoAddress;
005import jmri.IdTag;
006import jmri.InstanceManager;
007import jmri.NamedBean;
008import jmri.RailCom;
009import jmri.RailComManager;
010import jmri.implementation.AbstractIdTagReporter;
011import jmri.jmrix.can.CanSystemConnectionMemo;
012
013import org.openlcb.Connection;
014import org.openlcb.ConsumerRangeIdentifiedMessage;
015import org.openlcb.EventID;
016import org.openlcb.EventState;
017import org.openlcb.Message;
018import org.openlcb.OlcbInterface;
019import org.openlcb.ProducerConsumerEventReportMessage;
020import org.openlcb.ProducerIdentifiedMessage;
021import org.openlcb.implementations.EventTable;
022
023import java.util.Collection;
024import java.util.HashSet;
025import java.util.Set;
026
027import javax.annotation.CheckReturnValue;
028import javax.annotation.Nonnull;
029import javax.annotation.OverridingMethodsMustInvokeSuper;
030
031/**
032 * Implement jmri.AbstractReporter for OpenLCB protocol.
033 *
034 * @author Bob Jacobsen Copyright (C) 2008, 2010, 2011
035 * @author Balazs Racz Copyright (C) 2023
036 * @since 5.3.5
037 */
038public final class OlcbReporter extends AbstractIdTagReporter implements CollectingReporter {
039
040    /// How many bits does a reporter event range contain.
041    private static final int REPORTER_BIT_COUNT = 16;
042    /// Next bit in the event ID beyond the reporter event range.
043    private static final long REPORTER_LSB = (1L << REPORTER_BIT_COUNT);
044    /// Mask for the bits which are the actual report.
045    private static final long REPORTER_EVENT_MASK = REPORTER_LSB - 1;
046
047    // the four cases for the MS bits in the report
048    private static final int REPORTER_UNOCCUPIED_EXIT = 0;
049    private static final int REPORTER_OCCUPIED_FORWARD_ENTRY = 0x1;
050    private static final int REPORTER_OCCUPIED_BACKWARD_ENTRY = 0x2;
051    private static final int REPORTER_OCCUPIED_UNKNOWN_ENTRY = 0x3;
052
053    /// Mask for the address bits of the reporter.
054    private static final long ADDRESS_MASK = (1L << 14) - 1;
055    /// The high bits of the address report for a DCC short address.
056    private static final int HIBITS_SHORTADDRESS = 0x38;
057    /// The high bits of the address report for a DCC consist address.
058    private static final int HIBITS_CONSIST = 0x39;
059
060    private OlcbAddress baseAddress;    // event ID for zero report
061    private EventID baseEventID;
062    private long baseEventNumber;
063    private final OlcbInterface iface;
064    private final CanSystemConnectionMemo memo;
065    private final Connection messageListener = new Receiver();
066
067    EventTable.EventTableEntryHolder baseEventTableEntryHolder = null;
068
069    Set<Object> entrySet = new HashSet<>();
070    
071    public OlcbReporter(String prefix, String address, CanSystemConnectionMemo memo) {
072        super(prefix + "R" + address);
073        this.memo = memo;
074        if (memo != null) { // greatly simplify testing
075            this.iface = memo.get(OlcbInterface.class);
076        } else {
077            this.iface = null;
078        }
079        init(address);
080    }
081
082    /**
083     * Common initialization for both constructors.
084     * <p>
085     *
086     */
087    private void init(String address) {
088        iface.registerMessageListener(messageListener);
089        // build local addresses
090        OlcbAddress a = new OlcbAddress(address, memo);
091        OlcbAddress[] v = a.split(memo);
092        if (v == null) {
093            log.error("Did not find usable system name: {}", address);
094            return;
095        }
096        switch (v.length) {
097            case 1:
098                baseAddress = v[0];
099                baseEventID = baseAddress.toEventID();
100                baseEventNumber = baseEventID.toLong();
101                break;
102            default:
103                log.error("Can't parse OpenLCB Reporter system name: {}", address);
104        }
105    }
106
107    /**
108     * Helper function that will be invoked after construction once the properties have been
109     * loaded. Used specifically for preventing double initialization when loading sensors from
110     * XML.
111     */
112    void finishLoad() {
113        if (baseEventTableEntryHolder != null) {
114            baseEventTableEntryHolder.release();
115            baseEventTableEntryHolder = null;
116        }
117        baseEventTableEntryHolder = iface.getEventTable().addEvent(baseEventID, getEventName());
118        // Reports identified message.
119        Message m = new ConsumerRangeIdentifiedMessage(iface.getNodeId(), getEventRangeID());
120        iface.getOutputConnection().put(m, messageListener);
121    }
122
123    /**
124     * Computes the 64-bit representation of the event range covered by this reporter.
125     * This is defined for the Producer/Consumer Range identified messages in the OpenLCB
126     * standards.
127     * @return Event ID representing the event base address and the mask.
128     */
129    private EventID getEventRangeID() {
130        long eventRange = baseEventNumber;
131        if ((baseEventNumber & REPORTER_LSB) == 0) {
132            eventRange |= REPORTER_EVENT_MASK;
133        }
134        byte[] contents = new byte[8];
135        for (int i = 1; i <= 8; i++) {
136            contents[8-i] = (byte)(eventRange & 0xff);
137            eventRange >>= 8;
138        }
139        return new EventID(contents);
140    }
141
142    /**
143     * Computes the display name of a given event to be entered into the Event Table.
144     * @return user-visible string to represent this event.
145     */
146    private String getEventName() {
147        String name = getUserName();
148        if (name == null) name = mSystemName;
149        return Bundle.getMessage("ReporterEventName", name);
150    }
151
152    /**
153     * Updates event table entries when the user name changes.
154     * @param s new user name
155     * @throws BadUserNameException see {@link NamedBean}
156     */
157    @Override
158    @OverridingMethodsMustInvokeSuper
159    public void setUserName(String s) throws BadUserNameException {
160        super.setUserName(s);
161        if (baseEventTableEntryHolder != null) {
162            baseEventTableEntryHolder.getEntry().updateDescription(getEventName());
163        }
164    }
165
166    @Override
167    public void dispose() {
168        if (baseEventTableEntryHolder != null) {
169            baseEventTableEntryHolder.release();
170            baseEventTableEntryHolder = null;
171        }
172        iface.unRegisterMessageListener(messageListener);
173        super.dispose();
174    }
175
176    /**
177     *  {@inheritDoc}
178     */
179    @Override
180    @CheckReturnValue
181    public Collection<Object> getCollection() {
182        return entrySet;
183    }
184    
185    /**
186     * {@inheritDoc}
187     *
188     * Sorts by decoded EventID(s)
189     */
190    @CheckReturnValue
191    @Override
192    public int compareSystemNameSuffix(@Nonnull String suffix1, @Nonnull String suffix2, @Nonnull NamedBean n) {
193        return OlcbAddress.compareSystemNameSuffix(suffix1, suffix2, memo);
194    }
195
196    /**
197     * State is always an integer, which is the numeric value from the last loco
198     * address that we reported, or -1 if the last update was an exit.
199     *
200     * @return loco address number or -1 if the last message specified exiting
201     */
202    @Override
203    public int getState() {
204        return lastLoco;
205    }
206
207    /**
208     * {@inheritDoc}
209     */
210    @Override
211    public void setState(int s) {
212        lastLoco = s;
213    }
214    int lastLoco = -1;
215
216    @Override
217    public void notify(IdTag tag) {
218        log.trace("notified {} with tag {}", this, tag);
219        if (tag == null ) {
220        
221            if (log.isTraceEnabled()) {
222                for (var id : entrySet) {
223                    log.trace("  tag {} where seen {}", id, ((IdTag)id).getWhereLastSeen());
224                }
225            }
226        
227            var copySet = new HashSet<Object>(entrySet); // to avoid concurrent modification
228            copySet.stream().filter(id -> ((IdTag)id).getWhereLastSeen()!=this).forEach(entrySet::remove);
229        }
230        super.notify(tag);
231    }
232    
233    /**
234     * Callback from the message decoder when a relevant event message arrives.
235     * @param reportBits The bottom 14 bits of the event report. (THe top bits are already checked against our base event number)
236     * @param isEntry true for entry, false for exit
237     */
238    private void handleReport(long reportBits, boolean isEntry) {
239        log.trace("handleReport {} with isEntry {}", this, isEntry);
240        // Remove any tags held here if they've been moved to another reporter
241        var copySet = new HashSet<Object>(entrySet); // to avoid concurrent modification
242        copySet.stream().filter(id -> ((IdTag)id).getWhereLastSeen()!=this).forEach(entrySet::remove);
243
244        // The extra notify with null is necessary to clear past notifications even if we have a new report.
245        notify(null);
246        
247        DccLocoAddress.Protocol protocol;
248        boolean isConsist;
249        long addressBits = reportBits & ADDRESS_MASK;
250        int address = 0;
251        int hiBits = (int) ((addressBits >> 8) & 0x3f);
252        if (addressBits < 0x2800) {
253            address = (int) addressBits;
254            protocol = DccLocoAddress.Protocol.DCC_LONG;
255            isConsist = false;
256        } else if (hiBits == HIBITS_SHORTADDRESS) {
257            address = (int) (addressBits & 0xff);
258            protocol = DccLocoAddress.Protocol.DCC_SHORT;
259            isConsist = false;
260        } else if (hiBits == HIBITS_CONSIST) {
261            address = (int) (addressBits & 0x7f);
262            protocol = DccLocoAddress.Protocol.DCC_SHORT;
263            isConsist = true;
264        } else {
265            log.warn("Unexpected address field formatting, treating as DCC_LONG: {}", Long.toHexString(reportBits));
266            protocol = DccLocoAddress.Protocol.DCC_LONG;
267            isConsist = false;
268        }
269        
270        RailCom.Direction direction;
271        
272        int directionBits = (int)(reportBits >> 14) & 0x3;
273        
274        switch ( directionBits ) {
275            case REPORTER_UNOCCUPIED_EXIT:
276                direction = RailCom.Direction.UNKNOWN;
277                break;
278            case REPORTER_OCCUPIED_FORWARD_ENTRY:
279                direction = RailCom.Direction.FORWARD;
280                break;
281            case REPORTER_OCCUPIED_BACKWARD_ENTRY:
282                direction = RailCom.Direction.BACKWARD;
283                break;
284            default:        // needed to keep static checker happy
285            case REPORTER_OCCUPIED_UNKNOWN_ENTRY:
286                direction = RailCom.Direction.UNKNOWN;
287                break;
288        }
289
290        // address 0x3800 is a special case:  Arrival means reporter is unoccupied, departure is ignored
291        if (addressBits == 0x3800) {
292            if (directionBits == REPORTER_UNOCCUPIED_EXIT) {
293                return;
294            } else {
295                log.trace("{} clearing collection", this);
296                entrySet.clear();
297                return; // having cleared the reporter earlier
298            }
299        }
300
301        RailCom tag = (RailCom) InstanceManager.getDefault(RailComManager.class).provideIdTag("" + address);
302
303        if (!isEntry || directionBits == REPORTER_UNOCCUPIED_EXIT) {
304            log.trace("{} removes tag {}", this,  tag);
305            entrySet.remove(tag);
306            return; // having cleared the reporter earlier
307        }
308        
309        entrySet.add(tag);
310        tag.setOrientation(RailCom.Orientation.UNKNOWN);
311        tag.setDirection(direction);
312        tag.setDccAddress(new DccLocoAddress(address, protocol, isConsist));
313        notify(tag);
314    }
315    private class Receiver extends org.openlcb.MessageDecoder {
316        @Override
317        public void handleProducerConsumerEventReport(ProducerConsumerEventReportMessage msg, Connection sender) {
318            long id = msg.getEventID().toLong();
319            if ((id & ~REPORTER_EVENT_MASK) != baseEventNumber) {
320                // Not for us.
321                return;
322            }
323            boolean entry = ((id >>14) & 0x3) != REPORTER_UNOCCUPIED_EXIT;
324            handleReport(id & REPORTER_EVENT_MASK, entry);
325        }
326
327        @Override
328        public void handleProducerIdentified(ProducerIdentifiedMessage msg, Connection sender) {
329            long id = msg.getEventID().toLong();
330            if ((id & ~REPORTER_EVENT_MASK) != baseEventNumber) {
331                // Not for us.
332                return;
333            }
334            if (msg.getEventState() == EventState.Invalid) {
335                handleReport(id & REPORTER_EVENT_MASK, false);
336            } else if (msg.getEventState() == EventState.Valid) {
337                handleReport(id & REPORTER_EVENT_MASK, true);
338            }
339        }
340    }
341
342    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(OlcbReporter.class);
343
344}