001package jmri.jmrix.openlcb;
002
003import jmri.InstanceManager;
004import jmri.NamedBean;
005import jmri.RailCom;
006import jmri.RailComManager;
007import jmri.implementation.AbstractIdTagReporter;
008import jmri.implementation.AbstractReporter;
009import org.openlcb.Connection;
010import org.openlcb.ConsumerRangeIdentifiedMessage;
011import org.openlcb.EventID;
012import org.openlcb.EventState;
013import org.openlcb.Message;
014import org.openlcb.OlcbInterface;
015import org.openlcb.ProducerConsumerEventReportMessage;
016import org.openlcb.ProducerIdentifiedMessage;
017import org.openlcb.implementations.EventTable;
018import org.slf4j.Logger;
019import org.slf4j.LoggerFactory;
020
021import javax.annotation.CheckReturnValue;
022import javax.annotation.Nonnull;
023import javax.annotation.OverridingMethodsMustInvokeSuper;
024
025/**
026 * Implement jmri.AbstractReporter for OpenLCB protocol.
027 *
028 * @author Bob Jacobsen Copyright (C) 2008, 2010, 2011
029 * @author Balazs Racz Copyright (C) 2023
030 * @since 5.3.5
031 */
032public class OlcbReporter extends AbstractIdTagReporter {
033
034    /// How many bits does a reporter event range contain.
035    private static final int REPORTER_BIT_COUNT = 16;
036    /// Next bit in the event ID beyond the reporter event range.
037    private static final long REPORTER_LSB = (1L << REPORTER_BIT_COUNT);
038    /// Mask for the bits which are the actual report.
039    private static final long REPORTER_EVENT_MASK = REPORTER_LSB - 1;
040
041    /// When this bit is set, the report is an exit report.
042    private static final long EXIT_BIT = (1L << 14);
043    /// When this bit is set, the orientation of the locomotive is reverse, when clear it is normal.
044    private static final long ORIENTATION_BIT = (1L << 15);
045
046    /// Mask for the address bits of the reporter.
047    private static final long ADDRESS_MASK = (1L << 14) - 1;
048    /// The high bits of the address report for a DCC short address.
049    private static final int HIBITS_SHORTADDRESS = 0x28;
050    /// The high bits of the address report for a DCC consist address.
051    private static final int HIBITS_CONSIST = 0x29;
052
053    private OlcbAddress baseAddress;    // event ID for zero report
054    private EventID baseEventID;
055    private long baseEventNumber;
056    private final OlcbInterface iface;
057    private final Connection messageListener = new Receiver();
058
059    EventTable.EventTableEntryHolder baseEventTableEntryHolder = null;
060
061    public OlcbReporter(String prefix, String address, OlcbInterface iface) {
062        super(prefix + "R" + address);
063        this.iface = iface;
064        init(address);
065    }
066
067    /**
068     * Common initialization for both constructors.
069     * <p>
070     *
071     */
072    private void init(String address) {
073        iface.registerMessageListener(messageListener);
074        // build local addresses
075        OlcbAddress a = new OlcbAddress(address);
076        OlcbAddress[] v = a.split();
077        if (v == null) {
078            log.error("Did not find usable system name: {}", address);
079            return;
080        }
081        switch (v.length) {
082            case 1:
083                baseAddress = v[0];
084                baseEventID = baseAddress.toEventID();
085                baseEventNumber = baseEventID.toLong();
086                break;
087            default:
088                log.error("Can't parse OpenLCB Reporter system name: {}", address);
089        }
090    }
091
092    /**
093     * Helper function that will be invoked after construction once the properties have been
094     * loaded. Used specifically for preventing double initialization when loading sensors from
095     * XML.
096     */
097    void finishLoad() {
098        if (baseEventTableEntryHolder != null) {
099            baseEventTableEntryHolder.release();
100            baseEventTableEntryHolder = null;
101        }
102        baseEventTableEntryHolder = iface.getEventTable().addEvent(baseEventID, getEventName());
103        // Reports identified message.
104        Message m = new ConsumerRangeIdentifiedMessage(iface.getNodeId(), getEventRangeID());
105        iface.getOutputConnection().put(m, messageListener);
106    }
107
108    /**
109     * Computes the 64-bit representation of the event range covered by this reporter.
110     * This is defined for the Producer/Consumer Range identified messages in the OpenLCB
111     * standards.
112     * @return Event ID representing the event base address and the mask.
113     */
114    private EventID getEventRangeID() {
115        long eventRange = baseEventNumber;
116        if ((baseEventNumber & REPORTER_LSB) == 0) {
117            eventRange |= REPORTER_EVENT_MASK;
118        }
119        byte[] contents = new byte[8];
120        for (int i = 1; i <= 8; i++) {
121            contents[8-i] = (byte)(eventRange & 0xff);
122            eventRange >>= 8;
123        }
124        return new EventID(contents);
125    }
126
127    /**
128     * Computes the display name of a given event to be entered into the Event Table.
129     * @return user-visible string to represent this event.
130     */
131    private String getEventName() {
132        String name = getUserName();
133        if (name == null) name = mSystemName;
134        return Bundle.getMessage("ReporterEventName", name);
135    }
136
137    /**
138     * Updates event table entries when the user name changes.
139     * @param s new user name
140     * @throws BadUserNameException see {@link NamedBean}
141     */
142    @Override
143    @OverridingMethodsMustInvokeSuper
144    public void setUserName(String s) throws BadUserNameException {
145        super.setUserName(s);
146        if (baseEventTableEntryHolder != null) {
147            baseEventTableEntryHolder.getEntry().updateDescription(getEventName());
148        }
149    }
150
151    @Override
152    public void dispose() {
153        if (baseEventTableEntryHolder != null) {
154            baseEventTableEntryHolder.release();
155            baseEventTableEntryHolder = null;
156        }
157        iface.unRegisterMessageListener(messageListener);
158        super.dispose();
159    }
160
161    /**
162     * {@inheritDoc}
163     *
164     * Sorts by decoded EventID(s)
165     */
166    @CheckReturnValue
167    @Override
168    public int compareSystemNameSuffix(@Nonnull String suffix1, @Nonnull String suffix2, @Nonnull NamedBean n) {
169        return OlcbAddress.compareSystemNameSuffix(suffix1, suffix2);
170    }
171
172    /**
173     * State is always an integer, which is the numeric value from the last loco
174     * address that we reported, or -1 if the last update was an exit.
175     *
176     * @return loco address number or -1 if the last message specified exiting
177     */
178    @Override
179    public int getState() {
180        return lastLoco;
181    }
182
183    /**
184     * {@inheritDoc}
185     */
186    @Override
187    public void setState(int s) {
188        lastLoco = s;
189    }
190    int lastLoco = -1;
191
192    /**
193     * Callback from the message decoder when a relevant event message arrives.
194     * @param reportBits The bottom 14 bits of the event report. (THe top bits are already checked against our base event number)
195     * @param isEntry true for entry, false for exit
196     */
197    private void handleReport(long reportBits, boolean isEntry) {
198        // The extra notify with null is necessary to clear past notifications even if we have a new report.
199        notify(null);
200        if (!isEntry || ((reportBits & EXIT_BIT) != 0)) {
201            return;
202        }
203        long addressBits = reportBits & ADDRESS_MASK;
204        int address = 0;
205        int hiBits = (int) ((addressBits >> 8) & 0x3f);
206        int direction = (int) (reportBits & ORIENTATION_BIT);
207        if (addressBits < 0x2800) {
208            address = (int) addressBits;
209        } else if (hiBits == HIBITS_SHORTADDRESS) {
210            address = (int) (addressBits & 0xff);
211        } else if (hiBits == HIBITS_CONSIST) {
212            address = (int) (addressBits & 0x7f);
213        }
214        RailCom tag = (RailCom) InstanceManager.getDefault(RailComManager.class).provideIdTag("" + address);
215        if (direction != 0) {
216            tag.setOrientation(RailCom.ORIENTB);
217        } else {
218            tag.setOrientation(RailCom.ORIENTA);
219        }
220        notify(tag);
221    }
222    private class Receiver extends org.openlcb.MessageDecoder {
223        @Override
224        public void handleProducerConsumerEventReport(ProducerConsumerEventReportMessage msg, Connection sender) {
225            long id = msg.getEventID().toLong();
226            if ((id & ~REPORTER_EVENT_MASK) != baseEventNumber) {
227                // Not for us.
228                return;
229            }
230            handleReport(id & REPORTER_EVENT_MASK, true);
231        }
232
233        @Override
234        public void handleProducerIdentified(ProducerIdentifiedMessage msg, Connection sender) {
235            long id = msg.getEventID().toLong();
236            if ((id & ~REPORTER_EVENT_MASK) != baseEventNumber) {
237                // Not for us.
238                return;
239            }
240            if (msg.getEventState() == EventState.Invalid) {
241                handleReport(id & REPORTER_EVENT_MASK, false);
242            } else if (msg.getEventState() == EventState.Valid) {
243                handleReport(id & REPORTER_EVENT_MASK, true);
244            }
245        }
246    }
247
248    private final static Logger log = LoggerFactory.getLogger(OlcbReporter.class);
249
250}