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 static private 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 classicRFIDReport(m); 126 } 127 } 128 } 129 130 private void ddesReport(CanReply m) { 131 int least_significant_bit = m.getElement(3) & 1; 132 if ( least_significant_bit == 0 ) { 133 canRc522Report(m); 134 } else { 135 canRcomReport(m); 136 } 137 } 138 139 private void classicRFIDReport(CanReply m) { 140 String buf = toClassicTag(m.getElement(3), m.getElement(4), m.getElement(5), m.getElement(6), m.getElement(7)); 141 log.debug("Reporter {} {} RFID tag read of tag: {}", this,getCbusReporterType(),buf); 142 IdTag tag = InstanceManager.getDefault(IdTagManager.class).provideIdTag(buf); 143 notify(tag); 144 startTimeout(tag); 145 } 146 147 // no DCC address correction to allow full 0-65535 range of tags on rolling stock 148 private void canRc522Report(CanReply m){ 149 String tagId = String.valueOf((m.getElement(4)<<8)+ m.getElement(5)); 150 log.debug("Reporter {} RFID tag read of tag: {}",this, tagId); 151 IdTag tag = InstanceManager.getDefault(IdTagManager.class).provideIdTag("ID"+tagId); 152 tag.setProperty("DDES Dat3", m.getElement(6)); 153 tag.setProperty("DDES Dat4", m.getElement(7)); 154 notify(tag); 155 startTimeout(tag); 156 } 157 158 159 160 private void canRcomReport(CanReply m) { 161 162 var locoAddress = parseAddress(m); 163 164 int speed = m.getElement(6)&0x7F; 165 if ((m.getElement(6)&0x80) == 0) { 166 speed = -1; // data unavailable 167 } 168 169 int flags = m.getElement(7); 170 171 RailCom.Orientation orientation; 172 switch (flags&0x03) { 173 case 2: 174 orientation = RailCom.Orientation.EAST; 175 break; 176 case 1: 177 orientation = RailCom.Orientation.WEST; 178 break; 179 case 0: 180 orientation = RailCom.Orientation.UNKNOWN; 181 break; 182 default: 183 log.warn("Unexpected orientation code 3"); 184 orientation = RailCom.Orientation.UNKNOWN; 185 break; 186 } 187 188 RailCom.Direction direction; 189 switch ((flags>>2)&0x03) { 190 case 1: 191 direction = RailCom.Direction.FORWARD; 192 break; 193 case 2: 194 direction = RailCom.Direction.BACKWARD; 195 break; 196 case 0: 197 direction = RailCom.Direction.UNKNOWN; 198 break; 199 default: 200 log.warn("Unexpected direction code 3"); 201 direction = RailCom.Direction.UNKNOWN; 202 break; 203 } 204 205 RailCom.Motion motion; 206 switch ((flags>>4)&0x03) { 207 case 1: 208 motion = RailCom.Motion.STATIONARY; 209 log.debug("Setting speed to zero because known to be not moving"); 210 speed = 0; 211 break; 212 case 2: 213 motion = RailCom.Motion.MOVING; 214 break; 215 case 0: 216 motion = RailCom.Motion.UNKNOWN; 217 break; 218 default: 219 log.warn("Unexpected motion code 3"); 220 motion = RailCom.Motion.UNKNOWN; 221 break; 222 } 223 224 RailCom.QoS qos; 225 switch ((flags>>6)&0x03) { 226 case 1: 227 qos = RailCom.QoS.POOR; 228 break; 229 case 2: 230 qos = RailCom.QoS.GOOD; 231 break; 232 case 0: 233 qos = RailCom.QoS.UNKNOWN; 234 break; 235 default: 236 log.warn("Unexpected QoS code 3"); 237 qos = RailCom.QoS.UNKNOWN; 238 break; 239 } 240 241 var idTag = railComManager.provideIdTag(""+locoAddress.getNumber()); 242 var tag = (RailCom)idTag; 243 244 tag.setDccAddress(locoAddress); 245 tag.setActualSpeed(speed); 246 tag.setOrientation(orientation); 247 tag.setDirection(direction); 248 tag.setMotion(motion); 249 tag.setQoS(qos); 250 251 notify(tag); 252 startTimeout(tag); 253 } 254 255 DccLocoAddress parseAddress(CanReply m) { // package access for testing 256 int dccTypeInt = m.getElement(4)&0xC0; 257 int dccNumber; 258 int b4 = m.getElement(4) & 0x3F; // excludes high "type" bits 259 int b5 = m.getElement(5); 260 LocoAddress.Protocol dccType; 261 switch (dccTypeInt) { 262 default: 263 case 0xC0: 264 dccNumber = (b4<<8) | b5; 265 dccType = LocoAddress.Protocol.DCC_LONG; 266 break; 267 case 0x00: 268 dccNumber = b5; 269 dccType = LocoAddress.Protocol.DCC_SHORT; 270 break; 271 case 0x40: 272 dccNumber = b5&0x7F; // remove direction bit 273 dccType = LocoAddress.Protocol.DCC_CONSIST; 274 break; 275 case 0x80: 276 int decUpper = (b4 << 1) | ((b5>>7)&0x01); // BCD upper value 277 dccNumber = decUpper*100 + (b5&0x7F); 278 dccType = LocoAddress.Protocol.DCC_EXTENDED_CONSIST; 279 280 } 281 return new DccLocoAddress(dccNumber, dccType); 282 } 283 284 private String toClassicTag(int b1, int b2, int b3, int b4, int b5) { 285 return String.format("%02X", b1) + String.format("%02X", b2) + String.format("%02X", b3) 286 + String.format("%02X", b4) + String.format("%02X", b5); 287 } 288 289 /** 290 * Get the Reporter Listener format type. 291 * <p> 292 * Defaults to Classic RfID, 5 byte unique. 293 * @return reporter format type. 294 */ 295 @Nonnull 296 public String getCbusReporterType() { 297 Object returnVal = getProperty(CbusReporterManager.CBUS_REPORTER_DESCRIPTOR_KEY); 298 return (returnVal==null ? CbusReporterManager.CBUS_DEFAULT_REPORTER_TYPE : returnVal.toString()); 299 } 300 301 /** 302 * Get if the Reporter should provide / update a CBUS Sensor, following Reporter Status. 303 * <p> 304 * Defaults to false. 305 * @return true if the reporter should maintain the Sensor. 306 */ 307 public boolean getMaintainSensor() { 308 Boolean returnVal = (Boolean) getProperty(CbusReporterManager.CBUS_MAINTAIN_SENSOR_DESCRIPTOR_KEY); 309 return (returnVal==null ? false : returnVal); 310 } 311 312 // delay can be set to non-null memo when older constructor fully deprecated. 313 private void startTimeout(IdTag tag){ 314 // only timeout when enabled 315 if (! eraseOnTimeoutAll && ! eraseOnTimeoutThisReporter) return; 316 317 int delay = (_memo==null ? 2000 : ((CbusReporterManager)_memo.get(jmri.ReporterManager.class)).getTimeout() ); 318 ThreadingUtil.runOnLayoutDelayed( () -> { 319 if (!disposed && getCurrentReport() == tag) { 320 notify(null); 321 } 322 },delay); 323 } 324 325 private boolean disposed = false; 326 327 /** 328 * {@inheritDoc} 329 */ 330 @Override 331 public void dispose() { 332 disposed = true; 333 super.dispose(); 334 } 335 336 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CbusReporter.class); 337}