001package jmri.jmrix.loconet; 002 003import java.util.HashSet; 004import java.util.regex.Matcher; 005import java.util.regex.Pattern; 006import jmri.DccLocoAddress; 007import jmri.InstanceManager; 008import jmri.IdTag; 009import jmri.LocoAddress; 010import jmri.CollectingReporter; 011import jmri.PhysicalLocationReporter; 012import jmri.implementation.AbstractIdTagReporter; 013import jmri.util.PhysicalLocation; 014import org.slf4j.Logger; 015import org.slf4j.LoggerFactory; 016 017/** 018 * Extend jmri.AbstractIdTagReporter for LocoNet layouts. 019 * <p> 020 * This implementation reports Transponding messages from LocoNet-based "Reporters". 021 * <p> 022 * For LocoNet connections, a "Reporter" represents either a Digitrax "transponding zone", a 023 * Lissy "measurement zone" or a Lissy RFID reader location. 024 * <br> 025 * The messages from these Reporters are handled by this code. 026 * <br> 027 * The LnReporterManager is responsible for decode of appropriate LocoNet messages 028 * and passing only those messages to the Reporter which match its Reporter address. 029 * <br> 030 * Each transponding message creates a new current report. The last report is 031 * always available, and is the same as the contents of the last transponding 032 * message received. Based on the report, for new tags a new Id Tag is created by the LnReporter. 033 * <p> 034 * Reports are Strings, formatted as 035 * <ul> 036 * <li>NNNN enter - locomotive address NNNN entered the transponding zone. Short 037 * vs long address is indicated by the NNNN value 038 * <li>NNNN exits - locomotive address NNNN left the transponding zone. 039 * <li>NNNN seen northbound - LISSY measurement 040 * <li>NNNN seen southbound - LISSY measurement 041 * </ul> 042 * 043 * Some of the message formats used in this class are Copyright Digitrax, Inc. 044 * and used with permission as part of the JMRI project. That permission does 045 * not extend to uses in other software products. If you wish to use this code, 046 * algorithm or these message formats outside of JMRI, please contact Digitrax 047 * Inc for separate permission. 048 * 049 * @author Bob Jacobsen Copyright (C) 2001, 2007 050 */ 051public class LnReporter extends AbstractIdTagReporter implements CollectingReporter { 052 053 public LnReporter(int number, LnTrafficController tc, String prefix) { // a human-readable Reporter number must be specified! 054 super(prefix + "R" + number); // can't use prefix here, as still in construction 055 log.debug("new Reporter {}", number); 056 _number = number; 057 // At construction, register for messages 058 entrySet = new HashSet<>(); 059 } 060 061 062 /** 063 * @return the LocoNet address number for this reporter. 064 */ 065 public int getNumber() { 066 return _number; 067 } 068 069 /** 070 * Process LocoNet message handed to us from the LnReporterManager 071 * @param l - a LocoNetMessage. 072 */ 073 public void messageFromManager(LocoNetMessage l) { 074 // check message type 075 if (isTranspondingLocationReport(l) || isTranspondingFindReport(l)) { 076 transpondingReport(l); 077 } 078 if (l.getOpCode() == LnConstants.OPC_LISSY_UPDATE) { 079 if (l.getElement(1) == 0x08) { 080 lissyReport(l); 081 } else if (l.getElement(2) == 0x41) { 082 lissyRfidReport(l); 083 } 084 } // else nothing 085 086 } 087 088 /** 089 * Check if message is a Transponding Location Report message 090 * 091 * A Transponding Location Report message is sent by transponding hardware 092 * when a transponding mobile decoder enters or leaves a transponding zone. 093 * 094 * @param l LocoNet message to check 095 * @return true if message is a Transponding Location Report, else false. 096 */ 097 public final boolean isTranspondingLocationReport(LocoNetMessage l) { 098 return ((l.getOpCode() == LnConstants.OPC_MULTI_SENSE) 099 && ((l.getElement(1) & 0xC0) == 0)) ; 100 } 101 102 /** 103 * Check if message is a Transponding Find Report message 104 * 105 * A Transponding Location Report message is sent by transponding hardware 106 * in response to a Transponding Find Request message when the addressed 107 * decoder is within a transponding zone and the decoder is transponding-enabled. 108 * 109 * @param l LocoNet message to check 110 * @return true if message is a Transponding Find Report, else false. 111 */ 112 public final boolean isTranspondingFindReport(LocoNetMessage l) { 113 return (l.getOpCode() == LnConstants.OPC_PEER_XFER 114 && l.getElement(1) == 0x09 115 && l.getElement(2) == 0 ); 116 } 117 118 /** 119 * Handle transponding message passed to us by the LnReporting Manager 120 * 121 * Assumes that the LocoNet message is a valid transponding message. 122 * 123 * @param l - incoming loconetmessage 124 */ 125 void transpondingReport(LocoNetMessage l) { 126 boolean enter; 127 int loco; 128 IdTag idTag; 129 if (l.getOpCode() == LnConstants.OPC_MULTI_SENSE) { 130 enter = ((l.getElement(1) & 0x20) != 0); // get reported direction 131 } else { 132 enter = true; // a response for a find request. Always handled as entry. 133 } 134 loco = getLocoAddrFromTranspondingMsg(l); // get loco address 135 136 log.debug("Transponding Report at {} for {}",_number, loco); 137 notify(null); // set report to null to make sure listeners update 138 139 idTag = InstanceManager.getDefault(TranspondingTagManager.class).provideIdTag("" + loco); 140 idTag.setProperty("entryexit", "enter"); 141 if (enter) { 142 idTag.setProperty("entryexit", "enter"); 143 if (!entrySet.contains(idTag)) { 144 entrySet.add(idTag); 145 } 146 } else { 147 idTag.setProperty("entryexit", "exits"); 148 if (entrySet.contains(idTag)) { 149 entrySet.remove(idTag); 150 } 151 } 152 log.debug("Tag: {} entry {}", idTag, enter); 153 notify(idTag); 154 setState(enter ? loco : -1); 155 } 156 157 /** 158 * extract long or short address from transponding message 159 * 160 * Assumes that the LocoNet message is a valid transponding message. 161 * 162 * @param l LocoNet message 163 * @return loco address 164 */ 165 public int getLocoAddrFromTranspondingMsg(LocoNetMessage l) { 166 if (l.getElement(3) == 0x7D) { 167 return l.getElement(4); 168 } 169 return l.getElement(3) * 128 + l.getElement(4); 170 171 } 172 173 /** 174 * Handle LISSY message 175 * @param l Message from which to extract LISSY content 176 */ 177 void lissyReport(LocoNetMessage l) { 178 179 // Only report messages where bit 6 is set in element 3, 180 // because these are the only messages with valid loco addresses 181 if ((l.getElement(3) & 0x40) != 0) { 182 int loco = (l.getElement(6) & 0x7F) + 128 * (l.getElement(5) & 0x7F); 183 184 // train category - Perhaps add to idTag as property? 185 int category = l.getElement(2) + 1; 186 187 // get direction 188 // north assumes loco is passing sensors S1->S2 189 boolean north = ((l.getElement(3) & 0x20) == 0); 190 191 notify(null); // set report to null to make sure listeners update 192 // get loco address 193 IdTag idTag = InstanceManager.getDefault(TranspondingTagManager.class).provideIdTag(""+loco+":"+category); 194 if(north) { 195 idTag.setProperty("seen", "seen northbound"); 196 } else { 197 idTag.setProperty("seen", "seen southbound"); 198 } 199 log.debug("Tag: {}", idTag); 200 notify(idTag); 201 setState(loco); 202 } 203 } 204 205 /** 206 * Handle LISSY RFID-7 and RFID-5 messages 207 * @param l Message from which to extract RFID content (UID) 208 */ 209 void lissyRfidReport(LocoNetMessage l) { 210 String tag; 211 StringBuilder tg = new StringBuilder(); 212 int max = l.getElement(1) - 2; // GCA51 RFID-7 elem(3) = size = 0x0E; RFID-5 elem(3) = size = 0x0C 213 int rfidHi = l.getElement(max); // MSbits are transmitted via element(max) 214 for (int j = 5; j < max; j++) { 215 int shift = j-5; 216 int hi = 0x0; 217 if(((rfidHi >> shift) & 0x1) == 1) hi = 0x80; 218 tg.append(String.format("%1$02X", l.getElement(j) + hi)); 219 } 220 tag = tg.toString(); 221 222 int rfidSensorAddress = l.getElement(3) << 7 | l.getElement(4); 223 224 notify(null); // set report to null to make sure listeners update 225 // get rfid tag 226 IdTag idTag = InstanceManager.getDefault(TranspondingTagManager.class).provideIdTag(""+tag); 227 // add info from reader 228 idTag.setProperty("rfid", rfidSensorAddress); 229 log.debug("Tag: {}", idTag); 230 notify(idTag); // sets report of this reporter to tag 231 } 232 233 /** 234 * Provide an int value for use in scripts, etc. This will be the numeric 235 * locomotive address last seen, unless the last message said the loco was 236 * exiting. Note that there may still some other locomotive in the 237 * transponding zone! 238 * 239 * @return -1 if the last message specified exiting 240 */ 241 @Override 242 public int getState() { 243 return lastLoco; 244 } 245 246 /** 247 * {@inheritDoc} 248 */ 249 @Override 250 public void setState(int s) { 251 lastLoco = s; 252 } 253 int lastLoco = -1; 254 255 /** 256 * Parses out a (possibly old) LnReporter-generated report string to extract info used by 257 * the public PhysicalLocationReporter methods. Returns a Matcher that, if successful, should 258 * have the following groups defined. 259 * matcher.group(1) : the locomotive address 260 * matcher.group(2) : (enter | exit | seen) 261 * matcher.group(3) | (northbound | southbound) -- Lissy messages only 262 * <p> 263 * NOTE: This code is dependent on the transpondingReport() and lissyReport() methods. 264 * If they change, the regex here must change. 265 */ 266 private Matcher parseReport(String rep) { 267 if (rep == null) { 268 return (null); 269 } 270 Pattern ln_p = Pattern.compile("(\\d+) (enter|exits|seen)\\s*(northbound|southbound)?"); // Match a number followed by the word "enter". This is the LocoNet pattern. // NOI18N 271 Matcher m = ln_p.matcher(rep); 272 return (m); 273 } 274 275 /** 276 * {@inheritDoc} 277 */ 278 // Parses out a (possibly old) LnReporter-generated report string to extract the address from the front. 279 // Assumes the LocoReporter format is "NNNN [enter|exit]" 280 @Override 281 public LocoAddress getLocoAddress(String rep) { 282 // Extract the number from the head of the report string 283 log.debug("report string: {}", rep); 284 Matcher m = this.parseReport(rep); 285 if ((m != null) && m.find()) { 286 log.debug("Parsed address: {}", m.group(1)); 287 return (new DccLocoAddress(Integer.parseInt(m.group(1)), LocoAddress.Protocol.DCC)); 288 } else { 289 return (null); 290 } 291 } 292 293 /** 294 * {@inheritDoc} 295 */ 296 // Parses out a (possibly old) LnReporter-generated report string to extract the direction from the end. 297 // Assumes the LocoReporter format is "NNNN [enter|exit]" 298 @Override 299 public PhysicalLocationReporter.Direction getDirection(String rep) { 300 // Extract the direction from the tail of the report string 301 log.debug("report string: {}", rep); // NOI18N 302 Matcher m = this.parseReport(rep); 303 if (m.find()) { 304 log.debug("Parsed direction: {}", m.group(2)); // NOI18N 305 switch (m.group(2)) { 306 case "enter": // LocoNet Enter message // NOI18N 307 case "seen": // Lissy message. Treat both as "entry" messages. // NOI18N 308 return (PhysicalLocationReporter.Direction.ENTER); 309 default: 310 return (PhysicalLocationReporter.Direction.EXIT); 311 } 312 } else { 313 return (PhysicalLocationReporter.Direction.UNKNOWN); 314 } 315 } 316 317 /** 318 * {@inheritDoc} 319 */ 320 @Override 321 public PhysicalLocation getPhysicalLocation() { 322 return (PhysicalLocation.getBeanPhysicalLocation(this)); 323 } 324 325 /** 326 * {@inheritDoc} 327 */ 328 // Does not use the parameter S. 329 @Override 330 public PhysicalLocation getPhysicalLocation(String s) { 331 return (PhysicalLocation.getBeanPhysicalLocation(this)); 332 } 333 334 335 // Collecting Reporter Interface methods 336 /** 337 * {@inheritDoc} 338 */ 339 @Override 340 public java.util.Collection<Object> getCollection(){ 341 return entrySet; 342 } 343 344 // data members 345 private final int _number; // LocoNet Reporter number 346 private final HashSet<Object> entrySet; 347 348 private final static Logger log = LoggerFactory.getLogger(LnReporter.class); 349 350}