001package jmri.jmrix.bidib.serialdriver;
002
003import java.io.DataInputStream;
004import java.io.DataOutputStream;
005import java.io.File;
006import java.io.IOException;
007import java.util.Arrays;
008import java.util.List;
009import java.util.ArrayList;
010import java.util.HashMap;
011import java.util.Map;
012import java.util.Collections;
013import java.util.Set;
014import jmri.util.SystemType;
015import jmri.jmrix.bidib.BiDiBSerialPortController;
016import jmri.jmrix.bidib.BiDiBTrafficController;
017import org.bidib.jbidibc.core.BidibFactory;
018import org.bidib.jbidibc.core.BidibInterface;
019import org.bidib.jbidibc.core.MessageListener;
020import org.bidib.jbidibc.core.node.listener.TransferListener;
021import org.bidib.jbidibc.core.NodeListener;
022import org.bidib.jbidibc.messages.exception.PortNotFoundException;
023import org.bidib.jbidibc.messages.exception.PortNotOpenedException;
024import org.bidib.jbidibc.messages.helpers.DefaultContext;
025import org.bidib.jbidibc.core.node.BidibNode;
026import org.bidib.jbidibc.messages.ConnectionListener;
027import org.bidib.jbidibc.messages.helpers.Context;
028import org.bidib.jbidibc.messages.utils.ByteUtils;
029import org.bidib.jbidibc.jserialcomm.JSerialCommSerialBidib;
030//import org.bidib.jbidibc.jserialcomm.PortIdentifierUtils;
031//import org.bidib.jbidibc.purejavacomm.PureJavaCommSerialBidib;
032//import org.bidib.jbidibc.purejavacomm.PortIdentifierUtils;
033import org.slf4j.Logger;
034import org.slf4j.LoggerFactory;
035
036/**
037 * Implements SerialPortAdapter for the BiDiB system.
038 * <p>
039 * This connects an BiDiB device via a serial com port.
040 *
041 * @author Bob Jacobsen Copyright (C) 2001, 2002
042 * @author Eckart Meyer Copyright (C) 2019-2024
043 */
044public class SerialDriverAdapter extends BiDiBSerialPortController {
045
046    private static final boolean useJSerailComm = true;
047    private static final boolean usePurjavacomm = !useJSerailComm;
048    private static final boolean useScm = false;
049    private static final Map<String, Long> connectionRootNodeList = new HashMap<>(); //our static connection list
050    
051    protected String portNameFilter = "";
052    protected Long rootNodeUid;
053    protected boolean useAutoScan = false;
054    
055//    @SuppressWarnings("OverridableMethodCallInConstructor")
056    public SerialDriverAdapter() {
057        //super(new BiDiBSystemConnectionMemo());
058        setManufacturer(jmri.jmrix.bidib.BiDiBConnectionTypeList.BIDIB);
059        configureBaudRate(validSpeeds[0]);
060        
061        if (SystemType.isLinux()) {
062            //portNameFilter = "/dev/ttyUSB*";
063            portNameFilter = "ttyUSB*";
064            //portNameFilter = "/dev/tty*";
065        }
066        //test
067        List<String> portList = getPortIdentifiers();
068        log.info("portList: {}", portList);
069    }
070    
071    /**
072     * Get the filter string for port names to scan when autoScan is on
073     * @return port name filter as a string (wildcard is allowed at the end)
074     */
075    public String getPortNameFilter() {
076        return portNameFilter;
077    }
078    
079    /**
080     * Set the port name filter
081     * @param filter filter string
082     */
083    public void setPortNameFilter(String filter) {
084        portNameFilter = filter;
085    }
086    
087    /**
088     * Get the root node unique ID
089     * @return UID as Long
090     */
091    public Long getRootNodeUid() {
092        return rootNodeUid;
093    }
094    
095    /**
096     * Set the root node unique ID
097     * @param uid Unique ID as Long
098     */
099    public void setRootNodeUid(Long uid) {
100        rootNodeUid = uid;
101    }
102    
103    /**
104     * Get the AutoScan status
105     * @return true of autoScan is on, false if not
106     */
107    public boolean getUseAutoScan() {
108        return useAutoScan;
109    }
110    
111    /**
112     * Set the AutoScan status
113     * @param flag true of ON is requested
114     */
115    public void setUseAutoScan(boolean flag) {
116        useAutoScan = flag;
117    }
118    
119    /**
120     * {@inheritDoc}
121     * 
122     * Get the port name in the format which is used by jbidibc
123     * @return real port name
124     */
125    @Override
126    public String getRealPortName() {
127        return getRealPortName(getCurrentPortName());
128    }
129
130    /**
131     * Get the canonical port name from the underlying operating system.
132     * For a symbolic link, the real path is returned.
133     * 
134     * @param portName human-readable name
135     * @return canonical path
136     */
137    static public String getCanonicalPortName(String portName) {
138        File file = new File(portName);
139        if (file.exists()) {
140            try {
141                portName = file.getCanonicalPath();
142                log.debug("Canonical port name: {}", portName);
143            }
144            catch (IOException ex) {
145            }
146        }
147        return portName;
148    }
149    
150    /**
151     * Static function to get the port name in the format which is used by jbidibc
152     * @param portName displayed port name
153     * @return real port name
154     */
155    static public String getRealPortName(String portName) {
156        if (SystemType.isLinux()) {
157            portName = "/dev/" + portName;
158        }
159        // TODO: MaxOSX. Windows just uses the displayed port name (COMx:)
160        return getCanonicalPortName(portName);
161    }
162    
163    /**
164     * This methods is called from serial connection config and creates the BiDiB object from jbidibc and opens it.
165     * The connectPort method of the traffic controller is called for generic initialisation.
166     * 
167     * @param portName port name from XML
168     * @param appName not used
169     * @return error string to be displayed by JMRI. null of no error
170     */
171    @Override
172    public String openPort(String portName, String appName) {
173        log.debug("openPort called for {}, driver: {}, expected UID: {}", portName, getRealPortName(), ByteUtils.formatHexUniqueId(rootNodeUid));
174        
175        MSG_RAW_LOGGER.debug("RAW> create BiDiB Instance for port {}", getCurrentPortName());
176        //BidibInterface bidib = createSerialBidib();
177        if (useAutoScan) {
178            String err = findPortbyUniqueID(rootNodeUid);//returns known port in "portName" or scan all ports for the requested unique ID of the root node
179            if (err != null) {
180                if (bidib != null) {
181                    bidib.close();
182                    bidib = null;
183                }
184                return err;
185            }
186        }
187        log.debug("port table: {}", connectionRootNodeList);
188        //bidib.close(); //we can leave it open (creating a new one is time consuming!) - tc.connect then will skip the internal open then
189        if (bidib == null) {
190            context = getContext();
191            bidib = createSerialBidib(context);
192        }
193        BiDiBTrafficController tc = new BiDiBTrafficController(bidib);
194        context  = tc.connnectPort(this); //must be done before configuring managers since they may need features from the device
195        log.debug("memo: {}", this.getSystemConnectionMemo());
196        this.getSystemConnectionMemo().setBiDiBTrafficController(tc);
197        if (context != null) {
198            opened = true;
199            Long uid = tc.getRootNode().getUniqueId() & 0x0000ffffffffffL; //mask the classid
200            if (context.get("serial.baudrate") != null) {
201                // Bidib serial controller has Auto-Baud with two baudrates: 115200 and 19200. Bidib specified only those two.
202                // If the controller has already an open connection, the baudrate is not set in context but it should have been configured before when it was opened
203                log.debug("opened with baud rate {}", context.get("serial.baudrate"));
204                configureBaudRateFromNumber(context.get("serial.baudrate").toString());
205            }
206            if (rootNodeUid != null  &&  !uid.equals(rootNodeUid)) {
207                opened = false;
208                connectionRootNodeList.remove(getRealPortName());
209                tc.getBidib().close(); //wrong UID close to make it available for other checks
210                return "Device found on port " + getRealPortName() + "(" + getCurrentPortName() + ") has Unique ID " + ByteUtils.formatHexUniqueId(uid) + ", but should be " + ByteUtils.formatHexUniqueId(rootNodeUid);
211            }
212            connectionRootNodeList.put(getRealPortName(), tc.getRootNode().getUniqueId() & 0x0000ffffffffffL);
213            setRootNodeUid(uid);
214        }
215        else {
216            opened = false;
217            connectionRootNodeList.put(getRealPortName(), null); //this port does not have a BiDiB device connected - remember this
218            return "No device found on port " + getCurrentPortName() + "(" + getCurrentPortName() + ")";
219        }
220        
221        return null; // indicates OK return
222//        return "CANT DO!!"; //DEBUG
223
224    }
225
226    /**
227     * Set up all of the other objects to operate with an BiDiB command station
228     * connected to this port.
229     */
230    @Override
231    public void configure() {
232        log.debug("configure");
233        this.getSystemConnectionMemo().configureManagers();
234    }
235    
236    /**
237     * Create a BiDiB object. jbidibc has support for various serial implementations.
238     * We tested SCM and PUREJAVACOMM. Both worked without problems. Since
239     * JMRI generally uses PUREJAVACOMM, we also use it here
240     * 
241     * @return a BiDiB object from jbidibc
242     */
243    static private BidibInterface createSerialBidib(Context context) {
244        if (useScm) {
245//            return BidibFactory.createBidib(ScmSerialBidib.class.getName());
246        }
247        if (usePurjavacomm) {
248//            return BidibFactory.createBidib(PureJavaCommSerialBidib.class.getName(), context);
249        }
250        if (useJSerailComm) {
251            return BidibFactory.createBidib(JSerialCommSerialBidib.class.getName(), context);
252        }
253        return null;
254    }
255    
256    /**
257     * {@inheritDoc}
258     */
259    @Override
260    public void registerAllListeners(ConnectionListener connectionListener, Set<NodeListener> nodeListeners,
261                Set<MessageListener> messageListeners, Set<TransferListener> transferListeners) {
262        
263        if (useScm) { //NOT SUPPORTED ANY MORE
264//            PureJavaCommSerialBidib b = (ScmSerialBidib)bidib;
265//            b.setConnectionListener(connectionListener);
266//            b.registerListeners(nodeListeners, messageListeners, transferListeners);
267        }
268        if (usePurjavacomm) {
269//            PureJavaCommSerialBidib b = (PureJavaCommSerialBidib)bidib;
270//            b.setConnectionListener(connectionListener);
271//            b.registerListeners(nodeListeners, messageListeners, transferListeners);
272        }
273        if (useJSerailComm) {
274            JSerialCommSerialBidib b = (JSerialCommSerialBidib)bidib;
275            b.setConnectionListener(connectionListener);
276            b.registerListeners(nodeListeners, messageListeners, transferListeners);
277        }
278    }
279    
280    /**
281     * Get a list of available port names
282     * @return list of portnames
283     */
284    /*static - no longer, since we need portNameFilter here */
285    public List<String> getPortIdentifiers() {
286        List<String> ret = null;
287        List<String> list = null;
288//        if (useScm) {
289//            list = new ArrayList<>();
290//            for (String s : ScmPortIdentifierUtils.getPortIdentifiers()) {
291//                list.add(s.replace("/dev/", ""));
292//            }
293//        }
294        if (usePurjavacomm) {
295            //list = org.bidib.jbidibc.purejavacomm.PortIdentifierUtils.getPortIdentifiers();
296        }
297        if (useJSerailComm) {
298            list = org.bidib.jbidibc.jserialcomm.PortIdentifierUtils.getPortIdentifiers();
299        }
300        if (list != null) {
301            ret = new ArrayList<>();
302            String portPrefix = portNameFilter.replaceAll("\\*", "");
303            log.trace("port name filter: {}", portPrefix);
304            for (String s : list) {
305                if (s.startsWith(portPrefix)) {
306                    ret.add(s);
307                }
308            }
309        }
310        return ret;
311    }
312    
313    /**
314     * Internal method to find a port, possibly with already created BiDiB object
315     * @param requid requested unique ID of the root node
316     * @return port name as String
317     */
318    //private String findPortbyUniqueID(Long requid, BidibInterface bidib) {
319    public String findPortbyUniqueID(Long requid) {
320        // find the port for the given UID
321        // first check our static if the port was already seen.
322        String port = getKownPortName(requid);
323        if (port == null) {
324            // then try the given port if it has the requested UID
325            if (!getCurrentPortName().isEmpty()) {
326                if (!connectionRootNodeList.containsKey(getRealPortName())) {
327                    //if (bidib == null) {
328                    //    bidib = createSerialBidib();
329                    //}
330                    //Long uid = checkPort(bidib, getCurrentPortName());
331                    Long uid = checkPort(getCurrentPortName());
332                    if (uid != null  &&  uid.equals(requid)) {
333                        port = getCurrentPortName();
334                    }
335                }
336            }
337        }
338        if (port == null) {
339            // if still not found, we have to scan all known ports
340            //if (bidib == null) {
341            //    bidib = createSerialBidib();
342            //}
343            //port = scanPorts(bidib, requid, portNameFilter);
344            port = scanPorts(requid, portNameFilter);
345        }
346        if (port != null) {
347            setPort(port);
348        }
349        else {
350            if (bidib != null) {
351                bidib.close();
352                bidib = null;
353            }
354            if (requid != null) {
355                return "No Device found for BiDiB Unique ID " + ByteUtils.formatHexUniqueId(requid);
356            }
357            else if (!getCurrentPortName().isEmpty()) {
358                return "No Device found on port " + getCurrentPortName();
359            }
360            else {
361                return "port name or Unique ID not specified!";
362            }
363        }
364        return null;
365    }
366    
367    /**
368     * Scan all ports (filtered by portNameFilter) for a unique ID of the root node.
369     * 
370     * @param requid requested unique ID of the root node
371     * @param portNameFilter a port name filter (e.g. /dev/ttyUSB* for Linux)
372     * @return found port name (e.g. /dev/ttyUSB0) or null of not found
373     */
374    //static private String scanPorts(BidibInterface bidib, Long requid, String portNameFilter) {
375    private String scanPorts(Long requid, String portNameFilter) {
376        //String portPrefix = portNameFilter.replaceAll("\\*", "");
377        log.trace("scanPorts for UID {}, filter: {}", ByteUtils.formatHexUniqueId(requid), portNameFilter);
378        List<String> portNameList = getPortIdentifiers();
379        for (String portName : portNameList) {
380            //log.trace("check port {}", portName);
381            //if (portName.startsWith(portPrefix)) {
382                log.trace("check port {}", portName);
383                if (!connectionRootNodeList.containsKey(getRealPortName(portName))) {
384                    log.debug("BIDIB: try port {}", portName);
385                    //Long uid = checkPort(bidib, portName);
386                    Long uid = checkPort(portName);
387                    if (uid.equals(requid)) {
388                        return portName;
389                    }
390                }
391            //}
392        }
393        return null;
394    }
395    
396    /**
397     * Check if the given port is a BiDiB connection and returns the unique ID of the root node.
398     * Return the UID from cache if we already know the UID.
399     * 
400     * @param portName port name to check
401     * @return unique ID of the root node
402     */
403//    public Long checkPort(String portName) {
404//        if (connectionRootNodeList.containsKey(portName)) {
405//            return connectionRootNodeList.get(portName);
406//        }
407//        else {
408//            BidibInterface bidib = createSerialBidib();
409//            Long uid = checkPort(bidib, portName);
410//            bidib.close();
411//            return uid;
412//        }
413//    }
414
415    /**
416     * Internal method to check if the given port is a BiDiB connection and returns the unique ID of the root node.
417     * Return the UID from cache if we already know the UID.
418     * 
419//     * @param bidib a BiDiB object from jbidibc
420     * @param portName port name to check
421     * @return unique ID of the root node
422     */
423    //private Long checkPort(BidibInterface bidib, String portName) {
424    public Long checkPort(String portName) {
425        Long uid = null;
426        try {
427            context = new DefaultContext();
428//            if (bidib.isOpened()) {
429//                bidib.close();
430//            }
431//            bidib.close();
432            log.trace("checkPort: port name: {}, real port name: {}", portName, getRealPortName(portName));
433            if (bidib != null) {
434                bidib.close();
435                bidib = null;
436            }
437            bidib = createSerialBidib(context);
438            String realPortName = getRealPortName(portName);
439            bidib.open(realPortName, null, Collections.<NodeListener> emptySet(), Collections.<MessageListener> emptySet(), Collections.<TransferListener> emptySet(), context);
440            BidibNode rootNode = bidib.getRootNode();
441            
442            uid = rootNode.getUniqueId() & 0x0000ffffffffffL; //mask the classid
443            log.info("root node UID: {}", ByteUtils.formatHexUniqueId(uid));
444            connectionRootNodeList.put(realPortName, uid);
445            
446        }
447        catch (PortNotOpenedException ex) {
448            log.warn("port not opened: {}", portName);
449        }
450        catch (PortNotFoundException ex) {
451            log.warn("port not found 1: {}", portName);
452        }
453        catch (Exception ex) {
454            log.warn("port not found 2: {}", portName);
455        }
456        if (uid != null) {
457            bidib.close();
458            bidib = null;
459        }
460        return uid;
461    }
462    
463    /**
464     * Check if the port name of a given UID already exists in the cache.
465     * 
466     * @param reqUid requested UID
467     * @return port name or null if not found in cache
468     */
469    public String getKownPortName(Long reqUid) {
470        for(Map.Entry<String, Long> entry : connectionRootNodeList.entrySet()) {
471            Long uid = entry.getValue();
472            if (uid.equals(reqUid)) {
473                File f = new File(entry.getKey());
474                String portName = f.getName();
475                //return entry.getKey();
476                return portName;
477            }
478        }
479        return null;
480    }
481
482    
483
484    // base class methods for the BiDiBSerialPortController interface
485    // not used but must be implemented
486
487    @Override
488    public DataInputStream getInputStream() {
489//        if (!opened) {
490//            log.error("getInputStream called before load(), stream not available");
491//            return null;
492//        }
493//        return new DataInputStream(serialStream);
494        return null;
495    }
496
497    @Override
498    public DataOutputStream getOutputStream() {
499//        if (!opened) {
500//            log.error("getOutputStream called before load(), stream not available");
501//        }
502//        try {
503//            return new DataOutputStream(activeSerialPort.getOutputStream());
504//        } catch (java.io.IOException e) {
505//            log.error("getOutputStream exception: " + e);
506//        }
507        return null;
508    }
509
510    /**
511     * {@inheritDoc}
512     */
513    @Override
514    public boolean status() {
515        return opened;
516    }
517
518    /**
519     * {@inheritDoc}
520     */
521    @Override
522    public String[] validBaudRates() {
523        return Arrays.copyOf(validSpeeds, validSpeeds.length);
524    }
525
526    /**
527     * {@inheritDoc}
528     */
529    @Override
530    public int[] validBaudNumbers() {
531        return Arrays.copyOf(validSpeedValues, validSpeedValues.length);
532    }
533
534    // see Bidib SCM serial controller - only those two baud rates are specified
535    protected String[] validSpeeds = new String[]{Bundle.getMessage("Baud115200"),
536            Bundle.getMessage("Baud19200")};
537    protected int[] validSpeedValues = new int[]{115200, 19200};
538    protected String selectedSpeed = validSpeeds[0];
539
540
541    private final static Logger log = LoggerFactory.getLogger(SerialDriverAdapter.class);
542    private static final Logger MSG_RAW_LOGGER = LoggerFactory.getLogger("RAW");
543
544}