001package jmri.jmrix.can.cbus; 002 003import javax.annotation.Nonnull; 004 005import jmri.*; 006import jmri.implementation.AbstractRailComReporter; 007import jmri.jmrix.can.*; 008import jmri.util.ThreadingUtil; 009 010/** 011 * Extend jmri.AbstractRailComReporter for CBUS controls. 012 * <hr> 013 * This file is part of JMRI. 014 * <p> 015 * JMRI is free software; you can redistribute it and/or modify it under the 016 * terms of version 2 of the GNU General Public License as published by the Free 017 * Software Foundation. See the "COPYING" file for a copy of this license. 018 * <p> 019 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY 020 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 021 * A PARTICULAR PURPOSE. See the GNU General Public License for more details. 022 * <p> 023 * 024 * CBUS Reporters can accept 025 * 5-byte unique Classic RFID on DDES or ACDAT OPCs, 026 * CANRC522 / CANRCOM DDES OPCs. 027 * 028 * @author Mark Riddoch Copyright (C) 2015 029 * @author Steve Young Copyright (c) 2019, 2020 030 * 031 */ 032public class CbusReporter extends AbstractRailComReporter implements CanListener { 033 034 private final int _number; 035 private final CanSystemConnectionMemo _memo; 036 037 038 /** 039 * Should all CbusReporters clear themselves after a timeout? 040 * <p> 041 * Default behavior is to not timeout; this is public access 042 * so it can be updated from a script 043 */ 044 public static boolean eraseOnTimeoutAll = false; 045 046 /** 047 * Should this CbusReporter clear itself after a timeout? 048 * <p> 049 * Default behavior is to not timeout; this is public access 050 * so it can be updated from a script 051 */ 052 public boolean eraseOnTimeoutThisReporter = false; 053 054 /** 055 * Create a new CbusReporter. 056 * 057 * 058 * @param address Reporter address, currently in String number format. No system prefix or type letter. 059 * @param memo System connection. 060 */ 061 public CbusReporter(String address, CanSystemConnectionMemo memo) { // a human-readable Reporter number must be specified! 062 super(memo.getSystemPrefix() + "R" + address); // can't use prefix here, as still in construction 063 _number = Integer.parseInt( address); 064 _memo = memo; 065 // At construction, don't register for messages; they're sent via the CbusReporterManager 066 // tc = memo.getTrafficController(); // can be removed when former constructor removed 067 // addTc(memo.getTrafficController()); 068 log.debug("Added new reporter {}R{}", memo.getSystemPrefix(), address); 069 } 070 071 /** 072 * {@inheritDoc} 073 * <p> 074 * CBUS hardware does not send exit reports; it relies on timeout clearing. 075 */ 076 @Override 077 public boolean hasExitReports() { 078 return false; 079 } 080 081 /** 082 * Set the CbusReporter State. 083 * 084 * May also provide / update a CBUS Sensor State, depending on property. 085 * {@inheritDoc} 086 */ 087 @Override 088 public void setState(int s) { 089 super.setState(s); 090 if ( getMaintainSensor() ) { 091 SensorManager sm = _memo.get(SensorManager.class); 092 sm.provide("+"+_number).setCommandedState( s==IdTag.SEEN ? Sensor.ACTIVE : Sensor.INACTIVE ); 093 } 094 } 095 096 /** 097 * {@inheritDoc} 098 * Resets report briefly back to null so Sensor Listeners are updated. 099 */ 100 @Override 101 public void notify(IdTag id){ 102 if ( this.getCurrentReport()!=null && id!=null ){ 103 super.notify(null); // 104 } 105 super.notify(id); 106 } 107 108 /** 109 * {@inheritDoc} 110 * CBUS Reporters can respond to ACDAT or DDES OPC's. 111 */ 112 @Override 113 public void message(CanMessage m) { 114 reply(new CanReply(m)); 115 } 116 117 /** 118 * {@inheritDoc} 119 * CBUS Reporters can respond to ACDAT or DDES OPC's 120 */ 121 @Override 122 public void reply(CanReply m) { 123 if ( m.extendedOrRtr() ) { 124 return; 125 } 126 if ( m.getOpCode() != CbusConstants.CBUS_DDES && m.getOpCode() != CbusConstants.CBUS_ACDAT) { 127 return; 128 } 129 130 if (((m.getElement(1) << 8) + m.getElement(2)) == _number) { // correct reporter number, for us 131 if (getCbusReporterType().equals(CbusReporterManager.CBUS_REPORTER_TYPE_CLASSIC)) { 132 // Classic 5 byte unique RFID tag on DDES or ACDAT, the tag 133 // bytes are opaque data and must not be inspected for a type. 134 classicRFIDReport(m); 135 } else if (m.getOpCode() == CbusConstants.CBUS_DDES) { 136 // self-describing DDES, CANRC522 or CANRCOM 137 ddesReport(m); 138 } else { 139 // ACDAT is always a 5 byte tag 140 classicRFIDReport(m); 141 } 142 } 143 } 144 145 /** 146 * Process a self-describing DDES from a CANRC522 or CANRCOM. 147 * The least significant bit of the first data byte selects the format. 148 * @param m the DDES frame 149 */ 150 private void ddesReport(CanReply m) { 151 int least_significant_bit = m.getElement(3) & 1; 152 if ( least_significant_bit == 0 ) { 153 canRc522Report(m); 154 } else { 155 canRcomReport(m); 156 } 157 } 158 159 private void classicRFIDReport(CanReply m) { 160 String buf = toClassicTag(m.getElement(3), m.getElement(4), m.getElement(5), m.getElement(6), m.getElement(7)); 161 log.debug("Reporter {} {} RFID tag read of tag: {}", this,getCbusReporterType(),buf); 162 IdTag tag = InstanceManager.getDefault(IdTagManager.class).provideIdTag(buf); 163 notify(tag); 164 startTimeout(tag); 165 } 166 167 // no DCC address correction to allow full 0-65535 range of tags on rolling stock 168 private void canRc522Report(CanReply m){ 169 String tagId = String.valueOf((m.getElement(4)<<8)+ m.getElement(5)); 170 log.debug("Reporter {} RFID tag read of tag: {}",this, tagId); 171 IdTag tag = InstanceManager.getDefault(IdTagManager.class).provideIdTag("ID"+tagId); 172 tag.setProperty("DDES Dat3", m.getElement(6)); 173 tag.setProperty("DDES Dat4", m.getElement(7)); 174 notify(tag); 175 startTimeout(tag); 176 } 177 178 179 180 private void canRcomReport(CanReply m) { 181 182 var locoAddress = parseAddress(m); 183 184 int speed = m.getElement(6)&0x7F; 185 if ((m.getElement(6)&0x80) == 0) { 186 speed = -1; // data unavailable 187 } 188 189 int flags = m.getElement(7); 190 191 RailCom.Orientation orientation; 192 switch (flags&0x03) { 193 case 2: 194 orientation = RailCom.Orientation.EAST; 195 break; 196 case 1: 197 orientation = RailCom.Orientation.WEST; 198 break; 199 case 0: 200 orientation = RailCom.Orientation.UNKNOWN; 201 break; 202 default: 203 log.warn("Unexpected orientation code 3"); 204 orientation = RailCom.Orientation.UNKNOWN; 205 break; 206 } 207 208 RailCom.Direction direction; 209 switch ((flags>>2)&0x03) { 210 case 1: 211 direction = RailCom.Direction.FORWARD; 212 break; 213 case 2: 214 direction = RailCom.Direction.BACKWARD; 215 break; 216 case 0: 217 direction = RailCom.Direction.UNKNOWN; 218 break; 219 default: 220 log.warn("Unexpected direction code 3"); 221 direction = RailCom.Direction.UNKNOWN; 222 break; 223 } 224 225 RailCom.Motion motion; 226 switch ((flags>>4)&0x03) { 227 case 1: 228 motion = RailCom.Motion.STATIONARY; 229 log.debug("Setting speed to zero because known to be not moving"); 230 speed = 0; 231 break; 232 case 2: 233 motion = RailCom.Motion.MOVING; 234 break; 235 case 0: 236 motion = RailCom.Motion.UNKNOWN; 237 break; 238 default: 239 log.warn("Unexpected motion code 3"); 240 motion = RailCom.Motion.UNKNOWN; 241 break; 242 } 243 244 RailCom.QoS qos; 245 switch ((flags>>6)&0x03) { 246 case 1: 247 qos = RailCom.QoS.POOR; 248 break; 249 case 2: 250 qos = RailCom.QoS.GOOD; 251 break; 252 case 0: 253 qos = RailCom.QoS.UNKNOWN; 254 break; 255 default: 256 log.warn("Unexpected QoS code 3"); 257 qos = RailCom.QoS.UNKNOWN; 258 break; 259 } 260 261 // RailComManager looked up when needed, not cached in a static field at class load, 262 // which could refer to a manager no longer registered with the InstanceManager. 263 var idTag = InstanceManager.getDefault(RailComManager.class).provideIdTag(""+locoAddress.getNumber()); 264 var tag = (RailCom)idTag; 265 266 tag.setDccAddress(locoAddress); 267 tag.setActualSpeed(speed); 268 tag.setOrientation(orientation); 269 tag.setDirection(direction); 270 tag.setMotion(motion); 271 tag.setQoS(qos); 272 273 notify(tag); 274 startTimeout(tag); 275 } 276 277 DccLocoAddress parseAddress(CanReply m) { // package access for testing 278 int dccTypeInt = m.getElement(4)&0xC0; 279 int dccNumber; 280 int b4 = m.getElement(4) & 0x3F; // excludes high "type" bits 281 int b5 = m.getElement(5); 282 LocoAddress.Protocol dccType; 283 boolean isConsist; 284 switch (dccTypeInt) { 285 default: 286 case 0xC0: 287 dccNumber = (b4<<8) | b5; 288 dccType = LocoAddress.Protocol.DCC_LONG; 289 isConsist = false; 290 break; 291 case 0x00: 292 dccNumber = b5; 293 dccType = LocoAddress.Protocol.DCC_SHORT; 294 isConsist = false; 295 break; 296 case 0x40: 297 dccNumber = b5&0x7F; // remove direction bit 298 dccType = LocoAddress.Protocol.DCC_SHORT; 299 isConsist = true; 300 break; 301 case 0x80: 302 int decUpper = (b4 << 1) | ((b5>>7)&0x01); // BCD upper value 303 dccNumber = decUpper*100 + (b5&0x7F); 304 dccType = LocoAddress.Protocol.DCC_LONG; 305 isConsist = true; 306 break; 307 } 308 return new DccLocoAddress(dccNumber, dccType, isConsist); 309 } 310 311 private String toClassicTag(int b1, int b2, int b3, int b4, int b5) { 312 return String.format("%02X", b1) + String.format("%02X", b2) + String.format("%02X", b3) 313 + String.format("%02X", b4) + String.format("%02X", b5); 314 } 315 316 /** 317 * Get the Reporter Listener format type. 318 * <p> 319 * Defaults to Classic RfID, 5 byte unique. 320 * @return reporter format type. 321 */ 322 @Nonnull 323 public String getCbusReporterType() { 324 Object returnVal = getProperty(CbusReporterManager.CBUS_REPORTER_DESCRIPTOR_KEY); 325 return (returnVal==null ? CbusReporterManager.CBUS_DEFAULT_REPORTER_TYPE : returnVal.toString()); 326 } 327 328 /** 329 * Get if the Reporter should provide / update a CBUS Sensor, following Reporter Status. 330 * <p> 331 * Defaults to false. 332 * @return true if the reporter should maintain the Sensor. 333 */ 334 public boolean getMaintainSensor() { 335 Boolean returnVal = (Boolean) getProperty(CbusReporterManager.CBUS_MAINTAIN_SENSOR_DESCRIPTOR_KEY); 336 return (returnVal==null ? false : returnVal); 337 } 338 339 // delay can be set to non-null memo when older constructor fully deprecated. 340 private void startTimeout(IdTag tag){ 341 // only timeout when enabled 342 if (! eraseOnTimeoutAll && ! eraseOnTimeoutThisReporter) return; 343 344 int delay = (_memo==null ? 2000 : ((CbusReporterManager)_memo.get(jmri.ReporterManager.class)).getTimeout() ); 345 // a later read restarts the timeout, also when it is the same tag again 346 final int generation = ++timeoutGeneration; 347 ThreadingUtil.runOnLayoutDelayed( () -> { 348 if (!disposed && generation == timeoutGeneration && getCurrentReport() == tag) { 349 notify(null); 350 } 351 },delay); 352 } 353 354 /** 355 * Counts the reads that started a timeout; only the timeout of the last 356 * read can clear the report. Used on the layout thread. 357 */ 358 private int timeoutGeneration = 0; 359 360 private boolean disposed = false; 361 362 /** 363 * {@inheritDoc} 364 */ 365 @Override 366 public void dispose() { 367 disposed = true; 368 super.dispose(); 369 } 370 371 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CbusReporter.class); 372}