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