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