001package jmri.jmrix.bidib;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004
005import java.util.HashMap;
006import java.util.TreeMap;
007import java.util.LinkedHashSet;
008import java.util.List;
009import java.util.Map;
010import java.util.Set;
011import java.util.Collection;
012import java.util.concurrent.CountDownLatch;
013import java.util.concurrent.TimeUnit;
014import java.util.concurrent.atomic.AtomicBoolean;
015
016import java.awt.event.ActionListener;
017import java.awt.event.ActionEvent;
018
019import jmri.CommandStation;
020import jmri.jmrix.PortAdapter;
021import jmri.NmraPacket;
022import jmri.InstanceManager;
023import jmri.ShutDownManager;
024import jmri.ShutDownTask;
025import jmri.implementation.AbstractShutDownTask;
026import jmri.jmrix.bidib.netbidib.NetBiDiBPairingRequestDialog;
027
028import org.bidib.jbidibc.messages.BidibLibrary;
029import org.bidib.jbidibc.messages.exception.ProtocolException;
030import org.bidib.jbidibc.messages.utils.ByteUtils;
031
032import org.bidib.jbidibc.core.BidibMessageProcessor;
033import org.bidib.jbidibc.core.BidibInterface;
034import org.bidib.jbidibc.messages.BidibPort;
035import org.bidib.jbidibc.messages.ConnectionListener;
036import org.bidib.jbidibc.core.DefaultMessageListener;
037import org.bidib.jbidibc.messages.Feature;
038import org.bidib.jbidibc.messages.LcConfig;
039import org.bidib.jbidibc.messages.LcConfigX;
040import org.bidib.jbidibc.core.MessageListener;
041import org.bidib.jbidibc.messages.base.RawMessageListener;
042import org.bidib.jbidibc.messages.Node;
043import org.bidib.jbidibc.core.NodeListener;
044import org.bidib.jbidibc.messages.ProtocolVersion;
045import org.bidib.jbidibc.messages.StringData;
046import org.bidib.jbidibc.messages.helpers.Context;
047import org.bidib.jbidibc.core.node.listener.TransferListener;
048import org.bidib.jbidibc.messages.exception.PortNotFoundException;
049import org.bidib.jbidibc.messages.message.BidibCommandMessage;
050import org.bidib.jbidibc.core.node.BidibNodeAccessor;
051import org.bidib.jbidibc.messages.utils.NodeUtils;
052import org.bidib.jbidibc.messages.enums.CommandStationState;
053import org.bidib.jbidibc.messages.enums.LcOutputType;
054import org.bidib.jbidibc.messages.enums.PortModelEnum;
055import org.bidib.jbidibc.messages.message.AccessoryGetMessage;
056import org.bidib.jbidibc.messages.message.BidibRequestFactory;
057import org.bidib.jbidibc.messages.message.CommandStationSetStateMessage;
058import org.bidib.jbidibc.messages.message.FeedbackGetRangeMessage;
059import org.bidib.jbidibc.messages.message.LocalPingMessage;
060import org.bidib.jbidibc.core.node.CommandStationNode;
061import org.bidib.jbidibc.core.node.BoosterNode;
062import org.bidib.jbidibc.core.node.RootNode;
063import org.bidib.jbidibc.messages.BoosterStateData;
064import org.bidib.jbidibc.messages.enums.BoosterControl;
065import org.bidib.jbidibc.messages.enums.BoosterState;
066import org.bidib.jbidibc.messages.enums.CommandStationProgState;
067import org.bidib.jbidibc.messages.enums.PairingResult;
068import org.bidib.jbidibc.messages.message.netbidib.NetBidibLinkData;
069import org.bidib.jbidibc.messages.port.BytePortConfigValue;
070import org.bidib.jbidibc.messages.port.PortConfigValue;
071import org.bidib.jbidibc.messages.port.ReconfigPortConfigValue;
072import org.bidib.jbidibc.messages.utils.CollectionUtils;
073import org.bidib.jbidibc.netbidib.NetBidibContextKeys;
074import org.bidib.jbidibc.netbidib.client.pairingstates.PairingStateEnum;
075import org.bidib.jbidibc.simulation.comm.SimulationBidib;
076
077import org.slf4j.Logger;
078import org.slf4j.LoggerFactory;
079
080/**
081 * The BiDiB Traffic Controller provides the interface for JMRI to the BiDiB Library (jbidibc) - it
082 * does not handle any protocol functions itself. Therefor it does not extend AbstractMRTrafficController.
083 * Instead, it delegates BiDiB handling to a BiDiB controller instance (serial, simulation, etc.) using BiDiBInterface.
084 * 
085 * @author Bob Jacobsen Copyright (C) 2002
086 * @author Eckart Meyer Copyright (C) 2019-2024
087 *
088 */
089
090@SuppressFBWarnings(value = "JLM_JSR166_UTILCONCURRENT_MONITORENTER")
091// This code uses several AtomicBoolean variables as synch objects.  In this use, 
092// they're synchronizing access to code blocks, not just synchronizing access
093// to the underlying boolean value. it would be possible to use separate
094// Object variables for this process, but keeping those in synch would actually
095// be more complex and confusing than this approach.
096
097public class BiDiBTrafficController implements CommandStation {
098    
099    public static final String ASYNCCONNECTIONINIT = "jmri-async-init";
100    public static final String ISNETBIDIB = "jmri-is-netbidib";
101
102    private final BidibInterface bidib;
103    private final Set<TransferListener> transferListeners = new LinkedHashSet<>();
104    private final Set<MessageListener> messageListeners = new LinkedHashSet<>();
105    private final Set<NodeListener> nodeListeners = new LinkedHashSet<>();
106    private final AtomicBoolean stallLock = new AtomicBoolean();
107    private java.util.TimerTask watchdogTimer = null;
108    private final AtomicBoolean watchdogStatus = new AtomicBoolean();
109    private final CountDownLatch continueLock = new CountDownLatch(1); //wait for all initialisations are done
110    private NetBiDiBPairingRequestDialog pairingDialog = null;
111    private PairingResult curPairingResult = PairingResult.UNPAIRED;
112    private boolean isAsyncInit = false;
113    private boolean isNetBiDiB = false;
114    private boolean connectionIsReady = false;
115    private final BiDiBNodeInitializer nodeInitializer;
116    private BiDiBPortController portController;
117    private final Set<ActionListener> connectionChangedListeners = new LinkedHashSet<>();
118//    private Long deviceUniqueId = null; //this is the unique Id of the device (root node)
119    protected final TreeMap<Long, Node> nodes = new TreeMap<>(); //our node list - use TreeMap since it retains order if insertion (HashMap has arbitrary order)
120
121    private Node cachedCommandStationNode = null;
122    
123    private final AtomicBoolean mIsProgMode = new AtomicBoolean();
124    volatile protected CommandStationState mSavedMode;
125    private Node currentGlobalProgrammerNode = null;
126    private final javax.swing.Timer progTimer = new javax.swing.Timer(200, e -> progTimeout());
127    private final javax.swing.Timer localPingTimer = new javax.swing.Timer(4000, e -> localPingTimeout());
128    private final javax.swing.Timer delayedCloseTimer;
129
130    private final Map<Long, String> debugStringBuffer = new HashMap<>();
131
132    /**
133     * Create a new BiDiBTrafficController instance.
134     * Must provide a BidibInterface reference at creation time.
135     *
136     * @param b reference to associated jbidibc object,
137     *                        preserved for later.
138     */
139    public BiDiBTrafficController(BidibInterface b) {
140        bidib = b;
141        delayedCloseTimer = new javax.swing.Timer(1000, e -> bidib.close() );
142        delayedCloseTimer.setRepeats(false);
143
144        log.debug("BiDiBTrafficController created");
145        mSavedMode = CommandStationState.OFF;
146        setWatchdogTimer(false); //preset not enabled
147
148        progTimer.setRepeats(false);
149        mIsProgMode.set(false);
150        
151        nodeInitializer = new BiDiBNodeInitializer(this, bidib, nodes);
152                
153    }
154    
155    /**
156     * Opens the BiDiB connection in the jbidibc library, add listeners and initialize BiDiB.
157     * 
158     * @param p BiDiB port adapter (serial or simulation)
159     * @return a jbidibc context
160     */
161    @SuppressFBWarnings(value = "BC_UNCONFIRMED_CAST",justification = "Cast safe by design")
162    public Context connnectPort(PortAdapter p) {
163        // init bidib
164        portController = (BiDiBPortController)p;
165        Context context = portController.getContext();
166        
167        // there is currently no difference between "use async init" and "this is a netBiDiB device"...
168        isAsyncInit = context.get(ASYNCCONNECTIONINIT, Boolean.class, false);
169        isNetBiDiB = context.get(ISNETBIDIB, Boolean.class, false);
170        
171        stallLock.set(false); //not stalled
172        
173        messageListeners.add(new DefaultMessageListener() {
174
175            @Override
176            public void error(byte[] address, int messageNum, int errorCode, byte[] reasonData) {
177                log.debug("Node error event: addr: {}, msg num: {}, error code: {}, data: {}", address, messageNum, errorCode, reasonData);
178                if (errorCode == 1) {
179                    log.info("error: {}", new String(reasonData));
180                }
181            }
182
183            @Override
184            @SuppressFBWarnings(value = "SLF4J_SIGN_ONLY_FORMAT",justification = "info message contains context information")
185            public void nodeString(byte[] address, int messageNum, int namespace, int stringId, String value) {
186                // handle debug messages from a node
187                if (namespace == StringData.NAMESPACE_DEBUG) {
188                    Node node = getNodeByAddr(address);
189                    String uid = ByteUtils.getUniqueIdAsString(node.getUniqueId());
190                    // the debug string buffer key is the node's 40 bit UID plus the string id in the upper 24 bit
191                    long key = (node.getUniqueId() & 0x0000ffffffffffL) | (long)stringId << 40;
192                    String prefix = "===== BiDiB";
193                    if (value.charAt(value.length() - 1) == '\n') {
194                        String txt = "";
195                        // check if we have previous received imcomplete text
196                        if (debugStringBuffer.containsKey(key)) {
197                            txt = debugStringBuffer.get(key);
198                            debugStringBuffer.remove(key);
199                        }
200                        txt += value.replace("\n","");
201                        switch(stringId) {
202                            case StringData.INDEX_DEBUG_STDOUT:
203                                log.info("{} {} stdout: {}", prefix, uid, txt);
204                                break;
205                            case StringData.INDEX_DEBUG_STDERR:
206                                log.info("{} {} stderr: {}", prefix, uid, txt);
207                                break;
208                            case StringData.INDEX_DEBUG_WARN:
209                                log.warn("{} {}: {}", prefix, uid, txt);
210                                break;
211                            case StringData.INDEX_DEBUG_INFO:
212                                log.info("{} {}: {}", prefix, uid, txt);
213                                break;
214                            case StringData.INDEX_DEBUG_DEBUG:
215                                log.debug("{} {}: {}", prefix, uid, txt);
216                                break;
217                            case StringData.INDEX_DEBUG_TRACE:
218                                log.trace("{} {}: {}", prefix, uid, txt);
219                                break;
220                            default: break;
221                        }
222                    }
223                    else {
224                        log.trace("incomplete debug string received: [{}]", value);
225                        String txt = "";
226                        if (debugStringBuffer.containsKey(key)) {
227                            txt = debugStringBuffer.get(key);
228                        }
229                        debugStringBuffer.put(key, (txt + value));
230                    }
231                }
232            }
233            
234            @Override
235            public void nodeLost(byte[] address, int messageNum, Node node) {
236                log.debug("Node lost event: {}", node);
237                nodeInitializer.nodeLost(node);
238            }
239
240            @Override
241            public void nodeNew(byte[] address, int messageNum, Node node) {
242                log.debug("Node new event: {}", node);
243                nodeInitializer.nodeNew(node);
244            }
245            
246            @Override
247            public void stall(byte[] address, int messageNum, boolean stall) {
248                log.trace("message listener stall! {} node: {}", stall, getNodeByAddr(address));
249                synchronized (stallLock) {
250                    if (log.isDebugEnabled()) {
251                        Node node = getNodeByAddr(address);
252                        log.debug("stall - msg num: {}, new state: {}, node: {}, ", messageNum, stall, node);
253                    }
254                    if (stall != stallLock.get()) {
255                        stallLock.set(stall);
256                        if (!stall) {
257                            log.debug("stall - wake send");
258                            stallLock.notifyAll(); //wake pending send if any
259                        }
260                    }
261                }
262            }
263            
264            @Override
265            public void localPong(byte[] address, int messageNum) {
266                if (log.isTraceEnabled()) {
267                    Node node = getNodeByAddr(address);
268                    log.trace("local pong - msg num: {}, node: {}, ", messageNum, node);
269                }
270            }
271            
272            // don't know if this is the correct place...
273            @Override
274            public void csState(byte[] address, int messageNum, CommandStationState commandStationState) {
275                Node node = getNodeByAddr(address);
276                log.debug("CS STATE event: {} on node {}, current watchdog status: {}", commandStationState, node, watchdogStatus.get());
277                synchronized (mIsProgMode) {
278                    if (CommandStationState.isPtProgState(commandStationState)) {
279                        mIsProgMode.set(true);
280                    }
281                    else {
282                        mIsProgMode.set(false);
283                        mSavedMode = commandStationState;
284                    }
285                }
286                boolean newState = (commandStationState == CommandStationState.GO);
287                if (node == getFirstCommandStationNode()  &&  newState != watchdogStatus.get()) {
288                    log.trace("watchdog: new state: {}, current state: {}", newState, watchdogStatus.get());
289                    setWatchdogTimer(newState);
290                }
291            }
292            @Override
293            public void csProgState(
294                byte[] address, int messageNum, CommandStationProgState commandStationProgState, int remainingTime, int cvNumber, int cvData) {
295                synchronized (progTimer) {
296                    if ( (commandStationProgState.getType() & 0x80) != 0) { //bit 7 = 1 means operation has finished
297                        progTimer.restart();
298                        log.trace("PROG finished, progTimer (re)started.");
299                    }
300                    else {
301                        progTimer.stop();
302                        log.trace("PROG pending, progTimer stopped.");
303                    }
304                }
305            }
306            @Override
307            public void boosterState(byte[] address, int messageNum, BoosterState state, BoosterControl control) {
308                Node node = getNodeByAddr(address);
309                log.info("BOOSTER STATE & CONTROL was signalled: {}, control: {}", state.getType(), control.getType());
310                if (node != getFirstCommandStationNode()  &&  node == currentGlobalProgrammerNode  &&  control != BoosterControl.LOCAL) {
311                    currentGlobalProgrammerNode = null;
312                }
313            }
314        });
315                
316        transferListeners.add(new TransferListener() {
317
318            @Override
319            public void sendStopped() {
320                // no implementation
321                //log.trace("sendStopped");
322            }
323
324            @Override
325            public void sendStarted() {
326                log.debug("sendStarted");
327                // TODO check node!
328                synchronized (stallLock) {
329                    if (stallLock.get()) {
330                        try {
331                            log.debug("sendStarted is stalled - waiting...");
332                            stallLock.wait(1000L);
333                            log.debug("sendStarted stall condition has been released");
334                        }
335                        catch (InterruptedException e) {
336                            log.warn("waited too long for releasing stall condition - continue...");
337                            stallLock.set(false);
338                        }
339                    }
340                }
341            }
342
343            @Override
344            public void receiveStopped() {
345                // no implementation
346                //log.trace("receiveStopped");
347            }
348
349            @Override
350            public void receiveStarted() {
351                // no implementation
352                //log.trace("receiveStarted");
353            }
354
355            @Override
356            public void ctsChanged(boolean cts, boolean manualEvent) { //new
357//            public void ctsChanged(boolean cts) { //jbidibc 12.5
358                // no implementation
359                log.trace("ctsChanged");
360            }
361        });
362        
363        ConnectionListener connectionListener = new ConnectionListener() {
364            
365            /**
366             * The port was opened.
367             * In case of a netBiDiB connection pairing has not been verified and the
368             * server is not logged in. This will be notified later.
369             * 
370             * For all other connections the port is now usable and we init the
371             * connection just here.
372             * 
373             * @param port is the name of the port just opened.
374             */
375            @Override
376            public void opened(String port) {
377                log.debug("opened port {}", port);
378                if (!isAsyncInit) {
379                    connectionIsReady = true;
380                    nodeInitializer.connectionInit();
381                    continueLock.countDown();
382                }
383            }
384
385            /**
386             * The port was closed
387             * If the local ping feature was enabled (netBiDiB), it stopped now.
388             * 
389             * @param port is the name of the closed port
390             */
391            @Override
392            public void closed(String port) {
393                log.debug("closed port {}", port);
394                if (bidib.getRootNode() != null) {
395                    if (bidib.getRootNode().isLocalPingEnabled()) {
396                        log.debug("Stop local ping");
397                        bidib.getRootNode().stopLocalPingWorker();
398                    }
399                }
400                connectionIsReady = false;
401                nodeInitializer.connectionLost();
402                continueLock.countDown();
403                fireConnectionChanged("closed");
404            }
405
406            @Override
407            public void stall(boolean stall) {
408                // no implementation
409                log.info("connection stall! {}", stall);
410            }
411            
412            @Override
413            public void status(String messageKey, Context context) {
414                // no implementation
415                log.trace("status - message key {}", messageKey);
416            }
417
418            /**
419             * This is called if the pairing state machine is now either in "paired"
420             * or "unpaired" state.
421             * 
422             * If "paired" is signalled, there is nothing more to do since the
423             * connection will only be ready after server has logged in.
424             * 
425             * If pairing failed, timed out or the an existing pairing was removed
426             * on the remote side, we will clear all nodes and then close the connection.
427             * 
428             * @param pairingResult
429             */
430            @Override
431            public void pairingFinished(final PairingResult pairingResult, long uniqueId) {
432                log.debug("** pairingFinished - result: {}, uniqueId: {}", pairingResult,
433                        ByteUtils.convertUniqueIdToString(ByteUtils.convertLongToUniqueId(uniqueId)));
434                curPairingResult = pairingResult;
435                //deviceUniqueId = uniqueId;
436                if (!curPairingResult.equals(PairingResult.PAIRED)) {
437                    // The pairing timed out or was cancelled on the server side.
438                    // Cancelling is also possible while in normal operation.
439                    // Close the connection.
440                    log.warn("Unpaired!");
441                    connectionIsReady = false;
442                    nodeInitializer.connectionLost();
443                    if (continueLock.getCount() == 0  &&  bidib.isOpened()) {
444                        //bidib.close(); //close() from a listener causes an exception in jbibibc, so delay the close
445                        delayedCloseTimer.start();
446                    }
447                    continueLock.countDown();
448                }
449            }
450
451            /**
452             * Called if the connection was not paired before (either local or remote).
453             * Send a pairing request to the remote an display a dialog box to inform
454             * the user to acknoledge the pairing at the remote side. The result is notified
455             * to the pairingFinished() event. If the user presses the Cancel button or closes
456             * the window, the connection will be closed (in the mainline code after unlock).
457             */
458            @Override
459            public void actionRequired(String messageKey, final Context context) {
460                log.info("actionRequired - messageKey: {}, context: {}", messageKey, context);
461                if (messageKey.equals(NetBidibContextKeys.KEY_ACTION_PAIRING_STATE)) {
462                    if (context.get(NetBidibContextKeys.KEY_PAIRING_STATE) == PairingStateEnum.Unpaired) {
463                        log.debug("**** send pairing request ****");
464                        log.trace("context: {}", context);
465                        // Send a pairing request to the remote side and show a dialog so the user
466                        // will be informed.
467                        bidib.signalUserAction(NetBidibContextKeys.KEY_PAIRING_REQUEST, context);
468
469                        pairingDialog = new NetBiDiBPairingRequestDialog(context, portController, new ActionListener() {
470                            
471                            /**
472                             * called when the pairing dialog was closed by the user or if the user pressed the cancel-button.
473                             * In this case the init should fail.
474                             */
475                            @Override
476                            public void actionPerformed(ActionEvent ae) {
477                                log.debug("pairingDialog cancelled: {}", ae);
478                                curPairingResult = PairingResult.UNPAIRED; //just to be sure...
479                                continueLock.countDown();
480                            }
481                        });
482                        // Show the dialog.
483                        // show() will not wait for user interaction or timeout, but will return immediately
484                        pairingDialog.show();
485                    }
486                }       
487            }
488            
489            /**
490             * The remote side has logged in. The connection is now fully usable.
491             */
492            @Override
493            public void logonReceived(int localNodeAddr, long uniqueId) {
494                log.debug("+++++ logonReceived - localNodeAddr: {}, uniqueId: {}",
495                    localNodeAddr, ByteUtils.convertUniqueIdToString(ByteUtils.convertLongToUniqueId(uniqueId)));
496                connectionIsReady = true;
497                //deviceUniqueId = uniqueId;
498                if (bidib.getRootNode().getMasterNode().isDetached()) {
499                    bidib.getRootNode().getMasterNode().setDetached(false);
500                }
501                nodeInitializer.connectionInit();
502                continueLock.countDown();
503                startLocalPing();
504                fireConnectionChanged("logon");
505            }
506
507            /**
508             * The remote side has logged off. Either as an result of a detach from our side or directly
509             * from the remote side.
510             * The nodes of this connection will be removed, but the connection will be not be closed, so
511             * it can be re-established later by a logonReceived event.
512             */
513            @Override
514            public void logoffReceived(long uniqueId) {
515                log.debug("----- logoffReceived - uniqueId: {}",
516                        ByteUtils.convertUniqueIdToString(ByteUtils.convertLongToUniqueId(uniqueId)));
517                connectionIsReady = false;
518                //deviceUniqueId = uniqueId;
519                connectionLost();
520                continueLock.countDown();
521                fireConnectionChanged("logoff");
522            }
523        };
524        
525        String portName = portController.getRealPortName();
526        log.info("Open BiDiB connection on \"{}\"", portName);
527
528        try {
529            if (!bidib.isOpened()) {
530                bidib.setResponseTimeout(1600);
531                bidib.open(portName, connectionListener, nodeListeners, messageListeners, transferListeners, context);
532            }
533            else {
534                // if we get here, we assume that the adapter has already opened the port just for scanning the device
535                // and that NO listeners have been registered. So just add them now.
536                // If one day we start to really use the listeners we would have to check if this is o.k.
537                portController.registerAllListeners(connectionListener, nodeListeners, messageListeners, transferListeners);
538            }
539            
540            
541            log.debug("the connection is now opened: {}", bidib.isOpened());
542            
543            if (isAsyncInit) {
544                // for async connections (netBiDiB) we have to wait here until the connection is paired
545                // the server has logged in and the nodes have been initialized.
546                
547                // get the pairing timeout and then wait a bit longer to be sure...
548                final NetBidibLinkData clientLinkData =
549                    context.get(Context.NET_BIDIB_CLIENT_LINK_DATA, NetBidibLinkData.class, null);
550                long timeout = clientLinkData.getRequestedPairingTimeout();
551
552                boolean success = continueLock.await(timeout + 10, TimeUnit.SECONDS);
553                if (pairingDialog != null) {
554                    pairingDialog.dispose(); //just to be sure the dialog disappears...
555                    pairingDialog = null;
556                }
557                if (!success  ||  !curPairingResult.equals(PairingResult.PAIRED)) {
558                    // an unpaired connection will be closed.
559                    if (bidib.isOpened()) {
560                        if (!success) {
561                            log.warn("pairing or login timed out! Root node cannot be initialized - closing connection");
562                        }
563                        else {
564                            log.warn("pairing or login failed! Root node cannot be initialized - closing connection");
565                        }
566                        bidib.close();
567                    }
568                    return null;
569                }
570            }
571            else {
572                // even in non-async mode (all but netBiDiB) the node init is done in another thread
573                // so we wait here for the node init has been completed.
574                boolean success = continueLock.await(30, TimeUnit.SECONDS);
575                if (!success) {
576                    log.warn("node init timeout!");
577                }
578            }
579            
580            if (!bidib.isOpened()  ||  !connectionIsReady) {
581                log.info("connection is not ready - not initialized.");
582                return null;
583            }
584            
585            // The connection is ready now.
586            // The nodes have already been initialized here from the connectionListener events.
587            
588            log.info("registering shutdown task");
589            ShutDownTask shutDownTask = new AbstractShutDownTask("BiDiB Shutdown Task") {
590                @Override
591                public void run() {
592                    log.info("Shutdown Task - Terminate {}", getUserName());
593                    terminate();
594                    log.info("Shutdown task finished {}", getUserName());
595                }
596            };
597            InstanceManager.getDefault(ShutDownManager.class).register(shutDownTask);
598            
599            return context;
600
601        }
602        catch (PortNotFoundException ex) {
603            log.error("The provided port was not found: {}. Verify that the BiDiB device is connected.", ex.getMessage());
604        }
605        catch (Exception ex) {
606            log.error("Execute command failed: ", ex); // NOSONAR
607        }
608        return null;
609    }
610    
611//    public Long getUniqueId() {
612//        return deviceUniqueId;
613//    }
614        
615    /**
616     * Check if the connection is ready to communicate.
617     * For netBiDiB this is the case only if the pairing was successful and
618     * the server has logged in.
619     * For all other connections the connection is ready if it has been
620     * successfully opened.
621     * 
622     * @return true if the connection is ready
623     */
624    public boolean isConnectionReady() {
625        return connectionIsReady;
626    }
627    
628    /**
629     * Check of the connection is netBiDiB.
630     * 
631     * @return true if this connection is netBiDiB
632     */
633    public boolean isNetBiDiB() {
634        return isNetBiDiB;
635    }
636    
637    /**
638     * Set the connection to lost state.
639     * All nodes will be removed and the components will be invalidated
640     */
641    public void connectionLost() {
642        connectionIsReady = false;
643        nodeInitializer.connectionLost();
644    }
645    
646    // Methods used by BiDiB only
647
648    /**
649     * Check if the connection is detached i.e. it is opened, paired
650     * but the logon has been rejected.
651     * 
652     * @return true if detached
653     */
654    public boolean isDetached() {
655        if (bidib != null) {
656            try {
657                RootNode rootNode = bidib.getRootNode();
658                return rootNode.getMasterNode().isDetached();
659            }
660            catch (Exception e) {
661                log.trace("cannot determine detached flag: {}", e.toString());
662            }
663        }
664        return false;
665    }
666    
667    /**
668     * Set or remove the detached state.
669     * If logoff is requested (detach), a logon reject is sent to the device
670     * and all nodes will be notified that they are no more reachable (lost node).
671     * The connection remains active, but other clients may request a logon from
672     * the connected device.
673     * 
674     * If a logon is requested (attach), the connected device is asked for a new logon.
675     * When the logon is received, the nodes will be re-initialized by the traffic
676     * controller.
677     * 
678     * @param logon - true for logon (attach), false for logoff (detach)
679     */
680    public void setLogon(boolean logon) {
681        Long uid;
682        try {
683            // get the JMRI uid
684            final NetBidibLinkData providedClientLinkData =
685                portController.getContext().get(Context.NET_BIDIB_CLIENT_LINK_DATA, NetBidibLinkData.class, null);
686            uid = providedClientLinkData.getUniqueId() & 0xFFFFFFFFFFL;
687        }
688        catch (Exception e) {
689            log.error("cannot determine our own Unique ID: {}", e.toString());
690            return;
691        }
692        if (logon) {
693            bidib.attach(uid);
694            // after successful logon the node(s) will be newly initialized by the traffic controller
695        }
696        else {
697            // since there won't be any event fired upon a detached connection, we have to do it ourself
698            connectionLost();
699            bidib.detach(uid);
700            bidib.getRootNode().getMasterNode().setDetached(true);
701        }
702    }
703    
704    /**
705     * Add/Remove an ActionListener to be called when the connection has changed.
706     * 
707     * @param l - an Object implementing the ActionListener interface
708     */
709    public void addConnectionChangedListener(ActionListener l) {
710        synchronized (connectionChangedListeners) {
711            connectionChangedListeners.add(l);
712        }
713    }
714    
715    public void removeConnectionChangedListener(ActionListener l) {
716        synchronized (connectionChangedListeners) {
717            connectionChangedListeners.remove(l);
718        }
719    }
720    
721    private void fireConnectionChanged(String cmd) {
722
723        final Collection<ActionListener> safeListeners;
724        synchronized (connectionChangedListeners) {
725            safeListeners = CollectionUtils.newArrayList(connectionChangedListeners);
726        }
727        for (ActionListener l : safeListeners) {
728            l.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, cmd));
729        }
730    }
731
732    /**
733     * Start jbidibc built-in local ping as a watchdog of the connection.
734     * If there is no response from the device, the connection will be closed.
735     */
736    public void startLocalPing() {
737        log.debug("Start the netBiDiB local ping worker on the rootNode.");
738        try {
739
740            if (bidib != null) {
741                bidib.getRootNode().setLocalPingEnabled(true);
742
743                bidib.getRootNode().startLocalPingWorker(true, localPingResult -> {
744                    if (localPingResult.equals(Boolean.FALSE)) {
745                        log.warn("The local pong was not received! Close connection");
746                        delayedCloseTimer.start();
747                    }
748                });
749            }
750        }
751        catch (Exception ex) {
752            log.warn("Start the local ping worker failed.", ex);
753        }
754    }
755 
756    // End Methods used by BiDiB only
757    
758
759    Node debugSavedNode;
760    public void TEST(boolean a) {///////////////////DEBUG
761        log.debug("TEST {}", a);
762        String nodename = "XXXX";
763        Node node = a ? debugSavedNode : getNodeByUserName(nodename);
764        if (node != null) {
765            if (a) {
766                nodeInitializer.nodeNew(node);
767                //nodeInitializer.nodeLost(node);
768            }
769            else {
770                debugSavedNode = node;
771                nodeInitializer.nodeLost(node);
772            }
773        }
774    }
775    
776    /**
777     * Get Bidib Interface
778     * 
779     * @return Bidib Interface
780     */
781    public BidibInterface getBidib() {
782        return bidib;
783    }
784
785// convenience methods for node handling
786    
787    /**
788     * Get the list of nodes found
789     * 
790     * @return list of nodes
791     */
792    public Map<Long, Node> getNodeList() {
793        return nodes;
794    }
795    
796    /**
797     * Get node by unique id from nodelist
798     * 
799     * @param uniqueId search for this
800     * @return node
801     */
802    public Node getNodeByUniqueID(long uniqueId) {
803        return nodes.get(uniqueId);
804    }
805    
806    /**
807     * Get node by node address from nodelist
808     * 
809     * @param addr input to search
810     * @return node 
811     */
812    public Node getNodeByAddr(byte[] addr) {
813        for(Map.Entry<Long, Node> entry : nodes.entrySet()) {
814            Node node = entry.getValue();
815            if (NodeUtils.isAddressEqual(node.getAddr(), addr)) {
816                return node;
817            }
818        }
819        return null;
820    }
821    
822    /**
823     * Get node by node username from nodelist
824     * 
825     * @param userName input to search
826     * @return node 
827     */
828    public Node getNodeByUserName(String userName) {
829        //log.debug("getNodeByUserName: [{}]", userName);
830        for(Map.Entry<Long, Node> entry : nodes.entrySet()) {
831            Node node = entry.getValue();
832            //log.debug("  node: {}, usename: {}", node, node.getStoredString(StringData.INDEX_USERNAME));
833            if (node.getStoredString(StringData.INDEX_USERNAME).equals(userName)) {
834                return node;
835            }
836        }
837        return null;
838    }
839    
840    /**
841     * Get root node from nodelist
842     * 
843     * @return node 
844     */
845    public Node getRootNode() {
846        byte[] addr = {0};
847        return getNodeByAddr(addr);
848    }
849    
850    /**
851     * A node suitable as a global programmer must be
852     * 
853     * - a command station,
854     * - must support service mode programming and
855     * - must be a booster.
856     * - for other nodes than the global command station the local DCC generator
857     *   must be switched on (MSG_BOOST_STAT returns this as "control")
858     * 
859     * @param node to check
860     * 
861     * @return true if the node is suitable as a global progreammer
862     */
863    public boolean isGlobalProgrammerNode(Node node) {
864       
865        if (NodeUtils.hasCommandStationFunctions(node.getUniqueId())) {
866//                if (node.equals(getRootNode())) { //DEBUG
867//                    log.trace("---is root node: {}", node);
868//                    continue;//TEST: pretend that the root does not support service mode.
869//                }
870            if (NodeUtils.hasCommandStationProgrammingFunctions(node.getUniqueId()) 
871                        &&  NodeUtils.hasBoosterFunctions(node.getUniqueId())) {
872                log.trace("node supports command station, programming and booster functions: {}", node);
873                if (node == getFirstCommandStationNode()  ||  hasLocalDccEnabled(node)) {
874                    return true;
875                }
876            }
877        }
878        return false;
879    }
880
881    /**
882     * Get the most probably only global programmer node (aka service mode node, used for prgramming track - PT, not for POM)
883     * If there are more than one suitable node, get the last one (reverse nodes list search).
884     * In this case the command station (probably the root node and first entry in the list) should
885     * probably not be used as a global programmer and the other have been added just for that purpose.
886     * TODO: the user should select the global programmer node if there multiple nodes suitable
887     * as a global programmer.
888     * 
889     * 
890     * @return programmer node or null if none available
891     */
892    public Node getFirstGlobalProgrammerNode() {
893        //log.debug("find  global programmer node");
894        for(Map.Entry<Long, Node> entry : nodes.descendingMap().entrySet()) {
895            Node node = entry.getValue();
896            //log.trace("node entry: {}", node);
897            if (isGlobalProgrammerNode(node)) {
898                synchronized (progTimer) {
899                    log.debug("global programmer found: {}", node);
900                    progTimer.restart();
901                    return node;
902                }
903            }
904        }
905        return null;
906    }
907    
908    /**
909     * Set the  global programmer node to use.
910     * 
911     * @param node to be used as global programmer node or null to remove the currentGlobalProgrammerNode
912     * 
913     * @return true if node is a suitable global programmer node, currentGlobalProgrammerNode is set to that node. false if it is not.
914     */
915    public boolean setCurrentGlobalProgrammerNode(Node node) {
916        if (node == null  ||  isGlobalProgrammerNode(node)) {
917            currentGlobalProgrammerNode = node;
918            return true;
919        }
920        return false;
921    }
922    
923    /**
924     * Get the cached global programmer node. If there is no, try to find a suitable node.
925     * Note that the global programmer node may dynamically change by user settings.
926     * Be sure to update or invalidate currentGlobalProgrammerNode.
927     *
928     * @return the current global programmer node or null if none available.
929     */
930    public Node getCurrentGlobalProgrammerNode() {
931        //log.trace("get current global programmer node: {}", currentGlobalProgrammerNode);
932        if (currentGlobalProgrammerNode == null) {
933            currentGlobalProgrammerNode = getFirstGlobalProgrammerNode();
934        }
935        return currentGlobalProgrammerNode;
936    }
937    
938    /**
939     * Ask the node if the local DCC generator is enabled. The state is returned as a
940     * BoosterControl value of LOCAL.
941     * Note that this function is expensive since it gets the information directly
942     * from the node and receiving a MSG_BOOST_STATE message.
943     * 
944     * As far as I know (2023) there is only one device that supports the dynamically DCC generator switching: the Fichtelbahn ReadyBoost
945     * 
946     * @param node to ask
947     * @return true if local DCC generator is enabled, false if the booster is connected to the
948     *         global command station.
949     *
950     */
951    private boolean hasLocalDccEnabled(Node node) {
952        if ((bidib instanceof SimulationBidib)) { // **** this a hack for the simulator since it will never return LOCAL dcc...
953            return true;
954        }
955        boolean hasLocalDCC = false;
956        // check if the node has a local DCC generator
957        BoosterNode bnode = getBidib().getBoosterNode(node);
958        if (bnode != null) {
959            try {
960                BoosterStateData bdata = bnode.queryState(); //send and wait for response
961                log.trace("Booster state data: {}", bdata);
962                if (bdata.getControl() == BoosterControl.LOCAL) {
963                    hasLocalDCC = true; //local DCC generator is enabled
964                }
965            }
966            catch (ProtocolException e) {}
967        }
968        log.debug("node has local DCC enabled: {}, {}", hasLocalDCC, node);
969        return hasLocalDCC;
970    }
971    
972    
973    /**
974     * Get the first and most probably only command station node (also used for Programming on the Main - POM)
975     * A cached value is returned here for performance reasons since the function is called very often.
976     * We don't expect the command station to change.
977     * 
978     * @return command station node
979     */
980    public Node getFirstCommandStationNode() {
981        if (cachedCommandStationNode == null) {
982            for(Map.Entry<Long, Node> entry : nodes.entrySet()) {
983                Node node = entry.getValue();
984                if (NodeUtils.hasCommandStationFunctions(node.getUniqueId())) {
985                    log.trace("node has command station functions: {}", node);
986                    cachedCommandStationNode = node;
987                    break;
988                }
989            }
990        }
991        return cachedCommandStationNode;
992    }
993    
994    /**
995     * Get the first booster node.
996     * There may be more booster nodes, so prefer the command station and then try the others
997     * @return booster node
998     */
999    public Node getFirstBoosterNode() {
1000        Node node = getFirstCommandStationNode();
1001        log.trace("getFirstBoosterNode: CS is {}", node);
1002        if (node != null  &&  NodeUtils.hasBoosterFunctions(node.getUniqueId())) {
1003            // if the command station has a booster, use this one
1004            log.trace("CS node also has booster functions: {}", node);
1005            return node;
1006        }
1007        // if the command station does not have a booster, try to find another node with booster capability
1008        for(Map.Entry<Long, Node> entry : nodes.entrySet()) {
1009            node = entry.getValue();
1010            if (NodeUtils.hasBoosterFunctions(node.getUniqueId())) {
1011                log.trace("node has booster functions: {}", node);
1012                return node;
1013            }
1014        }
1015        return null;
1016    }
1017
1018    /**
1019     * Get the first output node - a node that can control LC ports.
1020     * TODO: the method does not make much sense and its only purpose is to check if we have an output node at all.
1021     * Therefor it should be converted to "hasOutputNode" or similar.
1022     * @return output node
1023     */
1024    public Node getFirstOutputNode() {  
1025        for(Map.Entry<Long, Node> entry : nodes.entrySet()) {
1026            Node node = entry.getValue();
1027            if (NodeUtils.hasAccessoryFunctions(node.getUniqueId())  ||  NodeUtils.hasSwitchFunctions(node.getUniqueId())) {
1028                log.trace("node has output functions (accessories or ports): {}", node);
1029                return node;
1030            }
1031        }
1032        return null;
1033    }
1034    
1035    /**
1036     * Check if we have at least one node capable of Accessory functions
1037     * @return true or false
1038     */
1039    public boolean hasAccessoryNode() {
1040        for(Map.Entry<Long, Node> entry : nodes.entrySet()) {
1041            Node node = entry.getValue();
1042            if (NodeUtils.hasAccessoryFunctions(node.getUniqueId())) {
1043                log.trace("node has accessory functions: {}", node);
1044                return true;
1045            }
1046        }
1047        return false;
1048    }
1049    
1050// convenience methods for feature handling
1051    
1052    /**
1053     * Find a feature for given node.
1054     * 
1055     * @param node selected node
1056     * @param requestedFeatureId as integer
1057     * @return a Feature object or null if the node does not have the feature at all
1058     */
1059    public Feature findNodeFeature(Node node, final int requestedFeatureId) {
1060        return Feature.findFeature(node.getFeatures(), requestedFeatureId);
1061    }
1062    
1063    /**
1064     * Get the feature value of a node.
1065     * 
1066     * @param node selected node
1067     * @param requestedFeatureId feature to get
1068     * @return the feature value as integer or 0 if the node does not have the feature
1069     */
1070    public int getNodeFeature(Node node, final int requestedFeatureId) {
1071        Feature f = Feature.findFeature(node.getFeatures(), requestedFeatureId);
1072        if (f == null) {
1073            return 0;
1074        }
1075        else {
1076            return f.getValue();
1077        }
1078    }
1079    
1080    // this is here only as a workaround until PortModelEnum.getPortModel eventually will support flat_extended itself
1081    public PortModelEnum getPortModel(final Node node) {
1082        if (PortModelEnum.getPortModel(node) == PortModelEnum.type) {
1083            return PortModelEnum.type;
1084        }
1085        else {
1086            if (node.getPortFlatModel() >= 256) {
1087                return PortModelEnum.flat_extended;
1088            }
1089            else {
1090                return PortModelEnum.flat;
1091            }
1092        }
1093    }
1094
1095    // convenience methods to handle Message Listeners
1096    
1097    /**
1098     * Add a message Listener to the connection
1099     * @param messageListener to be added
1100     */
1101    public void addMessageListener(MessageListener messageListener) {
1102        if (bidib != null) {
1103            log.trace("addMessageListener called!");
1104            BidibMessageProcessor rcv = bidib.getBidibMessageProcessor();
1105            if (rcv != null) {
1106                rcv.addMessageListener(messageListener);
1107            }
1108        }
1109    }
1110
1111    /**
1112     * Remove a message Listener from the connection
1113     * @param messageListener to be removed
1114     */
1115    public void removeMessageListener(MessageListener messageListener) {
1116        if (bidib != null) {
1117            log.trace("removeMessageListener called!");
1118            BidibMessageProcessor rcv = bidib.getBidibMessageProcessor();
1119            if (rcv != null) {
1120                rcv.removeMessageListener(messageListener);
1121            }
1122        }
1123    }
1124
1125    /**
1126     * Add a raw message Listener to the connection
1127     * @param rawMessageListener to be added
1128     */
1129    public void addRawMessageListener(RawMessageListener rawMessageListener) {
1130        if (bidib != null) {
1131            bidib.addRawMessageListener(rawMessageListener);
1132        }
1133    }
1134
1135    /**
1136     * Remove a raw message Listener from the connection
1137     * @param rawMessageListener to be removed
1138     */
1139    public void removeRawMessageListener(RawMessageListener rawMessageListener) {
1140        if (bidib != null) {
1141            bidib.removeRawMessageListener(rawMessageListener);
1142        }
1143    }
1144
1145    
1146// Config and ConfigX handling
1147// NOTE: All of these methods should be either obsolete to moved to BiDiBOutputMessageHandler
1148    
1149    private int getTypeCount(Node node, LcOutputType type) {
1150        int id;
1151        switch (type) {
1152            case SWITCHPORT:
1153            case SWITCHPAIRPORT:
1154                id = BidibLibrary.FEATURE_CTRL_SWITCH_COUNT;
1155                break;
1156            case LIGHTPORT:
1157                id = BidibLibrary.FEATURE_CTRL_LIGHT_COUNT;
1158                break;
1159            case SERVOPORT:
1160                id = BidibLibrary.FEATURE_CTRL_SERVO_COUNT;
1161                break;
1162            case SOUNDPORT:
1163                id = BidibLibrary.FEATURE_CTRL_SOUND_COUNT;
1164                break;
1165            case MOTORPORT:
1166                id = BidibLibrary.FEATURE_CTRL_MOTOR_COUNT;
1167                break;
1168            case ANALOGPORT:
1169                id = BidibLibrary.FEATURE_CTRL_ANALOGOUT_COUNT;
1170                break;
1171            case BACKLIGHTPORT:
1172                id = BidibLibrary.FEATURE_CTRL_BACKLIGHT_COUNT;
1173                break;
1174            case INPUTPORT:
1175                id = BidibLibrary.FEATURE_CTRL_INPUT_COUNT;
1176                break;
1177            default:
1178                return 0;
1179        }
1180        return getNodeFeature(node, id);
1181    }
1182
1183// semi-synchroneous methods using MSG_LC_CONFIGX_GET_ALL / MSG_LC_CONFIGX_GET (or MSG_LC_CONFIG_GET for older nodes) - use for init only
1184// semi-synchroneous means, that this method waits until all data has been received,
1185// but the data itself is delivered through the MessageListener to each registered components (e.g. BiDiBLight)
1186// Therefor this method must only be called after all component managers have initialized all components and thus their message listeners
1187
1188    /**
1189     * Request CONFIGX from all ports on all nodes of this connection.
1190     * Returns after all data has been received.
1191     * Received data is delivered to registered Message Listeners.
1192     */
1193    public void allPortConfigX() {
1194        log.debug("{}: get alle LC ConfigX", getUserName());
1195        for(Map.Entry<Long, Node> entry : nodes.entrySet()) {
1196            Node node = entry.getValue();
1197            if (NodeUtils.hasSwitchFunctions(node.getUniqueId())) {
1198                getAllPortConfigX(node, null);
1199            }
1200        }
1201    }
1202    
1203    /**
1204     * Request CONFIGX from all ports on a given node and possibly only for a given type.
1205     * 
1206     * @param node requested node
1207     * @param type - if null, request all port types
1208     * @return Note: always returns null, since data is not collected synchroneously but delivered to registered Message Listeners.
1209     */
1210    public List<LcConfigX> getAllPortConfigX(Node node, LcOutputType type) {
1211        // get all ports of a type or really all ports if type = null or flat addressing is enabled
1212        List<LcConfigX> portConfigXList = null;
1213        int numPorts;
1214        try {
1215            if (node.getProtocolVersion().isHigherThan(ProtocolVersion.VERSION_0_5)) { //ConfigX is available since V0.6
1216                if (node.isPortFlatModelAvailable()) { //flat addressing
1217                    numPorts = node.getPortFlatModel();
1218                    if (numPorts > 0) {
1219                        bidib.getNode(node).getAllConfigX(getPortModel(node), type, 0, numPorts);
1220                    }
1221                }
1222                else { //type based addressing
1223                    for (LcOutputType t : LcOutputType.values()) {
1224                        if ( (type == null  ||  type == t)  &&  t.hasPortStatus()  &&  t.getType() <= 15) {
1225                            numPorts = getTypeCount(node, t);
1226                            if (numPorts > 0) {
1227                                bidib.getNode(node).getAllConfigX(getPortModel(node), t, 0, numPorts);
1228                            }
1229                        }
1230                    }
1231                }
1232            }
1233            else { //old Config - type based adressing only
1234                for (LcOutputType t : LcOutputType.values()) {
1235                    if ( (type == null  ||  type == t)  &&  t.hasPortStatus()  &&  t.getType() <= 15) {
1236                        numPorts = getTypeCount(node, t);
1237                        if (numPorts > 0) {
1238                            int[] plist = new int[numPorts];
1239                            for (int i = 0; i < numPorts; i++) {
1240                                plist[i] = i;
1241                            }
1242                            bidib.getNode(node).getConfigBulk(PortModelEnum.type, t, plist);
1243                        }
1244                    }
1245                }
1246            }
1247        } catch (ProtocolException e) {
1248            log.error("getAllConfigX message failed:", e);
1249        }
1250        return portConfigXList;
1251    }
1252    
1253    /**
1254     * Request CONFIGX if a given port on a given node and possibly only for a given type.
1255     * 
1256     * @param node requested node
1257     * @param portAddr as an integer
1258     * @param type - if null, request all port types
1259     * @return Note: always returns null, since data is not collected synchroneously but delivered to registered Message Listeners.
1260     */
1261    public LcConfigX getPortConfigX(Node node, int portAddr, LcOutputType type) {
1262        // synchroneous method using MSG_LC_CONFIGX_GET - use for init only
1263        try {
1264            if (node.getProtocolVersion().isHigherThan(ProtocolVersion.VERSION_0_5)) { //ConfigX is available since V0.6
1265                if (node.isPortFlatModelAvailable()) {
1266                    bidib.getNode(node).getConfigXBulk(getPortModel(node), type, 2 /*Window Size*/, portAddr);
1267                }
1268                else {
1269                    for (LcOutputType t : LcOutputType.values()) {
1270                        if ( (type == null  ||  type == t)  &&  t.hasPortStatus()  &&  t.getType() <= 15) {
1271                            bidib.getNode(node).getConfigXBulk(getPortModel(node), t, 2 /*Window Size*/, portAddr);
1272                        }
1273                    }
1274                }
1275            }
1276            else {
1277                for (LcOutputType t : LcOutputType.values()) {
1278                    if ( (type == null  ||  type == t)  &&  t.hasPortStatus()  &&  t.getType() <= 15) {
1279                        bidib.getNode(node).getConfigBulk(PortModelEnum.type, t, portAddr);
1280                    }
1281                }
1282            }
1283        } catch (ProtocolException e) {
1284            log.error("getConfigXBulk message failed", e);
1285        }
1286        return null; ////////TODO remove return value completely
1287    }
1288    
1289    /**
1290     * Convert a CONFIG object to a CONFIGX object.
1291     * This is a convenience method so the JMRI components need only to handle the CONFIGX format
1292     * 
1293     * @param node context node
1294     * @param lcConfig the LcConfig object
1295     * @return a new LcConfigX object
1296     */
1297    public LcConfigX convertConfig2ConfigX(Node node, LcConfig lcConfig) {
1298        Map<Byte, PortConfigValue<?>> portConfigValues = new HashMap<>();
1299        BidibPort bidibPort;
1300        PortModelEnum model;
1301        if (node.isPortFlatModelAvailable()) {
1302            model = PortModelEnum.flat;
1303            byte  portType = lcConfig.getOutputType(model).getType();
1304            int portMap = 1 << portType; //set the corresponding bit only
1305            ReconfigPortConfigValue pcfg = new ReconfigPortConfigValue(portType, portMap);
1306            portConfigValues.put(BidibLibrary.BIDIB_PCFG_RECONFIG, pcfg);
1307        }
1308        else {
1309            model = PortModelEnum.type;
1310        }
1311        bidibPort = BidibPort.prepareBidibPort(model, lcConfig.getOutputType(model), lcConfig.getOutputNumber(model));
1312        // fill portConfigValues from lcConfig
1313        switch (lcConfig.getOutputType(model)) {
1314            case SWITCHPORT:
1315                if (getNodeFeature(node, BidibLibrary.FEATURE_SWITCH_CONFIG_AVAILABLE) > 0) {
1316                    byte ioCtrl = ByteUtils.getLowByte(lcConfig.getValue1()); //BIDIB_PCFG_IO_CTRL - this is not supported for ConfigX any more
1317                    byte ticks = ByteUtils.getLowByte(lcConfig.getValue2()); //BIDIB_PCFG_TICKS
1318                    byte switchControl = (2 << 4) | 2; //tristate
1319                    switch (ioCtrl) {
1320                        case 0: //simple output
1321                            ticks = 0; //disable
1322                            switchControl = (1 << 4); //1 << 4 | 0
1323                            break;
1324                        case 1: //high pulse (same than simple output, but turns off after ticks
1325                            switchControl = (1 << 4); //1 << 4 | 0
1326                            break;
1327                        case 2: //low pulse
1328                            switchControl = 1; // 0 << 4 | 1
1329                            break;
1330                        case 3: //tristate
1331                            ticks = 0;
1332                            break;
1333                        default:
1334                            // same as tristate TODO: Support 4 (pullup) and 5 (pulldown) - port is an input then (??, spec not clear)
1335                            ticks = 0;
1336                            break;
1337                    }
1338                    BytePortConfigValue pcfgTicks = new BytePortConfigValue(ticks);
1339                    BytePortConfigValue pcfgSwitchControl = new BytePortConfigValue(switchControl);
1340                    portConfigValues.put(BidibLibrary.BIDIB_PCFG_TICKS, pcfgTicks);
1341                    portConfigValues.put(BidibLibrary.BIDIB_PCFG_SWITCH_CTRL, pcfgSwitchControl);
1342                }
1343                break;
1344            case LIGHTPORT:
1345                BytePortConfigValue pcfgLevelPortOff = new BytePortConfigValue(ByteUtils.getLowByte(lcConfig.getValue1()));
1346                BytePortConfigValue pcfgLevelPortOn = new BytePortConfigValue(ByteUtils.getLowByte(lcConfig.getValue2()));
1347                BytePortConfigValue pcfgDimmDown = new BytePortConfigValue(ByteUtils.getLowByte(lcConfig.getValue3()));
1348                BytePortConfigValue pcfgDimmUp = new BytePortConfigValue(ByteUtils.getLowByte(lcConfig.getValue4()));
1349                portConfigValues.put(BidibLibrary.BIDIB_PCFG_LEVEL_PORT_OFF, pcfgLevelPortOff);
1350                portConfigValues.put(BidibLibrary.BIDIB_PCFG_LEVEL_PORT_ON, pcfgLevelPortOn);
1351                portConfigValues.put(BidibLibrary.BIDIB_PCFG_DIMM_DOWN, pcfgDimmDown);
1352                portConfigValues.put(BidibLibrary.BIDIB_PCFG_DIMM_UP, pcfgDimmUp);
1353                break;
1354            case SERVOPORT:
1355                BytePortConfigValue pcfgServoAdjL = new BytePortConfigValue(ByteUtils.getLowByte(lcConfig.getValue1()));
1356                BytePortConfigValue pcfgServoAdjH = new BytePortConfigValue(ByteUtils.getLowByte(lcConfig.getValue2()));
1357                BytePortConfigValue pcfgServoSpeed = new BytePortConfigValue(ByteUtils.getLowByte(lcConfig.getValue3()));
1358                portConfigValues.put(BidibLibrary.BIDIB_PCFG_SERVO_ADJ_L, pcfgServoAdjL);
1359                portConfigValues.put(BidibLibrary.BIDIB_PCFG_SERVO_ADJ_H, pcfgServoAdjH);
1360                portConfigValues.put(BidibLibrary.BIDIB_PCFG_SERVO_SPEED, pcfgServoSpeed);
1361                break;
1362            case BACKLIGHTPORT:
1363                BytePortConfigValue pcfgDimmDown2 = new BytePortConfigValue(ByteUtils.getLowByte(lcConfig.getValue1()));
1364                BytePortConfigValue pcfgDimmUp2 = new BytePortConfigValue(ByteUtils.getLowByte(lcConfig.getValue2()));
1365                BytePortConfigValue pcfgOutputMap = new BytePortConfigValue(ByteUtils.getLowByte(lcConfig.getValue3()));
1366                portConfigValues.put(BidibLibrary.BIDIB_PCFG_DIMM_DOWN, pcfgDimmDown2);
1367                portConfigValues.put(BidibLibrary.BIDIB_PCFG_DIMM_UP, pcfgDimmUp2);
1368                portConfigValues.put(BidibLibrary.BIDIB_PCFG_OUTPUT_MAP, pcfgOutputMap);
1369                break;
1370            case INPUTPORT:
1371                // not really specified, but seems logical...
1372                byte ioCtrl = ByteUtils.getLowByte(lcConfig.getValue1()); //BIDIB_PCFG_IO_CTRL - this is not supported for ConfigX any more
1373                byte inputControl = 1; //active HIGH
1374                switch (ioCtrl) {
1375                    case 4: //pullup
1376                        inputControl = 2; //active LOW + Pullup
1377                        break;
1378                    case 5: //pulldown
1379                        inputControl = 3; //active HIGH + Pulldown
1380                        break;
1381                    default:
1382                        // do nothing, leave inputControl at 1
1383                        break;
1384                }
1385                BytePortConfigValue pcfgInputControl = new BytePortConfigValue(inputControl);
1386                portConfigValues.put(BidibLibrary.BIDIB_PCFG_INPUT_CTRL, pcfgInputControl);
1387                break;
1388            default:
1389                break;
1390        }
1391        LcConfigX configX = new LcConfigX(bidibPort, portConfigValues);
1392        return configX;
1393    }
1394    
1395    // Asynchronous methods to request the status of all BiDiB ports
1396    // For this reason there is no need to query  FEATURE_CTRL_PORT_QUERY_AVAILABLE, since without this feature there would simply be no answer for the port.
1397    
1398    /**
1399     * Request LC_STAT from all ports on all nodes of this connection.
1400     * Returns immediately.
1401     * Received data is delivered to registered Message Listeners.
1402     */
1403    public void allPortLcStat() {
1404        log.debug("{}: get alle LC stat", getUserName());
1405        for(Map.Entry<Long, Node> entry : nodes.entrySet()) {
1406            Node node = entry.getValue();
1407            if (NodeUtils.hasSwitchFunctions(node.getUniqueId())) {
1408                portLcStat(node, 0xFFFF);
1409            }
1410        }
1411    }
1412    
1413    /**
1414     * Request LC_STAT from all ports on a given node.
1415     * Returns immediately.
1416     * Received data is delivered to registered Message Listeners.
1417     * The differences for the addressing model an the old LC_STAT handling are hidden to the caller.
1418     * 
1419     * @param node selected node
1420     * @param typemask a 16 bit type mask where each bit represents a type, Bit0 is SWITCHPORT, Bit1 is LIGHTPORT and so on. Bit 15 is INPUTPORT.
1421     *        Return LC_STAT only for ports which are selected on the type mask (the correspondend bit is set).
1422     */
1423    public void portLcStat(Node node, int typemask) {
1424        if (NodeUtils.hasSwitchFunctions(node.getUniqueId())) {
1425            BidibRequestFactory rf = getBidib().getRootNode().getRequestFactory();
1426            if (node.getProtocolVersion().isHigherThan(ProtocolVersion.VERSION_0_6)) {
1427                // fast bulk query of all ports (new in bidib protocol version 0.7)
1428//                int numPorts;
1429//                if (node.isPortFlatModelAvailable()) {
1430//                    numPorts = node.getPortFlatModel();
1431//                    if (numPorts > 0) {
1432//                        //BidibCommandMessage m = (BidibCommandMessage)rf.createPortQueryAll(typemask, 0, numPorts);
1433//                        BidibCommandMessage m = (BidibCommandMessage)rf.createPortQueryAll(typemask, 0, 0xFFFF);
1434//                        sendBiDiBMessage(m, node);
1435//                    }
1436//                }
1437//                else { //type based addressing
1438//                    for (LcOutputType t : LcOutputType.values()) {
1439//                        int tmask = 1 << t.getType();
1440//                        if ( ((tmask & typemask) != 0)  &&  t.hasPortStatus()  &&  t.getType() <= 15) {
1441//                            numPorts = getTypeCount(node, t);
1442//                            if (numPorts > 0) {
1443//                                // its not clear how PORT_QUERY_ALL is defined for type based addressing
1444//                                // so we try the strictest way
1445//                                BidibPort fromPort = BidibPort.prepareBidibPort(PortModelEnum.type, t, 0);
1446//                                BidibPort toPort = BidibPort.prepareBidibPort(PortModelEnum.type, t, numPorts);
1447//                                int from = ByteUtils.getWORD(fromPort.getValues());
1448//                                int to = ByteUtils.getWORD(toPort.getValues());
1449//                                BidibCommandMessage m = (BidibCommandMessage)rf.createPortQueryAll(tmask, from, to);
1450//                                //BidibCommandMessage m = (BidibCommandMessage)rf.createPortQueryAll(typemask, 0, 0xFFFF);
1451//                                sendBiDiBMessage(m, node);
1452//                                //break;
1453//                            }
1454//                        }
1455//                    }
1456//                }
1457                // just query everything
1458                BidibCommandMessage m = rf.createPortQueryAll(typemask, 0, 0xFFFF);
1459                sendBiDiBMessage(m, node);
1460            }
1461            else {
1462                // old protocol versions (<= 0.6 - request every single port
1463                int numPorts;
1464                if (node.isPortFlatModelAvailable()) {
1465                    // since flat addressing is only available since version 0.6, this is only possible with exactly version 0.6
1466                    numPorts = node.getPortFlatModel();
1467                    for (int addr = 0; addr < numPorts; addr++) {
1468                        BidibCommandMessage m = rf.createLcPortQuery(getPortModel(node), null, addr);
1469                        sendBiDiBMessage(m, node);
1470                    }
1471                }
1472                else { //type based adressing
1473                    for (LcOutputType t : LcOutputType.values()) {
1474                        int tmask = 1 << t.getType();
1475                        if ( ((tmask & typemask) != 0)  &&  t.hasPortStatus()  &&  t.getType() <= 7) { //outputs only - for old protocol version
1476                            numPorts = getTypeCount(node, t);
1477                            for (int addr = 0; addr < numPorts; addr++) {
1478                                BidibCommandMessage m = rf.createLcPortQuery(getPortModel(node), t, addr);
1479                                sendBiDiBMessage(m, node);
1480                            }
1481                        }
1482                    }
1483                    // inputs have a separate message type in old versions: MSG_LC_KEY_QUERY
1484                    LcOutputType t = LcOutputType.INPUTPORT;
1485                    int tmask = 1 << t.getType();
1486                    if ( ((tmask & typemask) != 0) ) {
1487                        numPorts = getTypeCount(node, t);
1488                        for (int addr = 0; addr < numPorts; addr++) {
1489                            BidibCommandMessage m = rf.createLcKey(addr);
1490                            sendBiDiBMessage(m, node);
1491                        }
1492                    }
1493                }
1494            }
1495        }        
1496    }
1497
1498// Asynchronous methods to request the status of all BiDiB feedback channels
1499
1500    public void allAccessoryState() {
1501        log.debug("{}: get alle accessories", getUserName());
1502        for(Map.Entry<Long, Node> entry : nodes.entrySet()) {
1503            Node node = entry.getValue();
1504            if (NodeUtils.hasAccessoryFunctions(node.getUniqueId())) {
1505                accessoryState(node);
1506            }
1507        }
1508    }
1509    
1510    public void accessoryState(Node node) {
1511        int accSize = getNodeFeature(node, BidibLibrary.FEATURE_ACCESSORY_COUNT);
1512        if (NodeUtils.hasAccessoryFunctions(node.getUniqueId())  &&  accSize > 0 ) {
1513            log.info("Requesting accessory status on node {}", node);
1514            for (int addr = 0; addr < accSize; addr++) {
1515                sendBiDiBMessage(new AccessoryGetMessage(addr), node);
1516            }
1517        }
1518    }
1519    
1520    /**
1521     * Request Feedback Status (called BM status in BiDiB - BM (Belegtmelder) is german for "feedback") from all ports on all nodes of this connection.
1522     * Returns immediately.
1523     * Received data is delivered to registered Message Listeners.
1524     */
1525    public void allFeedback() {
1526        //log.debug("{}: get alle feedback", getUserName());
1527        for(Map.Entry<Long, Node> entry : nodes.entrySet()) {
1528            Node node = entry.getValue();
1529            if (NodeUtils.hasFeedbackFunctions(node.getUniqueId())) {
1530                feedback(node);
1531            }
1532        }
1533    }
1534    
1535    /**
1536     * Request Feedback Status (called BM status in BiDiB - BM (Belegtmelder) is german for "feedback") from all ports on a given node.
1537     * Returns immediately.
1538     * Received data is delivered to registered Message Listeners.
1539     * 
1540     * @param node selected node
1541     */
1542    public void feedback(Node node) {
1543        int bmSize = getNodeFeature(node, BidibLibrary.FEATURE_BM_SIZE);
1544        if (NodeUtils.hasFeedbackFunctions(node.getUniqueId())  &&  bmSize > 0 ) {
1545            log.info("Requesting feedback status on node {}", node);
1546            sendBiDiBMessage(new FeedbackGetRangeMessage(0, bmSize), node);
1547        }
1548    }
1549    
1550// End of obsolete Methods
1551    
1552// BiDiB Message handling
1553    
1554    /**
1555     * Forward a preformatted BiDiBMessage to the actual interface.
1556     *
1557     * @param m Message to send;
1558     * @param node BiDiB node to send the message to
1559     */
1560    public void sendBiDiBMessage(BidibCommandMessage m, Node node) {
1561        if (node == null) {
1562            log.error("node is undefined! - can't send message.");
1563            return;
1564        }
1565        log.trace("sendBiDiBMessage: {} on node {}", m, node);
1566        // be sure that the node is in correct mode
1567        if (checkProgMode((m.getType() == BidibLibrary.MSG_CS_PROG), node) >= 0) {
1568            try {
1569                log.trace("  bidib node: {}", getBidib().getNode(node));
1570                BidibNodeAccessor.sendNoWait(getBidib().getNode(node), m);
1571            } catch (ProtocolException e) {
1572                log.error("sending BiDiB message failed", e);
1573            }
1574        }
1575        else {
1576            log.error("switching to or from PROG mode (global programmer) failed!");
1577        }
1578    }
1579    
1580    /**
1581     * Check if the command station is in the requested state (Normal, PT)
1582     * If the command station is not in the requested state, a message is sent to BiDiB to switch to the requested state.
1583     * 
1584     * @param needProgMode true if we request the command station to be in programming state, false if normal state is requested
1585     * @param node selected node
1586     * @return 0 if nothing to do, 1 if state has been changed, -1 on error
1587     */
1588    public synchronized int checkProgMode(boolean needProgMode, Node node) {
1589        log.trace("checkProgMode: needProgMode: {}, node: {}", needProgMode, node);
1590        int hasChanged = 0;
1591        CommandStationState neededMode = needProgMode ? CommandStationState.PROG : mSavedMode;
1592        if (needProgMode != mIsProgMode.get()) {
1593            Node progNode = getCurrentGlobalProgrammerNode();
1594            if (node == progNode) {
1595                Node csNode = getFirstCommandStationNode();
1596
1597                log.debug("use global programmer node: {}", progNode);
1598                CommandStationNode progCsNode = getBidib().getCommandStationNode(progNode); //check if the programmer node also a command station - should have been tested before anyway
1599                if (progCsNode == null) { //just in case...
1600                    currentGlobalProgrammerNode = null;
1601                    hasChanged = -1;
1602                }
1603                else {
1604                    log.debug("change command station mode to PROG? {}", needProgMode);
1605                    if (needProgMode) {
1606                        if (node == csNode) {
1607                            // if we have to switch to prog mode, disable watchdog timer - but only, if we switch the command station
1608                            setWatchdogTimer(false);
1609                        }
1610                    }
1611                    try {
1612                        CommandStationState CurrentMode = progCsNode.setState(neededMode); //send and wait for response
1613                        synchronized (mIsProgMode) {
1614                            if (!needProgMode) {
1615                                mSavedMode = CurrentMode;
1616                            }
1617                            mIsProgMode.set(needProgMode);
1618                        }
1619                        hasChanged = 1;
1620                    }
1621                    catch (ProtocolException e) {
1622                        log.error("sending MSG_CS_STATE message failed", e);
1623                        currentGlobalProgrammerNode = null;
1624                        hasChanged = -1;
1625                    }
1626                    log.trace("new saved mode: {}, is ProgMode: {}", mSavedMode, mIsProgMode);
1627                }
1628            }
1629        }
1630        if (!mIsProgMode.get()) {
1631            synchronized (progTimer) {
1632                if (progTimer.isRunning()) {
1633                    progTimer.stop();
1634                    log.trace("progTimer stopped.");
1635                }
1636            }
1637            setCurrentGlobalProgrammerNode(null); //invalidate programmer node so it must be evaluated again the next time
1638        }
1639
1640        return hasChanged;
1641    }
1642        
1643    private void progTimeout() {
1644        log.trace("timeout - stop global programmer PROG mode - reset to {}", mSavedMode);
1645        checkProgMode(false, getCurrentGlobalProgrammerNode());
1646    }
1647
1648    
1649// BiDiB Watchdog
1650    
1651    private class WatchdogTimerTask extends java.util.TimerTask {
1652        @Override
1653        public void run () {
1654            // If the timer times out, send MSG_CS_STATE_ON message
1655            synchronized (watchdogStatus) {
1656                if (watchdogStatus.get()) { //if still enabled
1657                    sendBiDiBMessage(new CommandStationSetStateMessage(CommandStationState.GO), getFirstCommandStationNode());
1658                }
1659            }
1660        }   
1661    }
1662
1663    public final void setWatchdogTimer(boolean state) {
1664        synchronized (watchdogStatus) {
1665            Node csnode = getFirstCommandStationNode();
1666            long timeout = 0;
1667            log.trace("setWatchdogTimer {} on node {}", state, csnode);
1668            if (csnode != null) {
1669                timeout = getNodeFeature(csnode, BidibLibrary.FEATURE_GEN_WATCHDOG) * 100L; //value in milliseconds
1670                log.trace("FEATURE_GEN_WATCHDOG in ms: {}", timeout);
1671                if (timeout < 2000) {
1672                    timeout = timeout / 2; //half the devices watchdog timeout value for small values
1673                }
1674                else {
1675                    timeout = timeout - 1000; //one second less the devices watchdog timeout value for larger values
1676                }
1677            }
1678            if (timeout > 0  &&  state) {
1679                log.debug("set watchdog TRUE, timeout: {} ms", timeout);
1680                watchdogStatus.set(true);
1681                if (watchdogTimer != null) {
1682                    watchdogTimer.cancel();
1683                }
1684                watchdogTimer = new WatchdogTimerTask(); // Timer used to periodically MSG_CS_STATE_ON
1685                jmri.util.TimerUtil.schedule(watchdogTimer, timeout, timeout);
1686            }
1687            else {
1688                log.debug("set watchdog FALSE, requested state: {}, timeout", state);
1689                watchdogStatus.set(false);
1690                if (watchdogTimer != null) {
1691                    watchdogTimer.cancel();
1692                }
1693                watchdogTimer = null;
1694            }
1695        }
1696    }
1697    
1698// local ping (netBiDiB devices)
1699
1700    private void localPingTimeout() {
1701        log.trace("send local ping");
1702        if (connectionIsReady) {
1703            sendBiDiBMessage(new LocalPingMessage(), getFirstCommandStationNode());
1704        }
1705        else {
1706            localPingTimer.stop();
1707        }
1708    }
1709
1710
1711    /**
1712     * Reference to the system connection memo.
1713     */
1714    BiDiBSystemConnectionMemo mMemo = null;
1715
1716    /**
1717     * Get access to the system connection memo associated with this traffic
1718     * controller.
1719     *
1720     * @return associated systemConnectionMemo object
1721     */
1722    public BiDiBSystemConnectionMemo getSystemConnectionMemo() {
1723        return (mMemo);
1724    }
1725
1726    /**
1727     * Set the system connection memo associated with this traffic controller.
1728     *
1729     * @param m associated systemConnectionMemo object
1730     */
1731    public void setSystemConnectionMemo(BiDiBSystemConnectionMemo m) {
1732        mMemo = m;
1733    }
1734
1735// Command Station interface
1736
1737    /**
1738     * {@inheritDoc}
1739     */
1740    @Override
1741    public String getSystemPrefix() {
1742        if (mMemo != null) {
1743            return mMemo.getSystemPrefix();
1744        }
1745        return "";
1746    }
1747
1748    /**
1749     * {@inheritDoc}
1750     */
1751    @Override
1752    public String getUserName() {
1753        if (mMemo != null) {
1754            return mMemo.getUserName();
1755        }
1756        return "";
1757    }
1758
1759    /**
1760     * {@inheritDoc}
1761     * 
1762     * Not supported! We probably don't need the command station interface at all...
1763     * ... besides perhaps consist control or DCC Signal Mast / Head ??
1764     */
1765    @Override
1766    public boolean sendPacket(byte[] packet, int repeats) {
1767        log.debug("sendPacket: {}, prefix: {}", packet, mMemo.getSystemPrefix());
1768        if (packet != null  &&  packet.length >= 4) {
1769            //log.debug("Addr: {}, aspect: {}, repeats: {}", NmraPacket.getAccSignalDecoderPktAddress(packet), packet[2], repeats);
1770            //log.debug("Addr: {}, aspect: {}, repeats: {}", NmraPacket.getAccDecoderPktAddress(packet), packet[2], repeats);
1771            log.debug("Addr: {}, addr type: {}, aspect: {}, repeats: {}", NmraPacket.extractAddressType(packet), NmraPacket.extractAddressNumber(packet), packet[2], repeats);
1772            //throw new UnsupportedOperationException("Not supported yet.");
1773            log.warn("sendPacket is not supported for BiDiB so far");
1774        }
1775        return false;
1776    }
1777
1778// NOT USED for now
1779//    /**
1780//     * Get the Lower byte of a locomotive address from the decimal locomotive
1781//     * address.
1782//     */
1783//    public static int getDCCAddressLow(int address) {
1784//        /* For addresses below 128, we just return the address, otherwise,
1785//         we need to return the upper byte of the address after we add the
1786//         offset 0xC000. The first address used for addresses over 127 is 0xC080*/
1787//        if (address < 128) {
1788//            return (address);
1789//        } else {
1790//            int temp = address + 0xC000;
1791//            temp = temp & 0x00FF;
1792//            return temp;
1793//        }
1794//    }
1795//
1796//    /**
1797//     * Get the Upper byte of a locomotive address from the decimal locomotive
1798//     * address.
1799//     */
1800//    public static int getDCCAddressHigh(int address) {
1801//        /* this isn't actually the high byte, For addresses below 128, we
1802//         just return 0, otherwise, we need to return the upper byte of the
1803//         address after we add the offset 0xC000 The first address used for
1804//         addresses over 127 is 0xC080*/
1805//        if (address < 128) {
1806//            return (0x00);
1807//        } else {
1808//            int temp = address + 0xC000;
1809//            temp = temp & 0xFF00;
1810//            temp = temp / 256;
1811//            return temp;
1812//        }
1813//    }
1814    
1815    
1816// Shutdown function
1817
1818    protected void terminate () {
1819        log.debug("Cleanup starts {}", this);
1820        if (bidib == null  ||  !bidib.isOpened()) {
1821            return;    // no connection established
1822        }
1823        Node node = getCurrentGlobalProgrammerNode();
1824        if (node != null) {
1825            checkProgMode(false, node); //possibly switch to normal mode
1826        }
1827        setWatchdogTimer(false); //stop watchdog
1828        // sending SYS_DISABLE disables all spontaneous messages and thus informs all nodes that the host will probably disappear
1829        try {
1830            log.info("sending sysDisable to {}", getRootNode());
1831            bidib.getRootNode().sysDisable(); //Throws ProtocolException
1832        }
1833        catch (ProtocolException e) {
1834            log.error("unable to disable node", e);
1835        }
1836        
1837        log.debug("Cleanup ends");
1838    }
1839    
1840//    /** NO LONGER USED
1841//     * Internal class to handle traffic controller cleanup. The primary task of
1842//     * this thread is to make sure the DCC system has exited service mode when
1843//     * the program exits.
1844//     */
1845//    static class CleanupHook implements Runnable {
1846//
1847//        BiDiBTrafficController tc;
1848//
1849//        CleanupHook(BiDiBTrafficController tc) {
1850//            this.tc = tc;
1851//        }
1852//
1853//        @Override
1854//        public void run() {
1855//            tc.terminate();
1856//        }
1857//    }
1858//
1859    
1860    private final static Logger log = LoggerFactory.getLogger(BiDiBTrafficController.class);
1861
1862}