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