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