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