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}