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