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 private static final RailComManager railComManager = InstanceManager.getDefault(RailComManager.class); 038 039 /** 040 * Should all CbusReporters clear themselves after a timeout? 041 * <p> 042 * Default behavior is to not timeout; this is public access 043 * so it can be updated from a script 044 */ 045 public static boolean eraseOnTimeoutAll = false; 046 047 /** 048 * Should this CbusReporter clear itself after a timeout? 049 * <p> 050 * Default behavior is to not timeout; this is public access 051 * so it can be updated from a script 052 */ 053 public boolean eraseOnTimeoutThisReporter = false; 054 055 /** 056 * Create a new CbusReporter. 057 * 058 * 059 * @param address Reporter address, currently in String number format. No system prefix or type letter. 060 * @param memo System connection. 061 */ 062 public CbusReporter(String address, CanSystemConnectionMemo memo) { // a human-readable Reporter number must be specified! 063 super(memo.getSystemPrefix() + "R" + address); // can't use prefix here, as still in construction 064 _number = Integer.parseInt( address); 065 _memo = memo; 066 // At construction, don't register for messages; they're sent via the CbusReporterManager 067 // tc = memo.getTrafficController(); // can be removed when former constructor removed 068 // addTc(memo.getTrafficController()); 069 log.debug("Added new reporter {}R{}", memo.getSystemPrefix(), address); 070 } 071 072 /** 073 * Set the CbusReporter State. 074 * 075 * May also provide / update a CBUS Sensor State, depending on property. 076 * {@inheritDoc} 077 */ 078 @Override 079 public void setState(int s) { 080 super.setState(s); 081 if ( getMaintainSensor() ) { 082 SensorManager sm = _memo.get(SensorManager.class); 083 sm.provide("+"+_number).setCommandedState( s==IdTag.SEEN ? Sensor.ACTIVE : Sensor.INACTIVE ); 084 } 085 } 086 087 /** 088 * {@inheritDoc} 089 * Resets report briefly back to null so Sensor Listeners are updated. 090 */ 091 @Override 092 public void notify(IdTag id){ 093 if ( this.getCurrentReport()!=null && id!=null ){ 094 super.notify(null); // 095 } 096 super.notify(id); 097 } 098 099 /** 100 * {@inheritDoc} 101 * CBUS Reporters can respond to ACDAT or DDES OPC's. 102 */ 103 @Override 104 public void message(CanMessage m) { 105 reply(new CanReply(m)); 106 } 107 108 /** 109 * {@inheritDoc} 110 * CBUS Reporters can respond to ACDAT or DDES OPC's 111 */ 112 @Override 113 public void reply(CanReply m) { 114 if ( m.extendedOrRtr() ) { 115 return; 116 } 117 if ( m.getOpCode() != CbusConstants.CBUS_DDES && m.getOpCode() != CbusConstants.CBUS_ACDAT) { 118 return; 119 } 120 121 if (((m.getElement(1) << 8) + m.getElement(2)) == _number) { // correct reporter number, for us 122 if (m.getOpCode() == CbusConstants.CBUS_DDES && !getCbusReporterType().equals(CbusReporterManager.CBUS_REPORTER_TYPE_CLASSIC) ) { 123 ddesReport(m); 124 } else { 125 int least_significant_bit = m.getElement(3) & 1; 126 if ( least_significant_bit == 0 ) { 127 classicRFIDReport(m); 128 } else { 129 canRcomReport(m); 130 } 131 } 132 } 133 } 134 135 private void ddesReport(CanReply m) { 136 int least_significant_bit = m.getElement(3) & 1; 137 if ( least_significant_bit == 0 ) { 138 canRc522Report(m); 139 } else { 140 canRcomReport(m); 141 } 142 } 143 144 private void classicRFIDReport(CanReply m) { 145 String buf = toClassicTag(m.getElement(3), m.getElement(4), m.getElement(5), m.getElement(6), m.getElement(7)); 146 log.debug("Reporter {} {} RFID tag read of tag: {}", this,getCbusReporterType(),buf); 147 IdTag tag = InstanceManager.getDefault(IdTagManager.class).provideIdTag(buf); 148 notify(tag); 149 startTimeout(tag); 150 } 151 152 // no DCC address correction to allow full 0-65535 range of tags on rolling stock 153 private void canRc522Report(CanReply m){ 154 String tagId = String.valueOf((m.getElement(4)<<8)+ m.getElement(5)); 155 log.debug("Reporter {} RFID tag read of tag: {}",this, tagId); 156 IdTag tag = InstanceManager.getDefault(IdTagManager.class).provideIdTag("ID"+tagId); 157 tag.setProperty("DDES Dat3", m.getElement(6)); 158 tag.setProperty("DDES Dat4", m.getElement(7)); 159 notify(tag); 160 startTimeout(tag); 161 } 162 163 164 165 private void canRcomReport(CanReply m) { 166 167 var locoAddress = parseAddress(m); 168 169 int speed = m.getElement(6)&0x7F; 170 if ((m.getElement(6)&0x80) == 0) { 171 speed = -1; // data unavailable 172 } 173 174 int flags = m.getElement(7); 175 176 RailCom.Orientation orientation; 177 switch (flags&0x03) { 178 case 2: 179 orientation = RailCom.Orientation.EAST; 180 break; 181 case 1: 182 orientation = RailCom.Orientation.WEST; 183 break; 184 case 0: 185 orientation = RailCom.Orientation.UNKNOWN; 186 break; 187 default: 188 log.warn("Unexpected orientation code 3"); 189 orientation = RailCom.Orientation.UNKNOWN; 190 break; 191 } 192 193 RailCom.Direction direction; 194 switch ((flags>>2)&0x03) { 195 case 1: 196 direction = RailCom.Direction.FORWARD; 197 break; 198 case 2: 199 direction = RailCom.Direction.BACKWARD; 200 break; 201 case 0: 202 direction = RailCom.Direction.UNKNOWN; 203 break; 204 default: 205 log.warn("Unexpected direction code 3"); 206 direction = RailCom.Direction.UNKNOWN; 207 break; 208 } 209 210 RailCom.Motion motion; 211 switch ((flags>>4)&0x03) { 212 case 1: 213 motion = RailCom.Motion.STATIONARY; 214 log.debug("Setting speed to zero because known to be not moving"); 215 speed = 0; 216 break; 217 case 2: 218 motion = RailCom.Motion.MOVING; 219 break; 220 case 0: 221 motion = RailCom.Motion.UNKNOWN; 222 break; 223 default: 224 log.warn("Unexpected motion code 3"); 225 motion = RailCom.Motion.UNKNOWN; 226 break; 227 } 228 229 RailCom.QoS qos; 230 switch ((flags>>6)&0x03) { 231 case 1: 232 qos = RailCom.QoS.POOR; 233 break; 234 case 2: 235 qos = RailCom.QoS.GOOD; 236 break; 237 case 0: 238 qos = RailCom.QoS.UNKNOWN; 239 break; 240 default: 241 log.warn("Unexpected QoS code 3"); 242 qos = RailCom.QoS.UNKNOWN; 243 break; 244 } 245 246 var idTag = railComManager.provideIdTag(""+locoAddress.getNumber()); 247 var tag = (RailCom)idTag; 248 249 tag.setDccAddress(locoAddress); 250 tag.setActualSpeed(speed); 251 tag.setOrientation(orientation); 252 tag.setDirection(direction); 253 tag.setMotion(motion); 254 tag.setQoS(qos); 255 256 notify(tag); 257 startTimeout(tag); 258 } 259 260 DccLocoAddress parseAddress(CanReply m) { // package access for testing 261 int dccTypeInt = m.getElement(4)&0xC0; 262 int dccNumber; 263 int b4 = m.getElement(4) & 0x3F; // excludes high "type" bits 264 int b5 = m.getElement(5); 265 LocoAddress.Protocol dccType; 266 boolean isConsist; 267 switch (dccTypeInt) { 268 default: 269 case 0xC0: 270 dccNumber = (b4<<8) | b5; 271 dccType = LocoAddress.Protocol.DCC_LONG; 272 isConsist = false; 273 break; 274 case 0x00: 275 dccNumber = b5; 276 dccType = LocoAddress.Protocol.DCC_SHORT; 277 isConsist = false; 278 break; 279 case 0x40: 280 dccNumber = b5&0x7F; // remove direction bit 281 dccType = LocoAddress.Protocol.DCC_SHORT; 282 isConsist = true; 283 break; 284 case 0x80: 285 int decUpper = (b4 << 1) | ((b5>>7)&0x01); // BCD upper value 286 dccNumber = decUpper*100 + (b5&0x7F); 287 dccType = LocoAddress.Protocol.DCC_LONG; 288 isConsist = true; 289 break; 290 } 291 return new DccLocoAddress(dccNumber, dccType, isConsist); 292 } 293 294 private String toClassicTag(int b1, int b2, int b3, int b4, int b5) { 295 return String.format("%02X", b1) + String.format("%02X", b2) + String.format("%02X", b3) 296 + String.format("%02X", b4) + String.format("%02X", b5); 297 } 298 299 /** 300 * Get the Reporter Listener format type. 301 * <p> 302 * Defaults to Classic RfID, 5 byte unique. 303 * @return reporter format type. 304 */ 305 @Nonnull 306 public String getCbusReporterType() { 307 Object returnVal = getProperty(CbusReporterManager.CBUS_REPORTER_DESCRIPTOR_KEY); 308 return (returnVal==null ? CbusReporterManager.CBUS_DEFAULT_REPORTER_TYPE : returnVal.toString()); 309 } 310 311 /** 312 * Get if the Reporter should provide / update a CBUS Sensor, following Reporter Status. 313 * <p> 314 * Defaults to false. 315 * @return true if the reporter should maintain the Sensor. 316 */ 317 public boolean getMaintainSensor() { 318 Boolean returnVal = (Boolean) getProperty(CbusReporterManager.CBUS_MAINTAIN_SENSOR_DESCRIPTOR_KEY); 319 return (returnVal==null ? false : returnVal); 320 } 321 322 // delay can be set to non-null memo when older constructor fully deprecated. 323 private void startTimeout(IdTag tag){ 324 // only timeout when enabled 325 if (! eraseOnTimeoutAll && ! eraseOnTimeoutThisReporter) return; 326 327 int delay = (_memo==null ? 2000 : ((CbusReporterManager)_memo.get(jmri.ReporterManager.class)).getTimeout() ); 328 ThreadingUtil.runOnLayoutDelayed( () -> { 329 if (!disposed && getCurrentReport() == tag) { 330 notify(null); 331 } 332 },delay); 333 } 334 335 private boolean disposed = false; 336 337 /** 338 * {@inheritDoc} 339 */ 340 @Override 341 public void dispose() { 342 disposed = true; 343 super.dispose(); 344 } 345 346 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CbusReporter.class); 347}