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