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}