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}