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.Reporter; 011import jmri.implementation.AbstractIdTagReporter; 012import jmri.jmrix.can.CanSystemConnectionMemo; 013 014import org.openlcb.Connection; 015import org.openlcb.ConsumerRangeIdentifiedMessage; 016import org.openlcb.EventID; 017import org.openlcb.EventState; 018import org.openlcb.IdentifyProducersMessage; 019import org.openlcb.Message; 020import org.openlcb.OlcbInterface; 021import org.openlcb.ProducerConsumerEventReportMessage; 022import org.openlcb.ProducerIdentifiedMessage; 023import org.openlcb.implementations.EventTable; 024 025import java.util.ArrayList; 026import java.util.Collection; 027import java.util.Collections; 028import java.util.List; 029 030import javax.annotation.CheckReturnValue; 031import javax.annotation.Nonnull; 032import javax.annotation.OverridingMethodsMustInvokeSuper; 033 034/** 035 * Implement jmri.AbstractReporter for OpenLCB protocol. 036 * 037 * @author Bob Jacobsen Copyright (C) 2008, 2010, 2011 038 * @author Balazs Racz Copyright (C) 2023 039 * @since 5.3.5 040 */ 041public final class OlcbReporter extends AbstractIdTagReporter implements CollectingReporter { 042 043 /// How many bits does a reporter event range contain. 044 private static final int REPORTER_BIT_COUNT = 16; 045 /// Next bit in the event ID beyond the reporter event range. 046 private static final long REPORTER_LSB = (1L << REPORTER_BIT_COUNT); 047 /// Mask for the bits which are the actual report. 048 private static final long REPORTER_EVENT_MASK = REPORTER_LSB - 1; 049 050 // the four cases for the MS bits in the report 051 private static final int REPORTER_UNOCCUPIED_EXIT = 0; 052 private static final int REPORTER_OCCUPIED_WEST_ENTRY = 0x1; 053 private static final int REPORTER_OCCUPIED_EAST_ENTRY = 0x2; 054 private static final int REPORTER_OCCUPIED_UNKNOWN_ENTRY = 0x3; 055 056 /// Mask for the address bits of the reporter. 057 private static final long ADDRESS_MASK = (1L << 14) - 1; 058 /// The high bits of the address report for a DCC short address. 059 private static final int HIBITS_SHORTADDRESS = 0x38; 060 /// The high bits of the address report for a DCC consist address. 061 private static final int HIBITS_CONSIST = 0x39; 062 063 private OlcbAddress baseAddress; // event ID for zero report 064 private EventID baseEventID; 065 private long baseEventNumber; 066 private final OlcbInterface iface; 067 private final CanSystemConnectionMemo memo; 068 private final Connection messageListener = new Receiver(); 069 070 EventTable.EventTableEntryHolder baseEventTableEntryHolder = null; 071 072 private final List<IdTag> entryOrder = new ArrayList<>(); 073 074 /** 075 * {@inheritDoc} 076 * <p> 077 * OpenLCB provides explicit exit events and section-cleared messages. 078 */ 079 @Override 080 public boolean hasExitReports() { 081 return true; 082 } 083 084 public OlcbReporter(String prefix, String address, CanSystemConnectionMemo memo) { 085 super(prefix + "R" + address); 086 this.memo = memo; 087 if (memo != null) { // greatly simplify testing 088 this.iface = memo.get(OlcbInterface.class); 089 } else { 090 this.iface = null; 091 } 092 init(address); 093 } 094 095 /** 096 * Common initialization for both constructors. 097 * <p> 098 * 099 */ 100 private void init(String address) { 101 iface.registerMessageListener(messageListener); 102 // build local addresses 103 OlcbAddress a = new OlcbAddress(address, memo); 104 OlcbAddress[] v = a.split(memo); 105 if (v == null) { 106 log.error("Did not find usable system name: {}", address); 107 return; 108 } 109 switch (v.length) { 110 case 1: 111 baseAddress = v[0]; 112 baseEventID = baseAddress.toEventID(); 113 baseEventNumber = baseEventID.toLong(); 114 break; 115 default: 116 log.error("Can't parse OpenLCB Reporter system name: {}", address); 117 } 118 } 119 120 /** 121 * Helper function that will be invoked after construction once the properties have been 122 * loaded. Used specifically for preventing double initialization when loading sensors from 123 * XML. 124 */ 125 void finishLoad() { 126 if (baseEventTableEntryHolder != null) { 127 baseEventTableEntryHolder.release(); 128 baseEventTableEntryHolder = null; 129 } 130 if (baseEventID != null && iface != null) { 131 baseEventTableEntryHolder = iface.getEventTable().addEvent(baseEventID, getEventName()); 132 // Reports identified message. 133 Message m = new ConsumerRangeIdentifiedMessage(iface.getNodeId(), getEventRangeID()); 134 iface.getOutputConnection().put(m, messageListener); 135 // Queries current state of the reporter. 136 m = new IdentifyProducersMessage(iface.getNodeId(), baseEventID); 137 iface.getOutputConnection().put(m, messageListener); 138 } 139 } 140 141 /** 142 * Computes the 64-bit representation of the event range covered by this reporter. 143 * This is defined for the Producer/Consumer Range identified messages in the OpenLCB 144 * standards. 145 * @return Event ID representing the event base address and the mask. 146 */ 147 private EventID getEventRangeID() { 148 long eventRange = baseEventNumber; 149 if ((baseEventNumber & REPORTER_LSB) == 0) { 150 eventRange |= REPORTER_EVENT_MASK; 151 } 152 byte[] contents = new byte[8]; 153 for (int i = 1; i <= 8; i++) { 154 contents[8-i] = (byte)(eventRange & 0xff); 155 eventRange >>= 8; 156 } 157 return new EventID(contents); 158 } 159 160 /** 161 * Computes the display name of a given event to be entered into the Event Table. 162 * @return user-visible string to represent this event. 163 */ 164 private String getEventName() { 165 String name = getUserName(); 166 if (name == null) name = mSystemName; 167 return Bundle.getMessage("ReporterEventName", name); 168 } 169 170 /** 171 * Updates event table entries when the user name changes. 172 * @param s new user name 173 * @throws BadUserNameException see {@link NamedBean} 174 */ 175 @Override 176 @OverridingMethodsMustInvokeSuper 177 public void setUserName(String s) throws BadUserNameException { 178 super.setUserName(s); 179 if (baseEventTableEntryHolder != null) { 180 baseEventTableEntryHolder.getEntry().updateDescription(getEventName()); 181 } 182 } 183 184 @Override 185 public void dispose() { 186 if (baseEventTableEntryHolder != null) { 187 baseEventTableEntryHolder.release(); 188 baseEventTableEntryHolder = null; 189 } 190 iface.unRegisterMessageListener(messageListener); 191 super.dispose(); 192 } 193 194 /** 195 * {@inheritDoc} 196 */ 197 @Override 198 @CheckReturnValue 199 @SuppressWarnings("unchecked") 200 public Collection<Object> getCollection() { 201 return (Collection<Object>)(Collection<?>) Collections.unmodifiableCollection(entryOrder); 202 } 203 204 /** 205 * {@inheritDoc} 206 * 207 * Sorts by decoded EventID(s) 208 */ 209 @CheckReturnValue 210 @Override 211 public int compareSystemNameSuffix(@Nonnull String suffix1, @Nonnull String suffix2, @Nonnull NamedBean n) { 212 return OlcbAddress.compareSystemNameSuffix(suffix1, suffix2, memo); 213 } 214 215 /** 216 * State is always an integer, which is the numeric value from the last loco 217 * address that we reported, or -1 if the last update was an exit. 218 * 219 * @return loco address number or -1 if the last message specified exiting 220 */ 221 @Override 222 public int getState() { 223 return lastLoco; 224 } 225 226 /** 227 * {@inheritDoc} 228 */ 229 @Override 230 public void setState(int s) { 231 lastLoco = s; 232 } 233 int lastLoco = -1; 234 235 /** 236 * Callback from the message decoder when a relevant event message arrives. 237 * @param reportBits The bottom 14 bits of the event report. (THe top bits are already checked against our base event number) 238 * @param isEntry true for entry, false for exit 239 */ 240 private void handleReport(long reportBits, boolean isEntry) { 241 log.trace("handleReport {} with isEntry {}", this, isEntry); 242 243 DccLocoAddress.Protocol protocol; 244 boolean isConsist; 245 long addressBits = reportBits & ADDRESS_MASK; 246 int address = 0; 247 int hiBits = (int) ((addressBits >> 8) & 0x3f); 248 if (addressBits < 0x2800) { 249 address = (int) addressBits; 250 protocol = DccLocoAddress.Protocol.DCC_LONG; 251 isConsist = false; 252 } else if (hiBits == HIBITS_SHORTADDRESS) { 253 address = (int) (addressBits & 0xff); 254 protocol = DccLocoAddress.Protocol.DCC_SHORT; 255 isConsist = false; 256 } else if (hiBits == HIBITS_CONSIST) { 257 address = (int) (addressBits & 0x7f); 258 protocol = DccLocoAddress.Protocol.DCC_SHORT; 259 isConsist = true; 260 } else { 261 log.warn("Unexpected address field formatting, treating as DCC_LONG: {}", Long.toHexString(reportBits)); 262 protocol = DccLocoAddress.Protocol.DCC_LONG; 263 isConsist = false; 264 } 265 266 RailCom.Orientation orientation; 267 268 int orientationBits = (int)(reportBits >> 14) & 0x3; 269 270 switch ( orientationBits ) { 271 case REPORTER_OCCUPIED_WEST_ENTRY: 272 orientation = RailCom.Orientation.WEST; 273 break; 274 case REPORTER_OCCUPIED_EAST_ENTRY: 275 orientation = RailCom.Orientation.EAST; 276 break; 277 case REPORTER_OCCUPIED_UNKNOWN_ENTRY: 278 case REPORTER_UNOCCUPIED_EXIT: 279 default: // needed to keep static checker happy 280 orientation = RailCom.Orientation.UNKNOWN; 281 break; 282 } 283 284 // address 0x3800 is a special case: Arrival means reporter is unoccupied, departure is ignored 285 if (addressBits == 0x3800) { 286 if (orientationBits == REPORTER_UNOCCUPIED_EXIT) { 287 return; 288 } else { 289 log.trace("{} clearing collection", this); 290 entryOrder.clear(); 291 notify(null); 292 return; // having cleared the reporter earlier 293 } 294 } 295 296 RailCom tag = (RailCom) InstanceManager.getDefault(RailComManager.class).provideIdTag("" + address); 297 298 if (!isEntry || orientationBits == REPORTER_UNOCCUPIED_EXIT) { 299 log.trace("{} removes tag {}", this, tag); 300 entryOrder.remove(tag); 301 if (getCurrentReport() == tag) { 302 updateCurrentReportAfterExit(); 303 } else { 304 notifyCollectionUpdated(); 305 } 306 return; // having cleared the reporter earlier 307 } 308 309 entryOrder.remove(tag); 310 entryOrder.add(tag); 311 tag.setOrientation(orientation); 312 tag.setDccAddress(new DccLocoAddress(address, protocol, isConsist)); 313 notify(tag); 314 } 315 316 /** 317 * Updates {@link #getCurrentReport()} after an IdTag has exited this reporter's block. 318 * <p> 319 * When a locomotive exits, this method selects an appropriate fallback report from the 320 * remaining tags in {@link #entryOrder}. It prioritizes the most recently entered tag 321 * that still considers this reporter its current location (i.e. {@code candidate.getWhereLastSeen() == this}). 322 * If no such locomotive exists but the block is not empty (for example, if other locomotives 323 * were temporarily seen in bridging or nested detectors), it falls back to the most recently 324 * entered tag remaining in the block without stealing {@code whereLastSeen}. 325 * If no tags remain, {@code notify(null)} clears the report. 326 */ 327 private void updateCurrentReportAfterExit() { 328 if (entryOrder.isEmpty()) { 329 notify(null); 330 return; 331 } 332 for (int i = entryOrder.size() - 1; i >= 0; i--) { 333 IdTag candidate = entryOrder.get(i); 334 if (candidate.getWhereLastSeen() == this) { 335 notify(candidate); 336 return; 337 } 338 } 339 // Fall back to the most recently entered remaining tag without modifying its 340 // whereLastSeen or notifying the reporter where it was seen elsewhere. 341 IdTag fallback = entryOrder.get(entryOrder.size() - 1); 342 setReport(fallback); 343 setState(IdTag.SEEN); 344 } 345 346 /** 347 * {@inheritDoc} 348 */ 349 @Override 350 public void notifySeenElsewhere(IdTag tag, Reporter newReporter) { 351 if (!this.equals(newReporter) && getCurrentReport() == tag) { 352 // The tag that just moved to another reporter was our current report. 353 // In OpenLCB, physical presence is governed by explicit hardware exit messages, 354 // so we do not remove `tag` from `entryOrder` here. 355 // However, to keep getCurrentReport() pointing to an ID that is currently present 356 // in this block, we check whether any other locomotive in entryOrder still has 357 // whereLastSeen == this. If so, we fall back to reporting that locomotive. 358 // If no other locomotive has whereLastSeen == this, we do NOT call notify(candidate) 359 // or notify(null), because doing so would re-assert whereLastSeen back to this reporter 360 // and trigger an infinite ping-pong between reporters. 361 for (int i = entryOrder.size() - 1; i >= 0; i--) { 362 IdTag candidate = entryOrder.get(i); 363 if (candidate != tag && candidate.getWhereLastSeen() == this) { 364 notify(candidate); 365 return; 366 } 367 } 368 } 369 } 370 private class Receiver extends org.openlcb.MessageDecoder { 371 @Override 372 public void handleProducerConsumerEventReport(ProducerConsumerEventReportMessage msg, Connection sender) { 373 long id = msg.getEventID().toLong(); 374 if ((id & ~REPORTER_EVENT_MASK) != baseEventNumber) { 375 // Not for us. 376 return; 377 } 378 boolean entry = ((id >>14) & 0x3) != REPORTER_UNOCCUPIED_EXIT; 379 handleReport(id & REPORTER_EVENT_MASK, entry); 380 } 381 382 @Override 383 public void handleProducerIdentified(ProducerIdentifiedMessage msg, Connection sender) { 384 long id = msg.getEventID().toLong(); 385 if ((id & ~REPORTER_EVENT_MASK) != baseEventNumber) { 386 // Not for us. 387 return; 388 } 389 long reportBits = id & REPORTER_EVENT_MASK; 390 int orientationBits = (int) (reportBits >> 14) & 0x3; 391 if (orientationBits == REPORTER_UNOCCUPIED_EXIT) { 392 // For exit events (0x0000..), Valid means the locomotive has exited (is absent). 393 // Invalid means the exit did not occur (the locomotive is present). 394 if (msg.getEventState() == EventState.Valid) { 395 handleReport(reportBits, false); 396 } 397 } else { 398 // For entry / orientation events (0x4000, 0x8000, 0xC000): 399 // Valid means the locomotive is present with this orientation. 400 if (msg.getEventState() == EventState.Valid) { 401 handleReport(reportBits, true); 402 } else if (msg.getEventState() == EventState.Invalid && 403 orientationBits == REPORTER_OCCUPIED_UNKNOWN_ENTRY) { 404 // Invalid on unknown orientation means the locomotive is not present at all. 405 handleReport(reportBits, false); 406 } 407 } 408 } 409 } 410 411 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(OlcbReporter.class); 412 413}