001package jmri.jmrix.can.cbus; 002 003import java.util.Arrays; 004import java.util.Locale; 005import java.util.regex.Matcher; 006import java.util.regex.Pattern; 007 008import javax.annotation.Nonnull; 009 010import jmri.JmriException; 011import jmri.jmrix.can.CanMessage; 012import jmri.jmrix.can.CanReply; 013import jmri.util.StringUtil; 014 015import org.slf4j.Logger; 016import org.slf4j.LoggerFactory; 017 018/** 019 * Utilities for handling CBUS addresses. 020 * <p> 021 * CBUS frames have a one byte command and length, optionally followed by data 022 * bytes. JMRI maps these into address strings. 023 * <p> 024 * Forms: 025 * <dl> 026 * <dt>Full hex string preceeded by "X"<dd>Needs to be pairs of digits: 0123, 027 * not 123 028 * <dt>+/-ddd<dd>ddd is node*100,000 (a.k.a NODEFACTOR) + event 029 * <dt>+/-nNNNeEEE<dd>where NNN is a node number and EEE is an event number 030 * </dl> 031 * If ddd < 65536 then the CBUS address is taken to represent a short event. 032 * 033 * @author Bob Jacobsen Copyright (C) 2008 034 * @author Andrew Crosland Copyright (C) 2011 035 */ 036public class CbusAddress { 037 038 // groups 039 // 1: +ddd/-ddd where ddd is node*NODEFACTOR + event 040 // 2: the +/- from that 041 // 3: xhhhhhh 042 // 5: NE form 043 // 6: the +/- from that 044 // 7: optional "N" 045 // 8: node number 046 // 9: event number 047 static final String SINGLE_ADDRESS_PATTERN = "((\\+|-)?\\d++)|([Xx](\\p{XDigit}\\p{XDigit}){1,8})|((\\+|-)?([Nn])?(\\d++)[Ee](\\d++))"; 048 049 private final Matcher hCode = Pattern.compile("^" + SINGLE_ADDRESS_PATTERN + "$").matcher(""); 050 051 private String aString = null; 052 protected int[] aFrame = null; 053 private boolean match = false; 054 055 static final int NODEFACTOR = 100000; 056 057 /** 058 * Construct from string without leading system or type letters. 059 * @param s CBUS Hardware Address format 060 */ 061 public CbusAddress(String s) { 062 aString = s; 063 // now parse 064 match = hCode.reset(aString).matches(); 065 if (match) { 066 if (hCode.group(1) != null) { 067 // hit on +/-ddd, sign is optional and an unsigned value is treated as + 068 String sign = hCode.group(2); 069 String digits = (sign == null) ? hCode.group(1) : hCode.group(1).substring(1); 070 071 // parse as long then range check to avoid int overflow for node >= 21475 072 long n = parseNonNegative(digits); 073 long node = n / NODEFACTOR; 074 long event = n % NODEFACTOR; 075 076 if (n < 0 || !setEventFrame(node, event)) { 077 log.debug("CBUS address {} out of range, node and event must be 0-65535", s); 078 match = false; 079 } else if ("-".equals(sign)) { 080 aFrame[0] = (node > 0) ? CbusConstants.CBUS_ACOF : CbusConstants.CBUS_ASOF; 081 } else { // + or unsigned 082 aFrame[0] = (node > 0) ? CbusConstants.CBUS_ACON : CbusConstants.CBUS_ASON; 083 } 084 } else if (hCode.group(3) != null) { 085 // hit on hex form 086 String l = hCode.group(3); 087 int len = (l.length() - 1) / 2; 088 aFrame = new int[len]; 089 // get the frame data 090 for (int i = 0; i < len; i++) { 091 String two = l.substring(1 + 2 * i, 1 + 2 * i + 2); 092 aFrame[i] = Integer.parseInt(two, 16); 093 } 094 } else if (hCode.group(5) != null) { 095 // hit on EN form 096 long node = parseNonNegative(hCode.group(8)); 097 long event = parseNonNegative(hCode.group(9)); 098 099 if (!setEventFrame(node, event)) { 100 log.debug("CBUS address {} out of range, node and event must be 0-65535", s); 101 match = false; 102 } else if ("-".equals(hCode.group(6))) { 103 aFrame[0] = CbusConstants.CBUS_ACOF; 104 } else { // + or unsigned 105 aFrame[0] = CbusConstants.CBUS_ACON; 106 } 107 } 108 } else { 109 // no match, leave match false and aFrame null 110 } 111 } 112 113 /** 114 * Parse a String of decimal digits. 115 * @param digits the digits to parse 116 * @return the value, or -1 if the value can not be parsed ( eg. too large ). 117 */ 118 private static long parseNonNegative(String digits) { 119 try { 120 return Long.parseLong(digits); 121 } catch (NumberFormatException ex) { 122 return -1; 123 } 124 } 125 126 /** 127 * Create the 5 element aFrame for a long or short event, element 0 is left as 0. 128 * @param node the node number 129 * @param event the event number 130 * @return true if node and event are within 0-65535 and aFrame was set, else false. 131 */ 132 private boolean setEventFrame(long node, long event) { 133 if (node < 0 || node > 0xffff || event < 0 || event > 0xffff) { 134 return false; 135 } 136 aFrame = new int[5]; 137 aFrame[4] = (int) (event & 0xff); 138 aFrame[3] = (int) ((event >> 8) & 0xff); 139 aFrame[2] = (int) (node & 0xff); 140 aFrame[1] = (int) ((node >> 8) & 0xff); 141 return true; 142 } 143 144 /** 145 * Two addresses are equal if they result in the same numeric contents. 146 * Two invalid addresses ( which have no numeric contents ) are also equal. 147 * @param r The other CbusAddress to compare 148 */ 149 @Override 150 public boolean equals(Object r) { 151 if (r == null) { 152 return false; 153 } 154 if (!(r.getClass().equals(this.getClass()))) { 155 return false; 156 } 157 CbusAddress opp = (CbusAddress) r; 158 return Arrays.equals(this.aFrame, opp.aFrame); 159 } 160 161 @Override 162 public int hashCode() { 163 return Arrays.hashCode(aFrame); // 0 for an invalid address 164 } 165 166 public CanMessage makeMessage(int header) { 167 return new CanMessage(aFrame, header); 168 } 169 170 /** 171 * Check if the address String was successfully parsed into a Frame. 172 * @return true if valid, else false. 173 */ 174 public boolean check() { 175 return match; 176 } 177 178 /** 179 * Does the CbusAddress match. 180 * 181 * @param r CanReply or CanMessage being tested 182 * @return true if matches 183 */ 184 public boolean match(jmri.jmrix.AbstractMessage r) { 185 if (r.getNumDataElements() != aFrame.length) { 186 return false; 187 } 188 if (CbusMessage.isShort(r)) { 189 // Skip node number for short events 190 if (aFrame[0] != r.getElement(0)) { 191 return false; 192 } 193 for (int i = 3; i < aFrame.length; i++) { 194 if (aFrame[i] != r.getElement(i)) { 195 return false; 196 } 197 } 198 } else { 199 for (int i = 0; i < aFrame.length; i++) { 200 if (aFrame[i] != r.getElement(i)) { 201 return false; 202 } 203 } 204 } 205 return true; 206 } 207 208 /** 209 * Does the CbusAddress match a CanReply event request. 210 * 211 * @param r CanReply being tested 212 * @return true if matches 213 */ 214 public boolean matchRequest(CanReply r) { 215 if (r.getNumDataElements() != aFrame.length) { 216 return false; 217 } 218 if (CbusMessage.isShort(r)) { 219 // Skip node number for short events 220 if (CbusConstants.CBUS_ASRQ != r.getElement(0)) { 221 return false; 222 } 223 for (int i = 3; i < aFrame.length; i++) { 224 if (aFrame[i] != r.getElement(i)) { 225 return false; 226 } 227 } 228 } else { 229 if (CbusConstants.CBUS_AREQ != r.getElement(0)) { 230 return false; 231 } 232 for (int i = 1; i < aFrame.length; i++) { 233 if (aFrame[i] != r.getElement(i)) { 234 return false; 235 } 236 } 237 } 238 return true; 239 } 240 241 /** 242 * Split a string containing one or more addresses into individual ones. 243 * 244 * @return 0 length if entire string can't be parsed. 245 */ 246 @Nonnull 247 public CbusAddress[] split() { 248 // reject strings ending in ";" 249 if (aString.endsWith(";")) { 250 return new CbusAddress[0]; 251 } 252 253 // split string at ";" points 254 String[] pStrings = aString.split(";"); 255 256 CbusAddress[] retval = new CbusAddress[pStrings.length]; 257 258 for (int i = 0; i < pStrings.length; i++) { 259 // check validity of each 260 if (pStrings[i].isEmpty()) { 261 return new CbusAddress[0]; 262 } 263 if (!hCode.reset(pStrings[i]).matches()) { 264 return new CbusAddress[0]; 265 } 266 retval[i] = new CbusAddress(pStrings[i]); 267 if (!retval[i].check()) { // matched the pattern but out of range 268 return new CbusAddress[0]; 269 } 270 } 271 return retval; 272 } 273 274 /** 275 * Increments a CBUS address by 1 eg +123 to +124 eg -N123E456 to -N123E457 276 * 277 * @param testAddr initial CbusAddress String, eg -N123E456 278 * @return incremented address. 279 * @throws jmri.JmriException if unable to make the address 280 */ 281 @Nonnull 282 public static String getIncrement(@Nonnull String testAddr) throws JmriException{ 283 log.debug("testing address {}", testAddr); 284 validateSysName(testAddr); 285 CbusAddress a = new CbusAddress(testAddr); 286 CbusAddress[] v = a.split(); 287 String newString; 288 switch (v.length) { 289 case 2: 290 int lasta = StringUtil.getLastIntFromString(v[0].toString()); 291 int lastb = StringUtil.getLastIntFromString(v[1].toString()); 292 StringBuilder sb = new StringBuilder(); 293 sb.append(StringUtil.replaceLast(v[0].toString(), String.valueOf(lasta), String.valueOf(lasta + 1))); 294 sb.append(";"); 295 sb.append(StringUtil.replaceLast(v[1].toString(), String.valueOf(lastb), String.valueOf(lastb + 1))); 296 newString = sb.toString(); 297 break; 298 case 1: 299 // get last part and increment 300 int last = StringUtil.getLastIntFromString(v[0].toString()); 301 newString = StringUtil.replaceLast(v[0].toString(), String.valueOf(last), String.valueOf(last + 1)); 302 break; 303 default: 304 throw new JmriException("Unable to increment " + testAddr); 305 } 306 try { 307 return validateSysName(newString); 308 } catch (IllegalArgumentException e) { 309 throw new JmriException("Unable to increment " + testAddr + " " + e.getMessage()); 310 } 311 } 312 313 // not A-F, N or X 314 private static final String[] invalidChars = { 315 "G","H","I","J","K","L","M","S","T","U","V","W","Y","Z", 316 "?",":","++","--",",","*","NN","XX"}; 317 318 /** 319 * Validate a CBUS hardware address validation. 320 * 321 * @param address the hardware address to check, excluding both system prefix and type letter. 322 * @return same address if all OK. 323 * @throws IllegalArgumentException when address is not validated. 324 * or contains too many parts 325 */ 326 public static String validateSysName(String address) throws IllegalArgumentException { 327 328 if (address == null || address.isEmpty()) { 329 throw new IllegalArgumentException("No Address passed "); 330 } 331 // address=address.toUpperCase().trim(); 332 for (String s : invalidChars) { 333 if (address.contains(s)) { 334 throw new jmri.NamedBean.BadSystemNameException(Locale.getDefault(), "InvalidSystemNameCharacter",address,s); 335 } 336 } 337 338 if (address.endsWith(";")) { 339 throw new IllegalArgumentException("Should not end with ; " + address); 340 } 341 342 // 1st set of switch cases enable strings to pass as a CbusAddress if unsigned 343 String[] addressArray = address.split(";"); 344 switch (addressArray.length) { 345 case 1: 346 address = checkPartOfName(addressArray[0], "+"); 347 // adds sign when addressArray[0] is unsigned int (eg. "4" address is updated to "+4") 348 break; 349 case 2: 350 address = checkPartOfName(addressArray[0], "+") + ";" + checkPartOfName(addressArray[1], "-"); 351 break; 352 default: 353 log.debug("validateSysName switch 1 found > 2 events"); 354 throw new IllegalArgumentException("Unable to convert Address: " + address); 355 } 356 357 CbusAddress a = new CbusAddress(address); 358 CbusAddress[] v = a.split(); 359 switch (v.length) { 360 case 1: 361 if (address.startsWith("+") || address.startsWith("-")) { 362 break; 363 } 364 long unsigned; 365 try { 366 unsigned = Long.parseLong(address); // accept unsigned integer 367 if (unsigned > 100000) { 368 break; 369 } 370 } catch (NumberFormatException ex) { 371 log.debug("Unable to convert {} into Cbus format +nn", address); 372 } 373 throw new IllegalArgumentException("can't make 2nd event from address " + address); 374 case 2: 375 break; 376 default: 377 log.debug("validateSysName switch 2 found > 2 events"); 378 throw new IllegalArgumentException("Wrong number of events in address: " + address); 379 } 380 return address; 381 } 382 383 /** 384 * Check part of a CbusAddress. Will add "+" or "-" if not present in part. 385 * 386 * @param testpart string part of Cbus address to check, will accept 387 * unsigned single integer 388 * @param plusOrMinus character to add in front if not yet present 389 * @return part of CBUS address including + or - (on off) sign 390 */ 391 private static String checkPartOfName(String testpart, String plusOrMinus) { 392 long unsigned = 0; 393 String part = testpart; 394 try { 395 unsigned = Long.parseLong(part); // long so node numbers >= 21475 do not overflow 396 log.debug("part {} is integer {}", part, unsigned); 397 if ((part.charAt(0) != '+') && (part.charAt(0) != '-')) { 398 if (unsigned > 0 && unsigned < 65536) { 399 part = plusOrMinus + part; 400 } else if (unsigned == 0) { 401 // an unsigned 0 is not accepted, must be +0 or -0 402 throw new IllegalArgumentException("Unsigned 0 event requires + or - : " + part); 403 } 404 } 405 if (unsigned > 65535 && unsigned < 100000) { 406 throw new IllegalArgumentException("On Too big for an event, too low for node + event : " + part); 407 } 408 if (unsigned < -65535 && unsigned > -100000) { 409 throw new IllegalArgumentException("Off Too big for an event, too low for node + event : " + part); 410 } 411 } catch (NumberFormatException ex) { 412 log.debug("Unable to convert {} into Cbus format +nn", part); 413 } 414 if (unsigned == 0) { 415 // so it's a string. 416 // ignoring anything starting with x or X as it may be a HEX value 417 // which is checked by core CbusAddress 418 try { 419 if (part.toUpperCase().charAt(0) != 'X') { 420 log.debug("not an int or hex {}", part); 421 422 // it's got a string in somewhere, start by checking event number 423 int lasta = StringUtil.getLastIntFromString(part); 424 log.debug("last string {}", lasta); 425 if (lasta > 65535) { 426 throw new IllegalArgumentException("Event Too Large in address: " + part); 427 } 428 int firsta = StringUtil.getFirstIntFromString(part); 429 log.debug("first string {}", firsta); 430 if (firsta > 65535) { 431 throw new IllegalArgumentException("Node Too Large in address: " + part); 432 } 433 } 434 } catch (StringIndexOutOfBoundsException ex) { 435 throw new IllegalArgumentException("Address Too Short? : " + part); 436 } 437 } 438 return part; 439 } 440 441 /** 442 * Used in Testing. 443 * @return true if split length is 1 or 2, else false. 444 */ 445 public boolean checkSplit() { 446 switch (split().length) { 447 case 1: 448 case 2: 449 return true; 450 default: 451 return false; 452 } 453 } 454 455 int[] elements() { 456 return java.util.Arrays.copyOf(aFrame, aFrame.length); 457 } 458 459 /** 460 * eg. X9801D203A4 or +N123E456 461 */ 462 @Override 463 public String toString() { 464 return aString; 465 } 466 467 /** 468 * eg.x9801D203A4 or x90007B01C8 469 * @return x followed by Can Frame Data 470 */ 471 public String toCanonicalString() { 472 String retval = "x"; 473 for (int i = 0; i < aFrame.length; i++) { 474 retval = jmri.util.StringUtil.appendTwoHexFromInt(aFrame[i], retval); 475 } 476 return retval; 477 } 478 479 private static final Logger log = LoggerFactory.getLogger(CbusAddress.class); 480 481}