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}