001package jmri.jmrix.bidib; 002 003import java.util.*; 004import java.util.regex.Matcher; 005import java.util.regex.Pattern; 006 007import org.bidib.jbidibc.messages.*; 008import org.bidib.jbidibc.messages.enums.LcOutputType; 009import org.bidib.jbidibc.messages.enums.PortModelEnum; 010import org.bidib.jbidibc.messages.utils.ByteUtils; 011import org.bidib.jbidibc.messages.utils.NodeUtils; 012import org.slf4j.Logger; 013import org.slf4j.LoggerFactory; 014 015/** 016 * Utilities for handling BiDiB addresses. 017 * 018 * @author Eckart Meyer Copyright (C) 2019-2023 019 * 020 */ 021public class BiDiBAddress { 022 023 private String aString = null; 024 private long nodeuid = 0; 025 private int addr = -1; //port address or DCC address 026 private String addrType = ""; //t: DCC address ("on the track"), p: local port, default is DCC address if a command station node is present 027 private LcOutputType portType; //used in type address mode only, not in flat address mode 028 private Node node = null; 029 030 static final String addrRegex = "^(?:[xX]([0-9a-fA-F]+):|([a-zA-Z0-9_\\-\\.]+):|)([afptAFPT]{0,1})(\\d+)([SLVUMABPI]{0,1})$"; 031 032 // Groups: 033 // 0 - all 034 // 1 - node (hex with X prefix) - null of not present 035 // 2 - node (name starting with a letter, but not X) - null if not present 036 // 3 - address type letter (a, f, p or t), empty string if not present 037 // 4 - address (decimal), required 038 // 5 - port type letter (type address model only), empty string of not present 039 040 private static volatile Pattern addrPattern = Pattern.compile(addrRegex); 041 042 private static final Map<Character, LcOutputType> portTypeList = createPortTypeList(); //port type map 043 044 private static Map<Character, LcOutputType> createPortTypeList() { 045 Map<Character, LcOutputType> l = new HashMap<>(); 046 l.put('S', LcOutputType.SWITCHPORT); 047 l.put('L', LcOutputType.LIGHTPORT); 048 l.put('V', LcOutputType.SERVOPORT); 049 l.put('U', LcOutputType.SOUNDPORT); 050 l.put('M', LcOutputType.MOTORPORT); 051 l.put('A', LcOutputType.ANALOGPORT); 052 l.put('B', LcOutputType.BACKLIGHTPORT); 053 l.put('P', LcOutputType.SWITCHPAIRPORT); 054 l.put('I', LcOutputType.INPUTPORT); 055 return Collections.unmodifiableMap(l); 056 } 057 058 059 /** 060 * Construct from system name - needs prefix and type letter 061 * 062 * @param systemName the JMRI system name for which the adress object is to be created 063 * @param typeLetter the type letter from the calling manager (T, L, S, R) 064 * @param memo connection memo object 065 */ 066 public BiDiBAddress(String systemName, char typeLetter, BiDiBSystemConnectionMemo memo) { 067 aString = systemName.substring(memo.getSystemPrefix().length() + 1); 068 log.debug("ctor: systemName: {}, typeLetter: {}, systemPrefix: {}", systemName, typeLetter, memo.getSystemPrefix()); 069 070 parse(systemName, typeLetter, memo); 071 } 072 073 // now parse 074 // supported formats are 075 // <nodeuid>:<addr> 076 // <addr> use root node 077 // For outputs (Turnouts and signals, type "T"), addr may start with "t" (DCC address), "p" (local port) or "a" (local assessory). 078 // If no address prefix is given, it defaults to DCC address ("t") as long as the node is a command station. 079 // If the node is not command station, it defaults to BiDiB accessory number ("a") for Turnouts and Signals (type letter T) 080 // otherwise to a local port number ("p"). 081 // For inputs (Sensors), addr may start with "f" (Bidib feedback) or "p" (just an input port). Default is "f". 082 083 // type addressing for ports: S=Switch, L=Light, V=Servo, U=Sound, M=Motor, A=Analogout, B=Backlight, P=Switchpair, I=Input 084 // addr: p123S 085 086 private void parse(String systemName, char typeLetter, BiDiBSystemConnectionMemo memo) { 087 BiDiBTrafficController tc = memo.getBiDiBTrafficController(); 088 if (!aString.isEmpty() && systemName.charAt(memo.getSystemPrefix().length()) == typeLetter) { 089 Node foundNode; 090 try { 091 Matcher matcher = addrPattern.matcher(aString); 092 if (!matcher.matches()) { 093 log.trace("systemName {} does not match regular expression", systemName); 094 //throw new Exception("Illegal address: " + aString); 095 throw new jmri.NamedBean.BadSystemNameException(Locale.getDefault(), "InvalidSystemName",systemName,""); 096 } 097 // DEBUG 098// for (int i = 0; i <= matcher.groupCount(); i++) { 099// log.trace(" {}: {}", i, matcher.group(i)); 100// } 101 if (matcher.group(1) != null) { 102 nodeuid = Long.parseLong(matcher.group(1), 16); //nodeuid in hex 103 } 104 else if (matcher.group(2) != null) { 105 Node n = tc.getNodeByUserName(matcher.group(2)); 106 if (n != null) { 107 nodeuid = n.getUniqueId() & 0xFFFFFFFFFFL; 108 } 109 else { 110 throw new Exception("No such node: " + matcher.group(2)); 111 } 112 } 113 addrType = matcher.group(3).toLowerCase(); 114 addr = Integer.parseInt(matcher.group(4)); 115 String t = matcher.group(5).toUpperCase(); 116 if (!t.isEmpty()) { 117 portType = portTypeList.get(t.charAt(0)); 118 } 119 120 if (nodeuid == 0) { 121 // no unique id given - use root node which always has node address 0 122 foundNode = tc.getRootNode(); 123 if (foundNode != null) { 124 nodeuid = foundNode.getUniqueId() & 0xFFFFFFFFFFL; 125 } 126 } 127 else { 128 log.trace("trying UID {}", ByteUtils.formatHexUniqueId(nodeuid)); 129 foundNode = tc.getNodeByUniqueID(nodeuid); 130 } 131 log.trace("found node: {}", foundNode); 132 if (foundNode != null) { 133 long uid = foundNode.getUniqueId(); 134 if (typeLetter == 'S') { 135 switch(addrType) { 136 case "t": 137 addrType = "f"; //what does "t" mean here? Silently convert to "f" 138 if (!NodeUtils.hasFeedbackFunctions(uid)) addrType = ""; 139 break; //don't use "fall through" as some code checkers does not like it... 140 case "f": 141 if (!NodeUtils.hasFeedbackFunctions(uid)) addrType = ""; 142 break; 143 case "p": 144 if (!NodeUtils.hasSwitchFunctions(uid)) addrType = ""; 145 break; 146 case "": 147 if (NodeUtils.hasFeedbackFunctions(uid)) addrType = "f"; 148 else if (NodeUtils.hasSwitchFunctions(uid)) addrType = "p"; 149 break; 150 default: 151 addrType = ""; 152 break; 153 } 154 if (addrType.equals("p")) { 155 if (portType == null) { 156 portType = LcOutputType.INPUTPORT; //types other than Input do not make sense... 157 } 158 if (!portType.equals(LcOutputType.INPUTPORT)) { 159 addrType = ""; 160 } 161 } 162 } 163 else if (typeLetter == 'R') { 164 if (addrType.isEmpty()) { 165 addrType = "f"; 166 } 167 if (!addrType.equals("f")) { 168 addrType = ""; 169 } 170 } 171 else if (typeLetter == 'T') { 172 switch(addrType) { 173 case "a": 174 if (!NodeUtils.hasAccessoryFunctions(uid)) addrType = ""; 175 break; 176 case "p": 177 if (!NodeUtils.hasSwitchFunctions(uid)) addrType = ""; 178 break; 179 case "t": 180 if (!NodeUtils.hasCommandStationFunctions(uid)) addrType = ""; 181 break; 182 case "": 183 if (NodeUtils.hasCommandStationFunctions(uid)) addrType = "t"; 184 else if (NodeUtils.hasAccessoryFunctions(uid)) addrType = "a"; 185 else if (NodeUtils.hasSwitchFunctions(uid)) addrType = "p"; 186 break; 187 default: 188 addrType = ""; 189 break; 190 } 191 if (addrType.equals("p") && portType != null && portType.equals(LcOutputType.INPUTPORT)) { 192 addrType = ""; 193 } 194 } 195 else if (typeLetter == 'L') { 196 switch(addrType) { 197 case "p": 198 if (!NodeUtils.hasSwitchFunctions(uid)) addrType = ""; 199 break; 200 case "t": 201 if (!NodeUtils.hasCommandStationFunctions(uid)) addrType = ""; 202 break; 203 case "": 204 if (NodeUtils.hasSwitchFunctions(uid)) addrType = "p"; 205 else if (NodeUtils.hasCommandStationFunctions(uid)) addrType = "t"; 206 break; 207 default: 208 addrType = ""; 209 break; 210 } 211 if (addrType.equals("p") && portType != null && portType.equals(LcOutputType.INPUTPORT)) { 212 addrType = ""; 213 } 214 } 215 if (addrType.equals("p")) { 216 if (!foundNode.isPortFlatModelAvailable() && portType == null) { 217// addrType = ""; //type addr model must have a port type 218 portType = LcOutputType.SWITCHPORT; 219 } 220 } 221 else { 222 if (portType != null) { 223 addrType = ""; //port type not allowed on other address types than 'p' 224 } 225 } 226 if (addr >= 0 && !addrType.isEmpty()) { 227 node = foundNode; 228 } 229 } 230 if (!isValid()) { 231 throw new Exception("Invalid BiDiB address: " + systemName); 232 } 233 } 234 catch (Exception e) { 235 //log.trace("parse of BiDiBAddress throws {}", e); 236 node = null; 237 } 238 } 239 240 if (isValid()) { 241 log.debug("BiDiB \"{}\" -> {}", systemName, toString()); 242 } 243 else { 244 log.warn("*** BiDiB system name \"{}\" is invalid", systemName); 245 } 246 } 247 248 /** 249 * Static method to check system name syntax. Does not check if the node is available 250 * 251 * @param systemName the JMRI system name for which the adress object is to be created 252 * @param typeLetter the type letter from the calling manager (T, L, S, R) 253 * @param memo connection memo object 254 * @return true if the system name is syntactically valid. 255 */ 256 static public boolean isValidSystemNameFormat(String systemName, char typeLetter, BiDiBSystemConnectionMemo memo) { 257 String aString = systemName.substring(memo.getSystemPrefix().length() + 1); 258 if (addrPattern == null) { 259 addrPattern = Pattern.compile(addrRegex); 260 log.trace("regexp: {}", addrRegex); 261 } 262 if (!aString.isEmpty() && systemName.charAt(memo.getSystemPrefix().length()) == typeLetter) { 263 Matcher matcher = addrPattern.matcher(aString); 264 if (matcher.matches()) { 265 return true; 266 } 267 else { 268 log.trace("systemName {} does not match regular expression", systemName); 269 //throw new Exception("Illegal address: " + aString); 270 //throw new jmri.NamedBean.BadSystemNameException(Locale.getDefault(), "InvalidSystemName",systemName); 271 return false; 272 } 273 } 274 return false; 275 } 276 277 /** 278 * Invalidate this BiDiBAddress by removing the node. 279 * Used when the node gets lost. 280 */ 281 public void invalidate() { 282 log.warn("BiDiB address invalidated: {}", this); 283 node = null; 284 nodeuid = 0; 285 } 286 287 /** 288 * Check if the object contains a valid BiDiB address 289 * The object is invalied the the system is syntactically wrong or if the requested node is not available 290 * 291 * @return true if valid 292 */ 293 public boolean isValid() { 294 return (node != null); 295 } 296 297 /** 298 * Check if the address is a BiDiB Port address (LC) 299 * 300 * @return true if the object represents a BiDiB Port address 301 */ 302 public boolean isPortAddr() { 303 return (addrType.equals("p")); 304 } 305 306 /** 307 * Check if the address is a BiDiB Accessory address. 308 * 309 * @return true if the object represents a BiDiB Accessory address 310 */ 311 public boolean isAccessoryAddr() { 312 return (addrType.equals("a")); 313 } 314 315 /** 316 * Check if the address is a BiDiB feedback Number (BM). 317 * 318 * @return true if the object represents a BiDiB feedback Number 319 */ 320 public boolean isFeedbackAddr() { 321 return (addrType.equals("f")); 322 } 323 324 /** 325 * Check if the address is a BiDiB track address (i.e. a DCC accessory address). 326 * 327 * @return true if the object represents a BiDiB track address 328 */ 329 public boolean isTrackAddr() { 330 return (addrType.equals("t")); 331 } 332 333 /** 334 * Get the 40 bit unique ID of the found node 335 * 336 * @return the 40 bit node unique ID 337 */ 338 public long getNodeUID() { 339 return nodeuid; 340 } 341 342 /** 343 * Get the address inside the node. 344 * This may be a DCC address (for DCC-Accessories), an accessory number (for BiDiB accessories) 345 * or a port number (for LC ports) 346 * 347 * @return address inside node 348 */ 349 public int getAddr() { 350 return addr; 351 } 352 353 /** 354 * Get the address as string exactly as given when the instance has been created 355 * 356 * @return address as string 357 */ 358 public String getAddrString() { 359 return aString; 360 } 361 362 /** 363 * Get the BiDiB Node object. 364 * If the node is not available, null is returned. 365 * 366 * @return Node object or null, if node is not available 367 */ 368 public Node getNode() { 369 return node; 370 } 371 372 /** 373 * Get the BiDiB Node address. 374 * If the node is not available, an empty address array is returned. 375 * Note: The BiDiB node address is dynamically created address from the BiDiBbus 376 * and is not suitable as a node ID. Use the Unique ID for that purpose. 377 * 378 * @return Node address (byte array) or empty address array, if node is not available 379 */ 380 public byte[] getNodeAddr() { 381 byte[] ret = {}; 382 if (node != null) { 383 ret = node.getAddr(); 384 } 385 return ret; 386 } 387 388 /** 389 * Get the port type as an LcOutputType object (SWITCHPORT, LIGHTPORT, ...) 390 * 391 * @return LcOutputType object 392 */ 393 public LcOutputType getPortType() { 394 return portType; 395 } 396 397 /** 398 * Get the address type as a lowercase single letter: 399 * t - DCC address of decoder (t stands for "Track") 400 * a - BiDiB Accessory Number 401 * p - BiDiB Port 402 * f - BiDiB Feedback Number (BM) 403 * 404 * Not a public method since we want to hide this letter, 405 * use isPortAddr() or isAccessoryAddr() instead. 406 * 407 * @return single letter address type 408 */ 409 protected String getAddrtype() { 410 return addrType; 411 } 412 413 // some convenience methods 414 415 /** 416 * Check if the object contains an address in the BiDiB type based address model. 417 * Returns false if the address is in the flat address model. 418 * 419 * @return true if address is in the BiDiB type based address model. 420 */ 421 public boolean isPortTypeBasedModel() { 422 if (node != null) { 423 if (isPortAddr() && !node.isPortFlatModelAvailable()) { 424 return true; 425 } 426 } 427 return false; 428 } 429 430 /** 431 * Check address against a BiDiBAddress object. 432 * 433 * @param other as BiDiBAddress 434 * @return true if same 435 */ 436 public boolean isAddressEqual(BiDiBAddress other) { 437 if (node == null || other.getNodeUID() != getNodeUID() || other.getAddr() != addr || !other.getAddrtype().equals(addrType)) { 438 return false; 439 } 440 if (isPortAddr() && isPortTypeBasedModel()) { 441 return other.getPortType() == portType; 442 } 443 return true; 444 } 445 446 /** 447 * Check address against a LcConfig object. 448 * 449 * @param lcConfig as LcConfig 450 * @return true if the address contained in the LcConfig object is the same 451 */ 452 public boolean isAddressEqual(LcConfig lcConfig) { 453 return isAddressEqual(lcConfig.getBidibPort()); 454 } 455 456 /** 457 * Check address against a LcConfigX object. 458 * 459 * @param lcConfigX as LcConfig 460 * @return true if the address contained in the LcConfigX object is the same 461 */ 462 public boolean isAddressEqual(LcConfigX lcConfigX) { 463 return isAddressEqual(lcConfigX.getBidibPort()); 464 } 465 466 /** 467 * Check address against a BiDiBAddress object. 468 * 469 * @param bidibPort as BiDiBPort 470 * @return true if same 471 */ 472 public boolean isAddressEqual(BidibPort bidibPort) { 473 if (node == null || !isPortAddr()) { 474 return false; 475 } 476 if (node.isPortFlatModelAvailable()) { 477 return bidibPort.getPortNumber(PortModelEnum.flat_extended) == addr; 478 } 479 else { 480 return (bidibPort.getPortNumber(PortModelEnum.type) == addr && bidibPort.getPortType(PortModelEnum.type) == portType); 481 } 482 } 483 484 /** 485 * Create a BiDiBPort object from this object 486 * 487 * @return new BiDiBPort object 488 */ 489 public BidibPort makeBidibPort() { 490 if (node == null || !isPortAddr()) { 491 return null; 492 } 493 return BidibPort.prepareBidibPort(PortModelEnum.getPortModel(node), portType, addr); 494 } 495 496 /** 497 * Static method to parse a system Name. 498 * A temporary BiDiDAdress object is created. 499 * 500 * @param systemName the JMRI system name for which the adress object is to be created 501 * @param typeLetter the type letter from the calling manager (T, L, S, R) 502 * @param memo connection memo object 503 * @return true if the system name is valid and the BiDiB Node is available 504 * @throws IllegalArgumentException when needed 505 */ 506 static public boolean isValidAddress(String systemName, char typeLetter, BiDiBSystemConnectionMemo memo) throws IllegalArgumentException { 507 BiDiBAddress addr = new BiDiBAddress(systemName, typeLetter, memo); 508 return addr.isValid(); 509 } 510 511 /** 512 * {@inheritDoc} 513 */ 514 @Override 515 public String toString() { 516 String s = ""; 517 if (isPortAddr()) { 518 s = "(" + (isPortTypeBasedModel() ? "type-based" : "flat") + "),portType=" + portType; 519 } 520 return "BiDiBAdress[UID=" + ByteUtils.formatHexUniqueId(nodeuid) + ",addrType=" + addrType + ",addr=" + addr + s + "]"; 521 } 522 523 private final static Logger log = LoggerFactory.getLogger(BiDiBAddress.class); 524}