001package jmri.jmrix.loconet.locobuffer;
002
003import java.io.DataInputStream;
004import java.io.DataOutputStream;
005import java.io.IOException;
006import java.io.InputStream;
007import java.util.Arrays;
008import java.util.Enumeration;
009import java.util.Vector;
010import jmri.jmrix.loconet.LnCommandStationType;
011import jmri.jmrix.loconet.LnPacketizer;
012import jmri.jmrix.loconet.LnPacketizerStrict;
013import jmri.jmrix.loconet.LnPortController;
014import jmri.jmrix.loconet.LocoNetSystemConnectionMemo;
015import org.slf4j.Logger;
016import org.slf4j.LoggerFactory;
017import purejavacomm.CommPortIdentifier;
018import purejavacomm.NoSuchPortException;
019import purejavacomm.PortInUseException;
020import purejavacomm.SerialPort;
021import purejavacomm.UnsupportedCommOperationException;
022
023/**
024 * Provide access to LocoNet via a LocoBuffer attached to a serial com port.
025 * <p>
026 * Normally controlled by the LocoBufferFrame class.
027 *
028 * @author Bob Jacobsen Copyright (C) 2001, 2008, 2010
029 */
030public class LocoBufferAdapter extends LnPortController {
031
032    public LocoBufferAdapter() {
033        this(new LocoNetSystemConnectionMemo());
034    }
035
036    public LocoBufferAdapter(LocoNetSystemConnectionMemo adapterMemo) {
037        super(adapterMemo);
038        option1Name = "FlowControl"; // NOI18N
039        option2Name = "CommandStation"; // NOI18N
040        option3Name = "TurnoutHandle"; // NOI18N
041        option4Name = "PacketizerType"; // NOI18N
042        options.put(option1Name, new Option(Bundle.getMessage("XconnectionUsesLabel", Bundle.getMessage("TypeSerial")), validOption1));  // NOI18N
043        options.put(option2Name, new Option(Bundle.getMessage("CommandStationTypeLabel"), getCommandStationListWithStandaloneLN(), false));  // NOI18N
044        options.put(option3Name, new Option(Bundle.getMessage("TurnoutHandling"),
045                new String[]{Bundle.getMessage("HandleNormal"), Bundle.getMessage("HandleSpread"), Bundle.getMessage("HandleOneOnly"), Bundle.getMessage("HandleBoth")})); // I18N
046        options.put(option4Name, new Option(Bundle.getMessage("PacketizerTypeLabel"), packetizerOptions()));  // NOI18N
047        options.put("TranspondingPresent", new Option(Bundle.getMessage("TranspondingPresent"),
048                new String[]{Bundle.getMessage("ButtonNo"), Bundle.getMessage("ButtonYes")} )); // NOI18N
049        options.put("InterrogateOnStart", new Option(Bundle.getMessage("InterrogateOnStart"),
050                new String[]{Bundle.getMessage("ButtonYes"), Bundle.getMessage("ButtonNo")} )); // NOI18N
051        options.put("LoconetProtocolAutoDetect", new Option(Bundle.getMessage("LoconetProtocolAutoDetectLabel"),
052                new String[]{Bundle.getMessage("ButtonNo"),Bundle.getMessage("LoconetProtocolAutoDetect")} )); // NOI18N
053    }
054    
055    /**
056     * Create a list of possible command stations and append "Standalone LocoNet"
057     * 
058     * Note: This is not suitable for use by any class which extends this class if
059     * the hardware interface is part of a command station.
060     * 
061     * @return String[] containing the array of command stations, plus "Standalone 
062     *          LocoNet"
063     */
064    public String[] getCommandStationListWithStandaloneLN() {
065        String[] result = new String[commandStationNames.length + 1];
066        for (int i = 0 ; i < result.length-1; ++i) {
067            result[i] = commandStationNames[i];
068        }
069        result[commandStationNames.length] = LnCommandStationType.COMMAND_STATION_STANDALONE.getName();
070        return result;
071    }
072    
073    Vector<String> portNameVector = null;
074    SerialPort activeSerialPort = null;
075
076    @Override
077    public Vector<String> getPortNames() {
078        // first, check that the comm package can be opened and ports seen
079        portNameVector = new Vector<>();
080        Enumeration<CommPortIdentifier> portIDs = CommPortIdentifier.getPortIdentifiers();
081        // find the names of suitable ports
082        while (portIDs.hasMoreElements()) {
083            CommPortIdentifier id = portIDs.nextElement();
084            // filter out line printers 
085            if (id.getPortType() != CommPortIdentifier.PORT_PARALLEL) // accumulate the names in a vector
086            {
087                portNameVector.addElement(id.getName());
088            }
089        }
090        return portNameVector;
091    }
092
093    @Override
094    public String openPort(String portName, String appName) {
095        // open the primary and secondary ports in LocoNet mode, check ability to set moderators
096        try {
097            // get and open the primary port
098            CommPortIdentifier portID = CommPortIdentifier.getPortIdentifier(portName);
099            try {
100                activeSerialPort = (SerialPort) portID.open(appName, 2000);  // name of program, msec to wait
101            } catch (PortInUseException p) {
102                return handlePortBusy(p, portName, log);
103            }
104            // try to set it for LocoNet via LocoBuffer
105            try {
106                setSerialPort(activeSerialPort);
107            } catch (UnsupportedCommOperationException e) {
108                log.error("Cannot set serial parameters on port {}: {}", portName, e.getMessage());
109                return "Cannot set serial parameters on port " + portName + ": " + e.getMessage(); // NOI18N
110            }
111
112            // set timeout
113            try {
114                activeSerialPort.enableReceiveTimeout(10);
115                log.debug("Serial timeout was observed as: {} enabled: {} threshold: {} enabled: {}", // NOI18N
116                    activeSerialPort.getReceiveTimeout(), 
117                    activeSerialPort.isReceiveTimeoutEnabled(),
118                    activeSerialPort.getReceiveThreshold(),
119                    activeSerialPort.isReceiveThresholdEnabled()                                        
120                );
121            } catch (Exception et) {
122                log.info("failed to set serial timeout: ", et); // NOI18N
123            }
124
125            // get and save stream
126            serialStream = activeSerialPort.getInputStream();
127
128            // purge contents, if any
129            purgeStream(serialStream);
130
131            // report status?
132            if (log.isInfoEnabled()) {
133                // report now
134                log.info("{} port opened at {} baud with DTR: {} RTS: {} DSR: {} CTS: {}  CD: {}", portName, activeSerialPort.getBaudRate(), activeSerialPort.isDTR(), activeSerialPort.isRTS(), activeSerialPort.isDSR(), activeSerialPort.isCTS(), activeSerialPort.isCD());
135            }
136            if (log.isDebugEnabled()) {
137                // report additional status
138                log.debug(" port flow control shows {}", activeSerialPort.getFlowControlMode() == SerialPort.FLOWCONTROL_RTSCTS_OUT ? "hardware flow control" : "no flow control"); // NOI18N
139
140                // log events
141                setPortEventLogging(activeSerialPort);
142            }
143
144            opened = true;
145
146        } catch (NoSuchPortException p) {
147            return handlePortNotFound(p, portName, log);
148        } catch (IOException ex) {
149            log.error("Unexpected exception while opening port {} trace follows:", portName, ex); // NOI18N
150            return "Unexpected error while opening port " + portName + ": " + ex;
151        }
152
153        return null; // normal operation
154    }
155
156    /**
157     * Can the port accept additional characters? The state of CTS determines
158     * this, as there seems to be no way to check the number of queued bytes and
159     * buffer length. This might go false for short intervals, but it might also
160     * stick off if something goes wrong.
161     * 
162     * @return an indication of whether the interface is accepting transmit messages.
163     */
164    @Override
165    public boolean okToSend() {
166        return activeSerialPort.isCTS();
167    }
168
169    /**
170     * Set up all of the other objects to operate with a LocoBuffer connected to
171     * this port.
172     */
173    @Override
174    public void configure() {
175
176        setCommandStationType(getOptionState(option2Name));
177        setTurnoutHandling(getOptionState(option3Name));
178        setTranspondingAvailable(getOptionState("TranspondingPresent"));
179        setInterrogateOnStart(getOptionState("InterrogateOnStart"));
180        setLoconetProtocolAutoDetect(getOptionState("LoconetProtocolAutoDetect"));
181        // connect to a packetizing traffic controller
182        LnPacketizer packets = getPacketizer(getOptionState(option4Name));
183        packets.connectPort(this);
184
185        // create memo
186        this.getSystemConnectionMemo().setLnTrafficController(packets);
187        // do the common manager config
188
189        this.getSystemConnectionMemo().configureCommandStation(commandStationType,
190                mTurnoutNoRetry, mTurnoutExtraSpace, mTranspondingAvailable, mInterrogateAtStart, mLoconetProtocolAutoDetect);
191        this.getSystemConnectionMemo().configureManagers();
192
193        // start operation
194        packets.startThreads();
195    }
196
197    // base class methods for the LnPortController interface
198    @Override
199    public DataInputStream getInputStream() {
200        if (!opened) {
201            log.error("getInputStream called before load(), stream not available"); // NOI18N
202            return null;
203        }
204        return new DataInputStream(serialStream);
205    }
206
207    @Override
208    public DataOutputStream getOutputStream() {
209        if (!opened) {
210            log.error("getOutputStream called before load(), stream not available"); // NOI18N
211        }
212        try {
213            return new DataOutputStream(activeSerialPort.getOutputStream());
214        } catch (java.io.IOException e) {
215            log.error("getOutputStream exception: {}", e.getMessage());
216        }
217        return null;
218    }
219
220    @Override
221    public boolean status() {
222        return opened;
223    }
224
225    /**
226     * Local method to do specific configuration, overridden in class
227     * @param activeSerialPort is the serial port to be configured
228     * @throws UnsupportedCommOperationException Usually if the hardware isn't present or capable
229     */
230    protected void setSerialPort(SerialPort activeSerialPort) throws UnsupportedCommOperationException {
231        // find the baud rate value, configure comm options
232        int baud = currentBaudNumber(mBaudRate);
233        activeSerialPort.setSerialPortParams(baud, SerialPort.DATABITS_8,
234                SerialPort.STOPBITS_1, SerialPort.PARITY_NONE);
235
236        // find and configure flow control from option
237        int flow = SerialPort.FLOWCONTROL_RTSCTS_OUT; // default, but also defaults in selectedOption1
238        if (getOptionState(option1Name).equals(validOption1[1])) {
239            flow = SerialPort.FLOWCONTROL_NONE;
240        }
241        configureLeadsAndFlowControl(activeSerialPort, flow);
242
243        log.info("LocoBuffer (serial) adapter{}{} RTSCTS_OUT=" + SerialPort.FLOWCONTROL_RTSCTS_OUT + " RTSCTS_IN=" + SerialPort.FLOWCONTROL_RTSCTS_IN, activeSerialPort.getFlowControlMode() == SerialPort.FLOWCONTROL_RTSCTS_OUT ? " set hardware flow control, mode=" : " set no flow control, mode=", activeSerialPort.getFlowControlMode());
244    }
245
246    /**
247     * {@inheritDoc}
248     */
249    @Override
250    public String[] validBaudRates() {
251        return Arrays.copyOf(validSpeeds, validSpeeds.length);
252    }
253
254    /**
255     * {@inheritDoc}
256     */
257    @Override
258    public int[] validBaudNumbers() {
259        return Arrays.copyOf(validSpeedValues, validSpeedValues.length);
260    }
261
262    protected String[] validSpeeds = new String[]{Bundle.getMessage("Baud19200LB"), Bundle.getMessage("Baud57600LB")};
263    protected int[] validSpeedValues = new int[]{19200, 57600};
264
265    @Override
266    public int defaultBaudIndex() {
267        return 0;
268    }
269
270    // meanings are assigned to these above, so make sure the order is consistent
271    protected String[] validOption1 = new String[]{Bundle.getMessage("FlowOptionHwRecomm"), Bundle.getMessage("FlowOptionNo")};
272
273    // private control members
274    private boolean opened = false;
275    InputStream serialStream = null;
276
277    /**
278     *  Define the readable data and internal code
279     */
280    private static String[][] packetizers = { {Bundle.getMessage("PacketizerTypelnPacketizer"),"lnPacketizer" },
281            {Bundle.getMessage("PacketizerTypelnPacketizerStrict"),"lnPacketizerStrict"} };
282
283    /**
284     *
285     * @return String array of readable choices
286     */
287    private String[] packetizerOptions() {
288        String[] retval = new String[packetizers.length];
289        for (int i=0;i < packetizers.length; i++) {
290            retval[i] = packetizers[i][0];
291        }
292        return retval;
293    }
294    /**
295     * for a given readable choice return internal value
296     * or the default
297     *
298     * @param s  string containing ?a packetizer name?
299     * @return internal value
300     */
301    protected String getPacketizerOption(String s) {
302        for (int i=0;i < packetizers.length; i++) {
303            if (packetizers[i][0].equals(s)) {
304                return packetizers[i][1];
305            }
306        }
307        return "lnPacketizer";
308    }
309    /**
310     * 
311     * @param s the packetizer to use in its readable form.
312     * @return a LnPacketizer
313     */
314    protected LnPacketizer getPacketizer(String s) {
315        LnPacketizer packets;
316        String packetSelection = getPacketizerOption(s);
317        switch (packetSelection) {
318            case "lnPacketizer":
319                packets = new LnPacketizer(this.getSystemConnectionMemo());
320                break;
321            case "lnPacketizerStrict":
322                packets = new LnPacketizerStrict(this.getSystemConnectionMemo());
323                break;
324            default:
325                packets = new LnPacketizer(this.getSystemConnectionMemo());
326                log.warn("Using Normal do not understand option [{}]", packetSelection);
327        }
328        return packets;
329    }
330
331    private final static Logger log = LoggerFactory.getLogger(LocoBufferAdapter.class);
332
333}