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