001package jmri.jmrix.openlcb; 002 003import java.util.regex.Matcher; 004import java.util.regex.Pattern; 005 006import javax.annotation.CheckReturnValue; 007 008import jmri.NamedBean.BadSystemNameException; 009 010import jmri.jmrix.can.CanMessage; 011import jmri.jmrix.can.CanReply; 012import jmri.jmrix.can.CanSystemConnectionMemo; 013 014import org.openlcb.EventID; 015 016import javax.annotation.Nonnull; 017 018/** 019 * Utilities for handling OpenLCB event messages as addresses. 020 * <p> 021 * OpenLCB event messages have header information, plus an EventID in the data 022 * part. JMRI maps these into address strings. 023 * <p> 024 * String forms: 025 * <dl> 026 * <dt>Special case for DCC Turnout addressing: Tnnn where nnn is a decimal number 027 * 028 * <dt>Full hex string preceeded by "x"<dd>Needs to be pairs of digits: 0123, 029 * not 123 030 * 031 * <dt>Full 8 byte ID as pairs separated by "." 032 * </dl> 033 * <p> 034 * Note: the {@link #check()} routine does a full, expensive 035 * validity check of the name. All other operations 036 * assume correctness, diagnose some invalid-format strings, but 037 * may appear to successfully handle other invalid forms. 038 * 039 * @author Bob Jacobsen Copyright (C) 2008, 2010, 2018, 2024 040 */ 041public final class OlcbAddress { 042 043 static final String singleAddressPattern = "([xX](\\p{XDigit}\\p{XDigit}){1,8})|((\\p{XDigit}?\\p{XDigit}.){7}\\p{XDigit}?\\p{XDigit})"; 044 045 private Matcher hCode = null; 046 047 private Matcher getMatcher() { 048 if (hCode == null) hCode = Pattern.compile("^" + singleAddressPattern + "$").matcher(""); 049 return hCode; 050 } 051 052 private String aString; // String value of the address 053 private int[] aFrame = null; // int[8] of event ID; if null, aString might be two addresses 054 private boolean match = false; // true if address properly parsed; false (may) mean two-part address 055 private boolean fromName = false; // true if this originate as an event name 056 /** 057 * Construct from OlcbEvent. 058 * 059 * @param e the event ID. 060 */ 061 public OlcbAddress(EventID e) { 062 byte[] contents = e.getContents(); 063 aFrame = new int[contents.length]; 064 int i = 0; 065 for (byte b : contents) { 066 aFrame[i++] = b; 067 } 068 aString = toCanonicalString(); 069 } 070 071 /** 072 * Construct from string without leading system or type letters 073 * @param input hex coded string of address 074 */ 075 public OlcbAddress(String input, final CanSystemConnectionMemo memo) { 076 // This is done manually, rather than via regular expressions, for performance reasons. 077 078 String s = input.strip(); 079 080 OlcbEventNameStore nameStore = null; 081 if (memo != null) { 082 nameStore = memo.get(OlcbEventNameStore.class); 083 } 084 EventID eid; 085 if (nameStore != null && (eid = nameStore.getEventID(s)) != null) { 086 // name form 087 // load the event ID into the aFrame c.f. OlcbAddress(EventID) ctor 088 byte[] contents = eid.getContents(); 089 aFrame = new int[contents.length]; 090 int i = 0; 091 for (byte b : contents) { 092 aFrame[i++] = b; 093 } 094 match = true; 095 fromName = true; 096 // leave aString as original argument 097 aString = s; 098 return; 099 } 100 101 // check for special addressing forms 102 if (s.startsWith("T")) { 103 // leading T, so convert to numeric form from turnout number 104 int from; 105 try { 106 from = Integer.parseInt(s.substring(1)); 107 } catch (NumberFormatException e) { 108 from = 0; 109 } 110 111 int DD = (from-1) & 0x3; 112 int aaaaaa = (( (from-1) >> 2)+1 ) & 0x3F; 113 int AAA = ( (from) >> 8) & 0x7; 114 long event = 0x0101020000FF0000L | (AAA << 9) | (aaaaaa << 3) | (DD << 1); 115 116 s = String.format("%016X;%016X", event, event+1); 117 log.trace(" Turnout form converted to {}", s); 118 } else if (s.startsWith("S")) { 119 // leading S, so convert to numeric form from sensor number 120 int from; 121 try { 122 from = Integer.parseInt(s.substring(1)); 123 } catch (NumberFormatException e) { 124 from = 0; 125 } 126 127 from = 0xFFF & (from - 1); // 1 based name to 0 based network, 12 bit value 128 129 long event1 = 0x0101020000FB0000L | from; // active/on 130 long event2 = 0x0101020000FA0000L | from; // inactive/off 131 132 s = String.format("%016X;%016X", event1, event2); 133 log.trace(" Sensor form converted to {}", s); 134 } 135 136 aString = s; 137 138 // numeric address string format 139 if (aString.contains(";")) { 140 // multi-part address; leave match false and aFrame null; only aString has content 141 // will later be split up and parsed with #split() call 142 return; 143 } 144 145 // check for name vs numeric address formats 146 147 if (aString.contains(".")) { 148 // dotted form, 7 dots 149 String[] terms = s.split("\\."); 150 if (terms.length != 8) { 151 log.debug("unexpected number of terms: {}, address is {}", terms.length, s); 152 } 153 int[] tFrame = new int[terms.length]; 154 int i = -1; 155 try { 156 for (i = 0; i < terms.length; i++) { 157 tFrame[i] = Integer.parseInt(terms[i].strip(), 16); 158 } 159 } catch (NumberFormatException ex) { 160 // leaving the string unparsed 161 log.debug("failed to parse EventID \"{}\" at {} due to {}; might be a partial value", s, i, terms[i].strip()); 162 return; 163 } 164 aFrame = tFrame; 165 match = true; 166 } else { 167 // assume single hex string - drop leading x if present 168 if (aString.startsWith("x")) aString = aString.substring(1); 169 if (aString.startsWith("X")) aString = aString.substring(1); 170 int len = aString.length() / 2; 171 int[] tFrame = new int[len]; 172 // get the frame data 173 try { 174 for (int i = 0; i < len; i++) { 175 String two = aString.substring(2 * i, 2 * i + 2); 176 tFrame[i] = Integer.parseInt(two, 16); 177 } 178 } catch (NumberFormatException ex) { 179 log.debug("failed to parse EventID \"{}\"; might be a partial value", s); 180 return; 181 } // leaving the string unparsed 182 aFrame = tFrame; 183 match = true; 184 } 185 } 186 187 /** 188 * Two addresses are equal if they result in the same numeric contents 189 */ 190 @Override 191 public boolean equals(Object r) { 192 if (r == null) { 193 return false; 194 } 195 if (!(r.getClass().equals(this.getClass()))) { // final class simplifies this 196 return false; 197 } 198 OlcbAddress opp = (OlcbAddress) r; 199 if (this.aFrame == null || opp.aFrame == null) { 200 // one or the other has just a string, e.g A;B form. 201 // compare strings 202 return this.aString.equals(opp.aString); 203 } 204 if (opp.aFrame.length != this.aFrame.length) { 205 return false; 206 } 207 for (int i = 0; i < this.aFrame.length; i++) { 208 if (this.aFrame[i] != opp.aFrame[i]) { 209 return false; 210 } 211 } 212 return true; 213 } 214 215 @Override 216 public int hashCode() { 217 int ret = 0; 218 for (int value : this.aFrame) { 219 ret += value*8; // don't want to overflow int, do want to spread out 220 } 221 return ret; 222 } 223 224 public int compare(@Nonnull OlcbAddress opp) { 225 // if neither matched, just do a lexical sort 226 if (!match && !opp.match) return aString.compareTo(opp.aString); 227 228 // match (single address) sorts before non-matched (double address) 229 if (match && !opp.match) return -1; 230 if (!match && opp.match) return +1; 231 232 // both matched, usual case: comparing on content 233 for (int i = 0; i < Math.min(aFrame.length, opp.aFrame.length); i++) { 234 if (aFrame[i] != opp.aFrame[i]) return Integer.signum(aFrame[i] - opp.aFrame[i]); 235 } 236 // check for different length (shorter sorts first) 237 return Integer.signum(aFrame.length - opp.aFrame.length); 238 } 239 240 public CanMessage makeMessage() { 241 CanMessage c = new CanMessage(aFrame, 0x195B4000); 242 c.setExtended(true); 243 return c; 244 } 245 246 /** 247 * Confirm that the address string (provided earlier) is fully 248 * valid. 249 * <p> 250 * This is an expensive call. It's complete-compliance done 251 * using a regular expression. It can reject some 252 * forms that the code will normally handle OK. 253 * @return true if valid, else false. 254 */ 255 public boolean check() { 256 return getMatcher().reset(aString).matches(); 257 } 258 259 boolean match(CanReply r) { 260 // check address first 261 if (r.getNumDataElements() != aFrame.length) { 262 return false; 263 } 264 for (int i = 0; i < aFrame.length; i++) { 265 if (aFrame[i] != r.getElement(i)) { 266 return false; 267 } 268 } 269 // check for event message type 270 if (!r.isExtended()) { 271 return false; 272 } 273 return (r.getHeader() & 0x1FFFF000) == 0x195B4000; 274 } 275 276 boolean match(CanMessage r) { 277 // check address first 278 if (r.getNumDataElements() != aFrame.length) { 279 return false; 280 } 281 for (int i = 0; i < aFrame.length; i++) { 282 if (aFrame[i] != r.getElement(i)) { 283 return false; 284 } 285 } 286 // check for event message type 287 if (!r.isExtended()) { 288 return false; 289 } 290 return (r.getHeader() & 0x1FFFF000) == 0x195B4000; 291 } 292 293 /** 294 * Split a string containing one or more addresses into individual ones. 295 * 296 * @return null if entire string can't be parsed. 297 */ 298 @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "PZLA_PREFER_ZERO_LENGTH_ARRAYS", 299 justification = "Documented API, no resources to improve") 300 public OlcbAddress[] split(final CanSystemConnectionMemo memo) { 301 // reject strings ending in ";" 302 if (aString == null || aString.endsWith(";")) { 303 return null; 304 } 305 306 // split string at ";" points 307 String[] pStrings = aString.split(";"); 308 309 OlcbAddress[] retval = new OlcbAddress[pStrings.length]; 310 311 for (int i = 0; i < pStrings.length; i++) { 312 // check validity of each 313 if (pStrings[i].equals("")) { 314 return null; 315 } 316 317 // too expensive to do full regex check here, as this is used a lot in e.g. sorts 318 // if (!getMatcher().reset(pStrings[i]).matches()) return null; 319 320 retval[i] = new OlcbAddress(pStrings[i], memo); 321 if (!retval[i].match) { 322 return null; 323 } 324 } 325 return retval; 326 } 327 328 public boolean checkSplit( final CanSystemConnectionMemo memo) { 329 return (split(memo) != null); 330 } 331 332 int[] elements() { 333 return aFrame; 334 } 335 336 @Override 337 /** 338 * @return The string that was used to create this address 339 */ 340 public String toString() { 341 return aString; 342 } 343 344 /** 345 * @return The canonical form of 0x1122334455667788 346 */ 347 public String toCanonicalString() { 348 String retval = "x"; 349 for (int value : aFrame) { 350 retval = jmri.util.StringUtil.appendTwoHexFromInt(value, retval); 351 } 352 return retval; 353 } 354 355 /** 356 * Provide as dotted pairs. 357 * @return dotted pair form off string. 358 */ 359 public String toDottedString() { 360 String retval = ""; 361 if (aFrame == null) return retval; 362 for (int value : aFrame) { 363 if (!retval.isEmpty()) 364 retval += "."; 365 retval = jmri.util.StringUtil.appendTwoHexFromInt(value, retval); 366 } 367 return retval; 368 } 369 370 /** 371 * @return null if no valid address was parsed earlier, e.g. there was a ; in the data 372 */ 373 public EventID toEventID() { 374 if (aFrame == null) return null; 375 byte[] b = new byte[8]; 376 for (int i = 0; i < Math.min(8, aFrame.length); ++i) b[i] = (byte)aFrame[i]; 377 return new EventID(b); 378 } 379 380 /** 381 * Was this parsed from a name (e.g. not explicit ID, not pair) 382 * @return true if constructed from an event name 383 */ 384 public boolean isFromName() { return fromName; } 385 /** 386 * Validates Strings for OpenLCB format. 387 * @param name the system name to validate. 388 * @param locale the locale for a localized exception. 389 * @param prefix system prefix, eg. MT for OpenLcb turnout. 390 * @return the unchanged value of the name parameter. 391 * @throws jmri.NamedBean.BadSystemNameException if provided name is an invalid format. 392 */ 393 @Nonnull 394 public static String validateSystemNameFormat(@Nonnull String name, @Nonnull java.util.Locale locale, 395 @Nonnull String prefix, final CanSystemConnectionMemo memo) throws BadSystemNameException { 396 String oAddr = name.substring(prefix.length()); 397 OlcbAddress a = new OlcbAddress(oAddr, memo); 398 OlcbAddress[] v = a.split(memo); 399 if (v == null) { 400 throw new BadSystemNameException(locale,"InvalidSystemNameCustom","Did not find usable system name: " + name + " does not convert to a valid Olcb address"); 401 } 402 switch (v.length) { 403 case 1: 404 case 2: 405 break; 406 default: 407 throw new BadSystemNameException(locale,"InvalidSystemNameCustom","Wrong number of events in address: " + name); 408 } 409 return name; 410 } 411 412 /** 413 * Validates 2 part Hardware Address Strings for OpenLCB format. 414 * @param name the system name to validate. 415 * @param locale the locale for a localized exception. 416 * @param prefix system prefix, eg. MT for OpenLcb turnout. 417 * @return the unchanged value of the name parameter. 418 * @throws jmri.NamedBean.BadSystemNameException if provided name is an invalid format. 419 */ 420 @Nonnull 421 public static String validateSystemNameFormat2Part(@Nonnull String name, @Nonnull java.util.Locale locale, 422 @Nonnull String prefix, final CanSystemConnectionMemo memo) throws BadSystemNameException { 423 String oAddr = name.substring(prefix.length()); 424 OlcbAddress a = new OlcbAddress(oAddr, memo); 425 OlcbAddress[] v = a.split(memo); 426 if (v == null) { 427 throw new BadSystemNameException(locale,"InvalidSystemNameCustom","Did not find usable system name: " + name + " to a valid Olcb address"); 428 } 429 if ( v.length == 2 ) { 430 return name; 431 } 432 throw new BadSystemNameException(locale,"InvalidSystemNameCustom","Address requires 2 Events: " + name); 433 } 434 435 /** 436 * See {@link jmri.NamedBean#compareSystemNameSuffix} for background. 437 * This is a common implementation for OpenLCB Sensors and Turnouts 438 * of the comparison method. 439 * 440 * @param suffix1 1st suffix to compare. 441 * @param suffix2 2nd suffix to compare. 442 * @return true if suffixes match, else false. 443 */ 444 @CheckReturnValue 445 public static int compareSystemNameSuffix(@Nonnull String suffix1, @Nonnull String suffix2, final CanSystemConnectionMemo memo) { 446 447 // extract addresses 448 OlcbAddress[] array1 = new OlcbAddress(suffix1, memo).split(memo); 449 OlcbAddress[] array2 = new OlcbAddress(suffix2, memo).split(memo); 450 451 // compare on content 452 for (int i = 0; i < Math.min(array1.length, array2.length); i++) { 453 int c = array1[i].compare(array2[i]); 454 if (c != 0) return c; 455 } 456 // check for different length (shorter sorts first) 457 return Integer.signum(array1.length - array2.length); 458 } 459 460 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(OlcbAddress.class); 461 462} 463 464 465