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}