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