001package jmri.jmrix.bidib.netbidib; 002 003import java.awt.event.ActionEvent; 004import java.awt.event.ActionListener; 005import java.io.DataInputStream; 006import java.io.DataOutputStream; 007import java.io.FileNotFoundException; 008import java.io.IOException; 009import java.util.Set; 010import jmri.util.FileUtil; 011import java.util.Enumeration; 012import java.util.ArrayList; 013import java.util.List; 014import java.util.Map; 015import java.util.LinkedHashMap; 016import java.net.InetAddress; 017import java.net.NetworkInterface; 018import java.net.UnknownHostException; 019import java.util.Arrays; 020import javax.jmdns.ServiceInfo; 021 022//import jmri.InstanceManager; 023import jmri.jmrix.bidib.BiDiBNetworkPortController; 024import jmri.jmrix.bidib.BiDiBPortController; 025import jmri.jmrix.bidib.BiDiBSystemConnectionMemo; 026import jmri.jmrix.bidib.BiDiBTrafficController; 027import jmri.util.zeroconf.ZeroConfClient; 028//import jmri.util.zeroconf.ZeroConfServiceManager; 029 030import org.bidib.jbidibc.core.MessageListener; 031import org.bidib.jbidibc.core.NodeListener; 032import org.bidib.jbidibc.core.node.listener.TransferListener; 033import org.bidib.jbidibc.messages.ConnectionListener; 034import org.bidib.jbidibc.netbidib.client.NetBidibClient; 035import org.bidib.jbidibc.netbidib.client.BidibNetAddress; 036import org.bidib.jbidibc.netbidib.pairingstore.LocalPairingStore; 037import org.bidib.jbidibc.messages.Node; 038import org.bidib.jbidibc.messages.enums.NetBidibRole; 039import org.bidib.jbidibc.messages.helpers.Context; 040import org.bidib.jbidibc.messages.message.netbidib.NetBidibLinkData; 041import org.bidib.jbidibc.messages.message.netbidib.NetBidibLinkData.PartnerType; 042import org.bidib.jbidibc.messages.utils.ByteUtils; 043import org.bidib.jbidibc.messages.ProtocolVersion; 044import org.bidib.jbidibc.messages.enums.PairingResult; 045import org.bidib.jbidibc.messages.helpers.DefaultContext; 046import org.bidib.jbidibc.netbidib.NetBidibContextKeys; 047import org.bidib.jbidibc.netbidib.client.pairingstates.PairingStateEnum; 048import org.bidib.jbidibc.netbidib.pairingstore.PairingStore; 049import org.bidib.jbidibc.netbidib.pairingstore.PairingStoreEntry; 050 051import org.slf4j.Logger; 052import org.slf4j.LoggerFactory; 053 054/** 055 * Implements BiDiBPortController for the netBiDiB system network 056 * connection. 057 * 058 * @author Eckart Meyer Copyright (C) 2024-2025 059 * 060 * mDNS code based on LIUSBEthernetAdapter. 061 */ 062public class NetBiDiBAdapter extends BiDiBNetworkPortController { 063 064 public static final String NET_BIDIB_DEFAULT_PAIRING_STORE_FILE = "preference:netBiDiBPairingStore.bidib"; 065 static final String OPTION_DEVICE_LIST = "AvailableDeviceList"; 066 static final String OPTION_UNIQUE_ID = "UniqueID"; 067 068 // The PID (product id as part of the Unique ID) was registered for JMRI with bidib.org (thanks to Andreas Kuhtz and Wolfgang Kufer) 069 static final int BIDIB_JMRI_PID = 0x00FE; //don't touch without synchronizing with bidib.org 070 071 private final Map<Long, NetBiDiDDevice> deviceList = new LinkedHashMap<>(); 072 private boolean mDNSConfigure = false; 073 private final javax.swing.Timer delayedCloseTimer; 074 private PairingStore pairingStore = null; 075 private NetBiDiBPairingRequestDialog pairingDialog = null; 076 private ActionListener pairingListener = null; 077 078 private Long uniqueId = null; //also used as mDNS advertisement name 079 long timeout; 080 private ZeroConfClient mdnsClient = null; 081 082 private final BiDiBPortController portController = this; //this instance is used from a listener class 083 084 protected static class NetBiDiDDevice { 085 private PairingStoreEntry pairingStoreEntry = new PairingStoreEntry(); 086 private BidibNetAddress bidibAddress = null; 087 088 public NetBiDiDDevice() { 089 } 090 091 public PairingStoreEntry getPairingStoreEntry() { 092 return pairingStoreEntry; 093 } 094 public void setPairingStoreEntry(PairingStoreEntry pairingStoreEntry) { 095 this.pairingStoreEntry = pairingStoreEntry; 096 //uniqueID = ByteUtils.parseHexUniqueId(pairingStoreEntry.getUid()); 097 } 098 099 public Long getUniqueId() { 100 //return ByteUtils.parseHexUniqueId(pairingStoreEntry.getUid()); 101 //return uniqueID & 0xFFFFFFFFFFL; 102 return ByteUtils.parseHexUniqueId(pairingStoreEntry.getUid()) & 0xFFFFFFFFFFL; 103 } 104 public void setUniqueId(Long uid) { 105 pairingStoreEntry.setUid(ByteUtils.formatHexUniqueId(uid)); 106 } 107 108 public void setAddressAndPort(String addr, String port) { 109 InetAddress address = null; 110 try { 111 address = InetAddress.getLocalHost(); //be sure there is a valid address 112 address = InetAddress.getByName(addr); 113 } 114 catch (UnknownHostException e) { 115 log.error("unable to resolve remote server address {}:", e.toString()); 116 } 117 int portAsInt; 118 try { 119 portAsInt = Integer.parseInt(port); 120 } 121 catch (NumberFormatException e) { 122 portAsInt = 0; 123 } 124 bidibAddress = new BidibNetAddress(address, portAsInt); 125 } 126 public InetAddress getAddress() { 127 return (bidibAddress == null) ? InetAddress.getLoopbackAddress() : bidibAddress.getAddress(); 128 } 129 public int getPort() { 130 return (bidibAddress == null) ? 0 : bidibAddress.getPortNumber(); 131 } 132 public void setAddress(InetAddress addr) { 133 if (addr == null) { 134 bidibAddress = null; 135 } 136 else { 137 bidibAddress = new BidibNetAddress(addr, getPort()); 138 } 139 } 140 public void setPort(int port) { 141 bidibAddress = new BidibNetAddress(getAddress(), port); 142 } 143 144 public String getProductName() { 145 return pairingStoreEntry.getProductName(); 146 } 147 148 public void setProductName(String productName) { 149 pairingStoreEntry.setProductName(productName); 150 } 151 152 public String getUserName() { 153 return pairingStoreEntry.getUserName(); 154 } 155 156 public void setUserName(String userName) { 157 pairingStoreEntry.setUserName(userName); 158 } 159 160 public boolean isPaired() { 161 return pairingStoreEntry.isPaired(); 162 } 163 164 public void setPaired(boolean paired) { 165 pairingStoreEntry.setPaired(paired); 166 } 167 168 public String getString() { 169 String s = pairingStoreEntry.getUserName() 170 + " (" + pairingStoreEntry.getProductName() 171 + ", " + ByteUtils.getUniqueIdAsString(getUniqueId()); 172 if (bidibAddress != null) { 173 s += ", " + bidibAddress.getAddress().toString(); 174 if (getPort() != 0) { 175 s += ":" + String.valueOf(getPort()); 176 } 177 } 178 if (pairingStoreEntry.isPaired()) { 179 s += ", paired"; 180 } 181 s += ")"; 182 return s; 183 } 184 } 185 186 public NetBiDiBAdapter() { 187 //super(new BiDiBSystemConnectionMemo()); 188 setManufacturer(jmri.jmrix.bidib.BiDiBConnectionTypeList.BIDIB); 189 delayedCloseTimer = new javax.swing.Timer(1000, e -> bidib.close() ); 190 delayedCloseTimer.setRepeats(false); 191 try { 192 pairingStore = new LocalPairingStore(FileUtil.getFile(NET_BIDIB_DEFAULT_PAIRING_STORE_FILE)); 193 } 194 catch (FileNotFoundException ex) { 195 log.warn("pairing store file is invalid: {}", ex.getMessage()); 196 } 197 //deviceListAddFromPairingStore(); 198 199 options.put("ConnectionKeepAlive", new Option(Bundle.getMessage("KeepAlive"), 200 new String[]{Bundle.getMessage("KeepAliveLocalPing"),Bundle.getMessage("KeepAliveNone")} )); // NOI18N 201 } 202 203 public void deviceListAddFromPairingStore() { 204 pairingStore.load(); 205 List<PairingStoreEntry> entries = pairingStore.getPairingStoreEntries(); 206 for (PairingStoreEntry pe : entries) { 207 log.debug("Pairing store entry: {}", pe); 208 Long uid = ByteUtils.parseHexUniqueId(pe.getUid()) & 0xFFFFFFFFFFL; 209 NetBiDiDDevice dev = deviceList.get(uid); 210// if (dev == null) { 211// dev = new NetBiDiDDevice(); 212// dev.setPairingStoreEntry(pe); 213// } 214 if (dev != null) { 215 dev.setPaired(pe.isPaired()); 216 deviceList.put(uid, dev); 217 } 218 } 219 } 220 221 @Override 222 public void connect(String host, int port) throws IOException { 223 setHostName(host); 224 setPort(port); 225 connect(); 226 } 227 228 /** 229 * This methods is called from network connection config. It creates the BiDiB object from jbidibc and opens it. 230 * The connectPort method of the traffic controller is called for generic initialisation. 231 * 232 */ 233 @Override 234 public void connect() {// throws IOException { 235 log.debug("connect() starts to {}:{}", getHostName(), getPort()); 236 237 opened = false; 238 239 prepareOpenContext(); 240 241 // create the BiDiB instance 242 bidib = NetBidibClient.createInstance(getContext()); 243 // create the correspondent traffic controller 244 BiDiBTrafficController tc = new BiDiBTrafficController(bidib); 245 this.getSystemConnectionMemo().setBiDiBTrafficController(tc); 246 247 log.debug("memo: {}, netBiDiB: {}", this.getSystemConnectionMemo(), bidib); 248 249 // connect to the device 250 context = tc.connnectPort(this); //must be done before configuring managers since they may need features from the device 251 252 opened = false; 253 if (context != null) { 254 opened = true; 255 } 256 else { 257 //opened = false; 258 log.warn("No device found on port {} ({}})", 259 getCurrentPortName(), getCurrentPortName()); 260 } 261 262// DEBUG! 263// final NetBidibLinkData clientLinkData = ctx.get(Context.NET_BIDIB_CLIENT_LINK_DATA, NetBidibLinkData.class, null); 264// try { 265// bidib.detach(clientLinkData.getUniqueId()); 266// //int magic = bidib.getRootNode().getMagic(0); 267// //log.debug("Root Node returned magic: 0x{}", ByteUtils.magicToHex(magic)); 268// bidib.attach(clientLinkData.getUniqueId()); 269// int magic2 = bidib.getRootNode().getMagic(0); 270// log.debug("Root Node returned magic: 0x{}", ByteUtils.magicToHex(magic2)); 271// } 272// catch (Exception e) { 273// log.warn("get magic failed!"); 274// } 275// /DEBUG! 276 277 } 278 279 private void prepareOpenContext() { 280 if (getContext() == null) { 281 context = new DefaultContext(); 282 } 283 Context ctx = getContext(); 284 285 // Register a local file pairingstore into context 286 try { 287 PairingStore pairingStore = new LocalPairingStore(FileUtil.getFile(NET_BIDIB_DEFAULT_PAIRING_STORE_FILE)); 288 pairingStore.load(); 289 ctx.register(Context.PAIRING_STORE, pairingStore); 290 } 291 catch (FileNotFoundException ex) { 292 log.warn("pairing store file is invalid: {}", ex.getMessage()); 293 } 294 295 final NetBidibLinkData providedClientLinkData = 296 ctx.get(Context.NET_BIDIB_CLIENT_LINK_DATA, NetBidibLinkData.class, null); 297 298 if (providedClientLinkData == null) { //if the context is not already set (not possible so far...) 299 300 final NetBidibLinkData localClientLinkData = new NetBidibLinkData(PartnerType.LOCAL); 301 localClientLinkData.setRequestorName("BiDiB-JMRI-Client"); //Must start with "BiDiB" since this is the begin of MSG_LOCAL_PROTOCOL_SIGNATURE 302 //localClientLinkData.setUniqueId(ByteUtils.convertUniqueIdToLong(uniqueId)); 303 localClientLinkData.setUniqueId(this.getNetBidibUniqueId()); 304 localClientLinkData.setProdString("JMRI"); 305 // Always set the pairing timeout. 306 // There is a default in the jbibibc library, but we can't get the value. 307 localClientLinkData.setRequestedPairingTimeout(20); 308 // set netBiDiB username to the hostname of the local machine. 309 // TODO: make this a user settable connection preference field 310 try { 311 String myHostName = InetAddress.getLocalHost().getHostName(); 312 log.debug("setting netBiDiB username to local hostname: {}", myHostName); 313 localClientLinkData.setUserString(myHostName); 314 } 315 catch (UnknownHostException ex) { 316 log.warn("Cannot determine local host name: {}", ex.toString()); 317 } 318 localClientLinkData.setProtocolVersion(ProtocolVersion.VERSION_0_8); 319 localClientLinkData.setNetBidibRole(NetBidibRole.INTERFACE); 320 321 //localClientLinkData.setRequestedPairingTimeout(netBidibSettings.getPairingTimeout()); TODO use default for now 322 323 log.info("Register the created client link data in the create context: {}", localClientLinkData); 324 ctx.register(Context.NET_BIDIB_CLIENT_LINK_DATA, localClientLinkData); 325 326 } 327 328 ctx.register(BiDiBTrafficController.ASYNCCONNECTIONINIT, true); //netBiDiB uses asynchroneous initialization 329 ctx.register(BiDiBTrafficController.ISNETBIDIB, true); 330 ctx.register(BiDiBTrafficController.USELOCALPING, getOptionState("ConnectionKeepAlive").equals(Bundle.getMessage("KeepAliveLocalPing"))); 331 332 log.debug("Context: {}", ctx); 333 334 } 335 336 /** 337 * {@inheritDoc} 338 */ 339 @Override 340 public void configure() { 341 log.debug("configure"); 342 this.getSystemConnectionMemo().configureManagers(); 343 } 344 345 /** 346 * {@inheritDoc} 347 */ 348 @Override 349 protected void closeConnection() { 350 BiDiBTrafficController tc = this.getSystemConnectionMemo().getBiDiBTrafficController(); 351 if (tc != null) { 352 tc.getBidib().close(); 353 } 354 } 355 356 /** 357 * {@inheritDoc} 358 */ 359 @Override 360 public void registerAllListeners(ConnectionListener connectionListener, Set<NodeListener> nodeListeners, 361 Set<MessageListener> messageListeners, Set<TransferListener> transferListeners) { 362 363 NetBidibClient b = (NetBidibClient)bidib; 364 b.setConnectionListener(connectionListener); 365 b.registerListeners(nodeListeners, messageListeners, transferListeners); 366 } 367 368 /** 369 * Get a unique id for ourself. The product id part is fixed and registered with bidib.org. 370 * The serial number is a hash from the MAC address. 371 * 372 * This is a variation of org.bidib.wizard.core.model.settings.NetBidibSettings.getNetBidibUniqueId(). 373 * Instead of just using the network interface from InetAddress.getLocalHost() - which can result to the loopback-interface, 374 * which does not have a hardware address - we loop through the list of interfaces until we find an interface which is up and 375 * not a loopback. It would be even better, if we check for virtual interfaces (those could be present if VMs run on the machine) 376 * and then exclude them. But there is no generic method to find those interfaces. So we just return an UID derived from the first 377 * found non-loopback interface or the default UID if there is no such interface. 378 * 379 * @return Unique ID as long 380 */ 381 public Long getNetBidibUniqueId() { 382 // set a default UID 383 byte[] uniqueId = 384 new byte[] { 0x00, 0x00, 0x0D, ByteUtils.getLowByte(BIDIB_JMRI_PID), ByteUtils.getHighByte(BIDIB_JMRI_PID), 385 0x00, (byte) 0xE8 }; 386 387 // try to generate the uniqueId from a mac address 388 try { 389 Enumeration<NetworkInterface> nis = NetworkInterface.getNetworkInterfaces(); 390 while (nis.hasMoreElements()) { 391 NetworkInterface networkInterface = nis.nextElement(); 392 // Check if the interface is up and not a loopback 393 if (networkInterface.isUp() && !networkInterface.isLoopback()) { 394 byte[] hardwareAddress = networkInterface.getHardwareAddress(); 395 if (hardwareAddress != null) { 396 String[] hexadecimal = new String[hardwareAddress.length]; 397 for (int i = 0; i < hardwareAddress.length; i++) { 398 hexadecimal[i] = String.format("%02X", hardwareAddress[i]); 399 } 400 String macAddress = String.join("", hexadecimal); 401 log.debug("MAC address used to generate an UID: {} from interface {}", macAddress, networkInterface.getDisplayName()); 402 int hashCode = macAddress.hashCode(); 403 404 uniqueId = 405 new byte[] { 0x00, 0x00, 0x0D, ByteUtils.getLowByte(BIDIB_JMRI_PID), ByteUtils.getHighByte(BIDIB_JMRI_PID), 406 ByteUtils.getHighByte(hashCode), ByteUtils.getLowByte(hashCode) }; 407 408 log.info("Generated netBiDiB uniqueId from the MAC address: {}", 409 ByteUtils.convertUniqueIdToString(uniqueId)); 410 break; 411 } 412 else { 413 log.warn("No hardware address for localhost available. Use default netBiDiB uniqueId."); 414 } 415 } 416 } 417 } 418 catch (Exception ex) { 419 log.warn("Generate the netBiDiB uniqueId from the MAC address failed.", ex); 420 } 421 return ByteUtils.convertUniqueIdToLong(uniqueId); 422 } 423 424 // base class methods for the BiDiBNetworkPortController interface 425 // not used but must be implemented 426 427 @Override 428 public DataInputStream getInputStream() { 429 return null; 430 } 431 432 @Override 433 public DataOutputStream getOutputStream() { 434 return null; 435 } 436 437 // autoconfig via mDNS 438 439 /** 440 * Set whether or not this adapter should be 441 * configured automatically via MDNS. 442 * 443 * @param autoconfig boolean value. 444 */ 445 @Override 446 public void setMdnsConfigure(boolean autoconfig) { 447 log.debug("Setting netBiDiB adapter autoconfiguration to: {}", autoconfig); 448 mDNSConfigure = autoconfig; 449 } 450 451 /** 452 * Get whether or not this adapter is configured 453 * to use autoconfiguration via MDNS. 454 * 455 * @return true if configured using MDNS. 456 */ 457 @Override 458 public boolean getMdnsConfigure() { 459 return mDNSConfigure; 460 } 461 462 /** 463 * Set the server's host name and port 464 * using mdns autoconfiguration. 465 */ 466 @Override 467 public void autoConfigure() { 468 log.info("Configuring BiDiB interface via JmDNS"); 469 //if (getHostName().equals(DEFAULT_IP_ADDRESS)) { 470 // setHostName(""); // reset the hostname to none. 471 //} 472 log.debug("current host address: {} {}, port: {}, UniqueID: {}", getHostAddress(), getHostName(), getPort(), ByteUtils.formatHexUniqueId(getUniqueId())); 473 String serviceType = Bundle.getMessage("defaultMDNSServiceType"); 474 log.debug("Listening for mDNS service: {}", serviceType); 475 if (getUniqueId() != null) { 476 log.info("try to find mDNS announcement for unique id: {} (IP: {})", ByteUtils.getUniqueIdAsString(getUniqueId()), getHostName()); 477 } 478 479// the folowing selections are valid only for a zeroconf server, the client does NOT use them... 480// ZeroConfServiceManager mgr = InstanceManager.getDefault(ZeroConfServiceManager.class); 481// mgr.getPreferences().setUseIPv6(false); 482// mgr.getPreferences().setUseLinkLocal(false); 483// mgr.getPreferences().setUseLoopback(false); 484 485 if (mdnsClient == null) { 486 mdnsClient = new ZeroConfClient(); 487 mdnsClient.startServiceListener(serviceType); 488 timeout = mdnsClient.getTimeout(); //the original default timeout 489 } 490 // leave the wait code below commented out for now. It 491 // does not appear to be needed for proper ZeroConf discovery. 492 //try { 493 // synchronized(mdnsClient){ 494 // // we may need to add a timeout here. 495 // mdnsClient.wait(keepAliveTimeoutValue); 496 // if(log.isDebugEnabled()) mdnsClient.listService(serviceType); 497 // } 498 //} catch(java.lang.InterruptedException ie){ 499 // log.error("MDNS auto Configuration failed."); 500 // return; 501 //} 502 List<ServiceInfo> infoList = new ArrayList<>(); 503 mdnsClient.setTimeout(0); //set minimum timeout 504 long startTime = System.currentTimeMillis(); 505 Long foundUniqueId = null; 506 while (System.currentTimeMillis() < startTime + timeout) { 507 try { 508 // getServices() looks for each other on all interfaces using the timeout set by 509 // setTimeout(). Therefor we have set the timeout to 0 to get the current services list 510 // almost immediately (the real minimum timeout is 200ms - a "feature" of the Jmdns library). 511 // If the mDNS announcement for the requested unique id is not found on any of the interfaces, 512 // we wait a while (1000ms) and try again until the overall timeout is reached. 513 infoList = mdnsClient.getServices(serviceType); 514 log.debug("mDNS: \n{}", infoList); 515 } catch (Exception e) { log.error("Error getting mDNS services list: {}", e.toString()); } 516 517 // Fill the device list with the found info from mDNS records. 518 // infoList always contains the complete list of the mDNS announcements found so far, 519 // so the clear our internal list before filling it (again). 520 deviceList.clear(); 521 522 for (ServiceInfo serviceInfo : infoList) { 523 //log.trace("{}", serviceInfo.getNiceTextString()); 524 log.trace("key: {}", serviceInfo.getKey()); 525 log.trace("server: {}", serviceInfo.getServer()); 526 log.trace("qualified name: {}", serviceInfo.getQualifiedName()); 527 log.trace("type: {}", serviceInfo.getType()); 528 log.trace("subtype: {}", serviceInfo.getSubtype()); 529 log.trace("app: {}, proto: {}", serviceInfo.getApplication(), serviceInfo.getProtocol()); 530 log.trace("name: {}, port: {}", serviceInfo.getName(), serviceInfo.getPort()); 531 log.trace("inet addresses: {}", new ArrayList<>(Arrays.asList(serviceInfo.getInetAddresses()))); 532 log.trace("hostnames: {}", new ArrayList<>(Arrays.asList(serviceInfo.getHostAddresses()))); 533 log.trace("urls: {}", new ArrayList<>(Arrays.asList(serviceInfo.getURLs()))); 534 Enumeration<String> propList = serviceInfo.getPropertyNames(); 535 while (propList.hasMoreElements()) { 536 String prop = propList.nextElement(); 537 log.trace("service info property {}: {}", prop, serviceInfo.getPropertyString(prop)); 538 } 539 Long uid = ByteUtils.parseHexUniqueId(serviceInfo.getPropertyString("uid")) & 0xFFFFFFFFFFL; 540 // if the same UID is announced twice (or more) overwrite the previous entry 541 NetBiDiDDevice dev = deviceList.getOrDefault(uid, new NetBiDiDDevice()); 542 dev.setAddress(serviceInfo.getInetAddresses()[0]); 543 dev.setPort(serviceInfo.getPort()); 544 dev.setUniqueId(uid); 545 dev.setProductName(serviceInfo.getPropertyString("prod")); 546 dev.setUserName(serviceInfo.getPropertyString("user")); 547 deviceList.put(uid, dev); 548 549 log.info("Found announcement: {}", dev.getString()); 550 551 // if no current unique id is known, try the known IP address if valid 552 if (getUniqueId() == null) { 553 try { 554 InetAddress curHostAddr = InetAddress.getByName(getHostName()); 555 if (dev.getAddress().equals(curHostAddr)) { 556 setUniqueId(dev.getUniqueId()); 557 } 558 } 559 catch (UnknownHostException e) { log.trace("No known hostname {}", getHostName()); } //no known host address is not an error 560 } 561 562 // set current hostname and port from the list if the this entry is the requested unique id 563 if (uid.equals(getUniqueId())) { 564 setHostName(dev.getAddress().getHostAddress()); 565 setPort(dev.getPort()); 566 foundUniqueId = uid; //we have found what we have looked for 567 //break; //exit the for loop as 568 } 569 } 570 if (foundUniqueId != null) { 571 break; //the while loop 572 } 573 try { 574 Thread.sleep(1000); //wait a moment and then try again until timeout has been reached or the announcement was found 575 } catch (final InterruptedException e) { 576 /* Stub */ 577 } 578 } 579 580 // some log info 581 if (foundUniqueId == null) { 582 // Write out a warning if we have been looking for a known uid. 583 // If we don't have a request uid, this is no warning as we just collect the announcements. 584 if (getUniqueId() != null) { 585 log.warn("no mDNS announcement found for requested unique id {} - last known IP: {}", ByteUtils.formatHexUniqueId(getUniqueId()), getHostName()); 586 } 587 } 588 else { 589 log.info("using mDNS announcement: {}", deviceList.get(foundUniqueId).getString()); 590 } 591 592 deviceListAddFromPairingStore(); //add "paired" status from the pairing store to the device list 593 } 594 595 /** 596 * Get and set the ZeroConf/mDNS advertisement name. 597 * <p> 598 * This value is the unique id in BiDiB. 599 * 600 * @return advertisement name. 601 */ 602 @Override 603 public String getAdvertisementName() { 604 //return Bundle.getMessage("defaultMDNSServiceName"); 605 //return ByteUtils.formatHexUniqueId(uniqueId); 606 /////// use "VnnPnnnnnn" instead 607 return ByteUtils.getUniqueIdAsStringCompact(getUniqueId()); 608 } 609 610 @Override 611 public void setAdvertisementName(String AdName) { 612 // AdName has the format "VvvPppppssss" 613 setUniqueId(ByteUtils.parseHexUniqueId(AdName.replaceAll("[VP]", ""))); //remove V and P and convert the remaining hex string to Long 614 } 615 616 /** 617 * Get the ZeroConf/mDNS service type. 618 * <p> 619 * This value is fixed in BiDiB, so return the default 620 * value. 621 * 622 * @return service type. 623 */ 624 @Override 625 public String getServiceType() { 626 return Bundle.getMessage("defaultMDNSServiceType"); 627 } 628 629 // netBiDiB Adapter specific methods 630 631 /** 632 * Get the device list of all found devices and return them as a map 633 * of strings suitable for display and indexed by the unique id. 634 * 635 * This is used by the connection config. 636 * 637 * @return map of strings containing device info. 638 */ 639 640 public Map<Long, String> getDeviceListEntries() { 641 Map<Long, String> stringList = new LinkedHashMap<>(); 642 for (NetBiDiDDevice dev : deviceList.values()) { 643 stringList.put(dev.getUniqueId(), dev.getString()); 644 } 645 return stringList; 646 } 647 648 /** 649 * Set hostname, port and unique id from the device list entry selected by a given index. 650 * 651 * @param i selected index into device list 652 */ 653 public void selectDeviceListItem(int i) { 654 if (i >= 0 && i < deviceList.size()) { 655 List<Map.Entry<Long, NetBiDiDDevice>> entryList = new ArrayList<>(deviceList.entrySet()); 656 NetBiDiDDevice dev = entryList.get(i).getValue(); 657 log.trace("index {}: uid: {}, entry: {}", i, ByteUtils.formatHexUniqueId(entryList.get(i).getKey()), entryList.get(i).getValue().getString()); 658 // update host name, port and unique id from device list 659 setHostName(dev.getAddress().getHostAddress()); 660 setPort(dev.getPort()); 661 setUniqueId(dev.getUniqueId()); 662 } 663 } 664 665 /** 666 * Get and set the BiDiB Unique ID. 667 * <p> 668 * If we haven't set the unique ID of the connection before, try to find it from the root node 669 * of the connection. This will work only if the connection is open and not detached. 670 * 671 * @return unique Id as Long 672 */ 673 public Long getUniqueId() { 674 if (uniqueId == null) { 675 if (bidib != null && bidib.isOpened() && !isDetached()) { 676 Node rootNode = getSystemConnectionMemo().getBiDiBTrafficController().getRootNode(); 677 if (rootNode != null && rootNode.getUniqueId() != 0) 678 uniqueId = rootNode.getUniqueId() & 0xFFFFFFFFFFL; 679 } 680 } 681 return uniqueId; 682 } 683 684 public void setUniqueId(Long uniqueId) { 685 this.uniqueId = uniqueId; 686 } 687 688//UNUSED 689// public boolean isLocalPaired() { 690// if (getUniqueId() != null) { 691// NetBiDiDDevice dev = deviceList.get(getUniqueId()); 692// if (dev != null) { 693// return dev.isPaired(); 694// } 695// } 696// return false; 697// } 698 699 /** 700 * Get the connection ready status from the traffic controller 701 * 702 * @return true if the connection is opened and ready to use (paired and logged in) 703 */ 704 public boolean isConnectionReady() { 705 BiDiBSystemConnectionMemo memo = getSystemConnectionMemo(); 706 if (memo != null) { 707 BiDiBTrafficController tc = memo.getBiDiBTrafficController(); 708 if (tc != null) { 709 return tc.isConnectionReady(); 710 } 711 } 712 return false; 713 } 714 715 /** 716 * Set new pairing state. 717 * 718 * If the pairing should be removed, close the connection, set pairing state in 719 * the device list and update the pairing store. 720 * 721 * If pairing should be initiated, a connection is temporary opened and a pariring dialog 722 * is displayed which informs the user to confirm the pairing on the remote device. 723 * If the process has completed, the temporary connection is closed. 724 * 725 * Pairing and unpairing is an asynchroneous process, so an action listener may be provided which 726 * is called when the process has completed. 727 * 728 * @param paired - true if the pairing should be initiated, false if pairing should be removed 729 * @param l - and event listener, called when pairing or unpairing has finished. 730 */ 731 public void setPaired(boolean paired, ActionListener l) { 732 pairingListener = l; 733 if (!paired) { 734 // close existent BiDiB connection 735 if (bidib != null) { 736 if (bidib.isOpened()) { 737 bidib.close(); 738 } 739 } 740 NetBiDiDDevice dev = deviceList.get(getUniqueId()); 741 if (dev != null) { 742 dev.setPaired(false); 743 } 744 // setup Pairing store 745 pairingStore.load(); 746 List<PairingStoreEntry> entries = pairingStore.getPairingStoreEntries(); 747 for (PairingStoreEntry pe : entries) { 748 log.debug("Pairing store entry: {}", pe); 749 Long uid = ByteUtils.parseHexUniqueId(pe.getUid()); //uid is the full uid with all class bits as stored in the pairing store 750 if ((uid & 0xFFFFFFFFFFL) == getUniqueId()) { //check if this uid (without class bits) matches our uid 751 pairingStore.setPaired(uid, false); 752 } 753 } 754 pairingStore.store(); 755 if (pairingListener != null) { 756 pairingListener.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "")); 757 } 758 } 759 else { 760 //connect(); 761 //closeConnection(); 762 prepareOpenContext(); 763 if (bidib == null) { 764 log.info("create netBiDiB instance"); 765 bidib = NetBidibClient.createInstance(getContext()); 766 //log.warn("Pairing request - no BiDiB instance available. This should never happen."); 767 //return; 768 } 769 if (bidib.isOpened()) { 770 log.warn("Pairing request - BiDiB instance is already opened. This should never happen."); 771 return; 772 } 773 ConnectionListener connectionListener = new ConnectionListener() { 774 775 @Override 776 public void opened(String port) { 777 // no implementation 778 log.debug("opened port {}", port); 779 } 780 781 @Override 782 public void closed(String port) { 783 log.debug("closed port {}", port); 784 if (pairingDialog != null) { 785 pairingDialog.hide(); 786 pairingDialog = null; 787 } 788 if (pairingListener != null) { 789 pairingListener.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "")); 790 } 791 } 792 793 @Override 794 public void status(String messageKey, Context context) { 795 // no implementation 796 } 797 798 @Override 799 public void pairingFinished(final PairingResult pairingResult, long uniqueId) { 800 log.debug("** pairingFinished - result: {}, uniqueId: {}", pairingResult, 801 ByteUtils.convertUniqueIdToString(ByteUtils.convertLongToUniqueId(uniqueId))); 802 // The pairing timed out or was cancelled on the server side. 803 // Cancelling is also possible while in normal operation. 804 // Close the connection. 805 if (bidib.isOpened()) { 806 //bidib.close(); //close() from a listener causes an exception in jbibibc, so delay the close 807 delayedCloseTimer.start(); 808 } 809 } 810 811 @Override 812 public void actionRequired(String messageKey, final Context context) { 813 log.info("actionRequired - messageKey: {}, context: {}", messageKey, context); 814 if (messageKey.equals(NetBidibContextKeys.KEY_ACTION_PAIRING_STATE)) { 815 if (context.get(NetBidibContextKeys.KEY_PAIRING_STATE) == PairingStateEnum.Unpaired) { 816 log.trace("**** send pairing request ****"); 817 log.trace("context: {}", context); 818 // Send a pairing request to the remote side and show a dialog so the user 819 // will be informed. 820 bidib.signalUserAction(NetBidibContextKeys.KEY_PAIRING_REQUEST, context); 821 822 pairingDialog = new NetBiDiBPairingRequestDialog(context, portController, new ActionListener() { 823 824 /** 825 * called when the pairing dialog was closed by the user or if the user pressed the cancel-button. 826 * In this case the init should fail. 827 */ 828 @Override 829 public void actionPerformed(ActionEvent ae) { 830 log.debug("pairingDialog cancelled: {}", ae); 831 //bidib.close(); //close() from a listener causes an exception in jbibibc, so delay the close 832 delayedCloseTimer.start(); 833 } 834 }); 835 // Show the dialog. 836 pairingDialog.show(); 837 } 838 } 839 } 840 841 842 }; 843 // open the device 844 String portName = getRealPortName(); 845 log.info("Open BiDiB connection for pairting on \"{}\"", portName); 846 847 bidib = NetBidibClient.createInstance(getContext()); 848 849 try { 850 bidib.setResponseTimeout(1600); 851 bidib.open(portName, connectionListener, null, null, null, context); 852 } 853 catch (Exception e) { 854 log.error("Execute command failed: ", e); // NOSONAR 855 } 856 } 857 } 858 859 /** 860 * Check of the connection is opened. 861 * This does not mean that it is paired or logged on. 862 * 863 * @return true if opened 864 */ 865 public boolean isOpened() { 866 if (bidib != null) { 867 return bidib.isOpened(); 868 } 869 return false; 870 } 871 872 /** 873 * Check if the connection is detached i.e. it is opened, paired 874 * but the logon has been rejected. 875 * 876 * @return true if detached 877 */ 878 public boolean isDetached() { 879 return getSystemConnectionMemo().getBiDiBTrafficController().isDetached(); 880 } 881 882 /** 883 * Set or remove the detached state. 884 * 885 * @param logon - true for logon (attach), false for logoff (detach) 886 */ 887 public void setLogon(boolean logon) { 888 getSystemConnectionMemo().getBiDiBTrafficController().setLogon(logon); 889 } 890 891 public void addConnectionChangedListener(ActionListener l) { 892 getSystemConnectionMemo().getBiDiBTrafficController().addConnectionChangedListener(l); 893 } 894 895 public void removeConnectionChangedListener(ActionListener l) { 896 getSystemConnectionMemo().getBiDiBTrafficController().removeConnectionChangedListener(l); 897 } 898 899// WE USE ZEROCONF CLIENT 900// /** 901// * Get all servers providing the specified service. 902// * 903// * @param service the name of service as generated using 904// * {@link jmri.util.zeroconf.ZeroConfServiceManager#key(java.lang.String, java.lang.String) } 905// * @return A list of servers or an empty list. 906// */ 907// @Nonnull 908// public List<ServiceInfo> getServices(@Nonnull String service) { 909// ArrayList<ServiceInfo> services = new ArrayList<>(); 910// for (JmDNS server : InstanceManager.getDefault(ZeroConfServiceManager.class).getDNSes().values()) { 911// if (server.list(service,0) != null) { 912// services.addAll(Arrays.asList(server.list(service,0))); 913// } 914// } 915// return services; 916// } 917 918 919 private final static Logger log = LoggerFactory.getLogger(NetBiDiBAdapter.class); 920 921 922}