001package jmri.jmrix.openlcb; 002 003import jmri.CollectingReporter; 004import jmri.DccLocoAddress; 005import jmri.IdTag; 006import jmri.InstanceManager; 007import jmri.NamedBean; 008import jmri.RailCom; 009import jmri.RailComManager; 010import jmri.implementation.AbstractIdTagReporter; 011import jmri.jmrix.can.CanSystemConnectionMemo; 012 013import org.openlcb.Connection; 014import org.openlcb.ConsumerRangeIdentifiedMessage; 015import org.openlcb.EventID; 016import org.openlcb.EventState; 017import org.openlcb.Message; 018import org.openlcb.OlcbInterface; 019import org.openlcb.ProducerConsumerEventReportMessage; 020import org.openlcb.ProducerIdentifiedMessage; 021import org.openlcb.implementations.EventTable; 022 023import java.util.Collection; 024import java.util.HashSet; 025import java.util.Set; 026 027import javax.annotation.CheckReturnValue; 028import javax.annotation.Nonnull; 029import javax.annotation.OverridingMethodsMustInvokeSuper; 030 031/** 032 * Implement jmri.AbstractReporter for OpenLCB protocol. 033 * 034 * @author Bob Jacobsen Copyright (C) 2008, 2010, 2011 035 * @author Balazs Racz Copyright (C) 2023 036 * @since 5.3.5 037 */ 038public final class OlcbReporter extends AbstractIdTagReporter implements CollectingReporter { 039 040 /// How many bits does a reporter event range contain. 041 private static final int REPORTER_BIT_COUNT = 16; 042 /// Next bit in the event ID beyond the reporter event range. 043 private static final long REPORTER_LSB = (1L << REPORTER_BIT_COUNT); 044 /// Mask for the bits which are the actual report. 045 private static final long REPORTER_EVENT_MASK = REPORTER_LSB - 1; 046 047 // the four cases for the MS bits in the report 048 private static final int REPORTER_UNOCCUPIED_EXIT = 0; 049 private static final int REPORTER_OCCUPIED_FORWARD_ENTRY = 0x1; 050 private static final int REPORTER_OCCUPIED_BACKWARD_ENTRY = 0x2; 051 private static final int REPORTER_OCCUPIED_UNKNOWN_ENTRY = 0x3; 052 053 /// Mask for the address bits of the reporter. 054 private static final long ADDRESS_MASK = (1L << 14) - 1; 055 /// The high bits of the address report for a DCC short address. 056 private static final int HIBITS_SHORTADDRESS = 0x38; 057 /// The high bits of the address report for a DCC consist address. 058 private static final int HIBITS_CONSIST = 0x39; 059 060 private OlcbAddress baseAddress; // event ID for zero report 061 private EventID baseEventID; 062 private long baseEventNumber; 063 private final OlcbInterface iface; 064 private final CanSystemConnectionMemo memo; 065 private final Connection messageListener = new Receiver(); 066 067 EventTable.EventTableEntryHolder baseEventTableEntryHolder = null; 068 069 Set<Object> entrySet = new HashSet<>(); 070 071 public OlcbReporter(String prefix, String address, CanSystemConnectionMemo memo) { 072 super(prefix + "R" + address); 073 this.memo = memo; 074 if (memo != null) { // greatly simplify testing 075 this.iface = memo.get(OlcbInterface.class); 076 } else { 077 this.iface = null; 078 } 079 init(address); 080 } 081 082 /** 083 * Common initialization for both constructors. 084 * <p> 085 * 086 */ 087 private void init(String address) { 088 iface.registerMessageListener(messageListener); 089 // build local addresses 090 OlcbAddress a = new OlcbAddress(address, memo); 091 OlcbAddress[] v = a.split(memo); 092 if (v == null) { 093 log.error("Did not find usable system name: {}", address); 094 return; 095 } 096 switch (v.length) { 097 case 1: 098 baseAddress = v[0]; 099 baseEventID = baseAddress.toEventID(); 100 baseEventNumber = baseEventID.toLong(); 101 break; 102 default: 103 log.error("Can't parse OpenLCB Reporter system name: {}", address); 104 } 105 } 106 107 /** 108 * Helper function that will be invoked after construction once the properties have been 109 * loaded. Used specifically for preventing double initialization when loading sensors from 110 * XML. 111 */ 112 void finishLoad() { 113 if (baseEventTableEntryHolder != null) { 114 baseEventTableEntryHolder.release(); 115 baseEventTableEntryHolder = null; 116 } 117 baseEventTableEntryHolder = iface.getEventTable().addEvent(baseEventID, getEventName()); 118 // Reports identified message. 119 Message m = new ConsumerRangeIdentifiedMessage(iface.getNodeId(), getEventRangeID()); 120 iface.getOutputConnection().put(m, messageListener); 121 } 122 123 /** 124 * Computes the 64-bit representation of the event range covered by this reporter. 125 * This is defined for the Producer/Consumer Range identified messages in the OpenLCB 126 * standards. 127 * @return Event ID representing the event base address and the mask. 128 */ 129 private EventID getEventRangeID() { 130 long eventRange = baseEventNumber; 131 if ((baseEventNumber & REPORTER_LSB) == 0) { 132 eventRange |= REPORTER_EVENT_MASK; 133 } 134 byte[] contents = new byte[8]; 135 for (int i = 1; i <= 8; i++) { 136 contents[8-i] = (byte)(eventRange & 0xff); 137 eventRange >>= 8; 138 } 139 return new EventID(contents); 140 } 141 142 /** 143 * Computes the display name of a given event to be entered into the Event Table. 144 * @return user-visible string to represent this event. 145 */ 146 private String getEventName() { 147 String name = getUserName(); 148 if (name == null) name = mSystemName; 149 return Bundle.getMessage("ReporterEventName", name); 150 } 151 152 /** 153 * Updates event table entries when the user name changes. 154 * @param s new user name 155 * @throws BadUserNameException see {@link NamedBean} 156 */ 157 @Override 158 @OverridingMethodsMustInvokeSuper 159 public void setUserName(String s) throws BadUserNameException { 160 super.setUserName(s); 161 if (baseEventTableEntryHolder != null) { 162 baseEventTableEntryHolder.getEntry().updateDescription(getEventName()); 163 } 164 } 165 166 @Override 167 public void dispose() { 168 if (baseEventTableEntryHolder != null) { 169 baseEventTableEntryHolder.release(); 170 baseEventTableEntryHolder = null; 171 } 172 iface.unRegisterMessageListener(messageListener); 173 super.dispose(); 174 } 175 176 /** 177 * {@inheritDoc} 178 */ 179 @Override 180 @CheckReturnValue 181 public Collection<Object> getCollection() { 182 return entrySet; 183 } 184 185 /** 186 * {@inheritDoc} 187 * 188 * Sorts by decoded EventID(s) 189 */ 190 @CheckReturnValue 191 @Override 192 public int compareSystemNameSuffix(@Nonnull String suffix1, @Nonnull String suffix2, @Nonnull NamedBean n) { 193 return OlcbAddress.compareSystemNameSuffix(suffix1, suffix2, memo); 194 } 195 196 /** 197 * State is always an integer, which is the numeric value from the last loco 198 * address that we reported, or -1 if the last update was an exit. 199 * 200 * @return loco address number or -1 if the last message specified exiting 201 */ 202 @Override 203 public int getState() { 204 return lastLoco; 205 } 206 207 /** 208 * {@inheritDoc} 209 */ 210 @Override 211 public void setState(int s) { 212 lastLoco = s; 213 } 214 int lastLoco = -1; 215 216 @Override 217 public void notify(IdTag tag) { 218 log.trace("notified {} with tag {}", this, tag); 219 if (tag == null ) { 220 221 if (log.isTraceEnabled()) { 222 for (var id : entrySet) { 223 log.trace(" tag {} where seen {}", id, ((IdTag)id).getWhereLastSeen()); 224 } 225 } 226 227 var copySet = new HashSet<Object>(entrySet); // to avoid concurrent modification 228 copySet.stream().filter(id -> ((IdTag)id).getWhereLastSeen()!=this).forEach(entrySet::remove); 229 } 230 super.notify(tag); 231 } 232 233 /** 234 * Callback from the message decoder when a relevant event message arrives. 235 * @param reportBits The bottom 14 bits of the event report. (THe top bits are already checked against our base event number) 236 * @param isEntry true for entry, false for exit 237 */ 238 private void handleReport(long reportBits, boolean isEntry) { 239 log.trace("handleReport {} with isEntry {}", this, isEntry); 240 // Remove any tags held here if they've been moved to another reporter 241 var copySet = new HashSet<Object>(entrySet); // to avoid concurrent modification 242 copySet.stream().filter(id -> ((IdTag)id).getWhereLastSeen()!=this).forEach(entrySet::remove); 243 244 // The extra notify with null is necessary to clear past notifications even if we have a new report. 245 notify(null); 246 247 DccLocoAddress.Protocol protocol; 248 boolean isConsist; 249 long addressBits = reportBits & ADDRESS_MASK; 250 int address = 0; 251 int hiBits = (int) ((addressBits >> 8) & 0x3f); 252 if (addressBits < 0x2800) { 253 address = (int) addressBits; 254 protocol = DccLocoAddress.Protocol.DCC_LONG; 255 isConsist = false; 256 } else if (hiBits == HIBITS_SHORTADDRESS) { 257 address = (int) (addressBits & 0xff); 258 protocol = DccLocoAddress.Protocol.DCC_SHORT; 259 isConsist = false; 260 } else if (hiBits == HIBITS_CONSIST) { 261 address = (int) (addressBits & 0x7f); 262 protocol = DccLocoAddress.Protocol.DCC_SHORT; 263 isConsist = true; 264 } else { 265 log.warn("Unexpected address field formatting, treating as DCC_LONG: {}", Long.toHexString(reportBits)); 266 protocol = DccLocoAddress.Protocol.DCC_LONG; 267 isConsist = false; 268 } 269 270 RailCom.Direction direction; 271 272 int directionBits = (int)(reportBits >> 14) & 0x3; 273 274 switch ( directionBits ) { 275 case REPORTER_UNOCCUPIED_EXIT: 276 direction = RailCom.Direction.UNKNOWN; 277 break; 278 case REPORTER_OCCUPIED_FORWARD_ENTRY: 279 direction = RailCom.Direction.FORWARD; 280 break; 281 case REPORTER_OCCUPIED_BACKWARD_ENTRY: 282 direction = RailCom.Direction.BACKWARD; 283 break; 284 default: // needed to keep static checker happy 285 case REPORTER_OCCUPIED_UNKNOWN_ENTRY: 286 direction = RailCom.Direction.UNKNOWN; 287 break; 288 } 289 290 // address 0x3800 is a special case: Arrival means reporter is unoccupied, departure is ignored 291 if (addressBits == 0x3800) { 292 if (directionBits == REPORTER_UNOCCUPIED_EXIT) { 293 return; 294 } else { 295 log.trace("{} clearing collection", this); 296 entrySet.clear(); 297 return; // having cleared the reporter earlier 298 } 299 } 300 301 RailCom tag = (RailCom) InstanceManager.getDefault(RailComManager.class).provideIdTag("" + address); 302 303 if (!isEntry || directionBits == REPORTER_UNOCCUPIED_EXIT) { 304 log.trace("{} removes tag {}", this, tag); 305 entrySet.remove(tag); 306 return; // having cleared the reporter earlier 307 } 308 309 entrySet.add(tag); 310 tag.setOrientation(RailCom.Orientation.UNKNOWN); 311 tag.setDirection(direction); 312 tag.setDccAddress(new DccLocoAddress(address, protocol, isConsist)); 313 notify(tag); 314 } 315 private class Receiver extends org.openlcb.MessageDecoder { 316 @Override 317 public void handleProducerConsumerEventReport(ProducerConsumerEventReportMessage msg, Connection sender) { 318 long id = msg.getEventID().toLong(); 319 if ((id & ~REPORTER_EVENT_MASK) != baseEventNumber) { 320 // Not for us. 321 return; 322 } 323 boolean entry = ((id >>14) & 0x3) != REPORTER_UNOCCUPIED_EXIT; 324 handleReport(id & REPORTER_EVENT_MASK, entry); 325 } 326 327 @Override 328 public void handleProducerIdentified(ProducerIdentifiedMessage msg, Connection sender) { 329 long id = msg.getEventID().toLong(); 330 if ((id & ~REPORTER_EVENT_MASK) != baseEventNumber) { 331 // Not for us. 332 return; 333 } 334 if (msg.getEventState() == EventState.Invalid) { 335 handleReport(id & REPORTER_EVENT_MASK, false); 336 } else if (msg.getEventState() == EventState.Valid) { 337 handleReport(id & REPORTER_EVENT_MASK, true); 338 } 339 } 340 } 341 342 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(OlcbReporter.class); 343 344}