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