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