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