001package jmri.jmrix.roco.z21;
002
003import java.io.DataInputStream;
004import java.io.DataOutputStream;
005import java.io.IOException;
006import java.io.PipedInputStream;
007import java.io.PipedOutputStream;
008
009import jmri.jmrix.loconet.LocoNetListener;
010import jmri.jmrix.loconet.LocoNetMessage;
011import jmri.jmrix.loconet.LocoNetMessageException;
012import jmri.jmrix.loconet.LocoNetSystemConnectionMemo;
013import jmri.jmrix.loconet.streamport.LnStreamPortController;
014import jmri.util.ImmediatePipedOutputStream;
015
016import org.slf4j.Logger;
017import org.slf4j.LoggerFactory;
018
019/**
020 * Interface between z21 messages and an LocoNet stream.
021 * <p>
022 * Parts of this code are derived from the
023 * jmri.jmrix.lenz.xnetsimulator.XNetSimulatorAdapter class.
024 *
025 * @author Paul Bender Copyright (C) 2014
026 */
027public class Z21LocoNetTunnel implements Z21Listener, LocoNetListener , Runnable {
028
029    LnStreamPortController lsc = null;
030    private DataOutputStream pout = null; // for output to other classes
031    private DataInputStream pin = null; // for input from other classes
032    // internal ends of the pipes
033    private DataOutputStream outpipe = null;  // feed pin
034    private DataInputStream inpipe = null; // feed pout
035    private Z21SystemConnectionMemo _memo;
036    private Thread sourceThread;
037
038    /**
039     * Build a new LocoNet tunnel.
040     * @param memo system connection.
041     */
042    public Z21LocoNetTunnel(Z21SystemConnectionMemo memo) {
043        // save the SystemConnectionMemo.
044        _memo = memo;
045        init();
046    }
047
048    private void init() {
049
050        // configure input and output pipes to use for
051        // the communication with the LocoNet implementation.
052        try {
053            PipedOutputStream tempPipeI = new ImmediatePipedOutputStream();
054            pout = new DataOutputStream(tempPipeI);
055            inpipe = new DataInputStream(new PipedInputStream(tempPipeI));
056            PipedOutputStream tempPipeO = new ImmediatePipedOutputStream();
057            outpipe = new DataOutputStream(tempPipeO);
058            pin = new DataInputStream(new PipedInputStream(tempPipeO));
059        } catch (java.io.IOException e) {
060            log.error("init (pipe): Exception: {}", e.toString());
061            return;
062        }
063
064        // start a thread to read from the input pipe.
065        sourceThread = new Thread(this);
066        sourceThread.setName("z21.Z21LocoNetTunnel sourceThread");
067        sourceThread.setDaemon(true);
068        sourceThread.start();
069
070        // Then use those pipes as the input and output pipes for
071        // a new LnStreamPortController object.
072        LocoNetSystemConnectionMemo lnMemo = new LocoNetSystemConnectionMemo();
073        setStreamPortController(new Z21LnStreamPortController(lnMemo,pin, pout, "None"));
074
075        // register as a Z21Listener, so we can receive replies
076        _memo.getTrafficController().addz21Listener(this);
077
078        // start the LocoNet configuration.
079        lsc.configure();
080    }
081
082    @Override
083    public void run() { // start a new thread
084        // this thread has one task.  It repeatedly reads from the input pipe
085        // and writes modified data to the output pipe.  This is the heart
086        // of the command station simulation.
087        log.debug("LocoNet Tunnel Thread Started");
088        for (;;) {
089            LocoNetMessage m = readMessage();
090            if(m != null) {
091               // don't forward a null message.
092               message(m);
093            }
094        }
095    }
096
097    /**
098     * Read one incoming message from the buffer and set
099     * outputBufferEmpty to true.
100     */
101    private LocoNetMessage readMessage() {
102        LocoNetMessage msg = null;
103        try {
104            msg = loadChars();
105        } catch (java.io.IOException|LocoNetMessageException e) {
106            // should do something meaningful here.
107        }
108        return (msg);
109    }
110
111    /**
112     * Get characters from the input source, and file a message.
113     * <p>
114     * Returns only when the message is complete.
115     * <p>
116     * Only used in the Receive thread.
117     *
118     * @return filled message
119     * @throws IOException when presented by the input source.
120     */
121    private LocoNetMessage loadChars() throws java.io.IOException,LocoNetMessageException {
122        int opCode;
123        // start by looking for command -  skip if bit not set
124        while (((opCode = (readByteProtected(inpipe) & 0xFF)) & 0x80) == 0) { // the real work is in the loop check
125            log.trace("Skipping: {}", Integer.toHexString(opCode)); // NOI18N
126        }
127        // here opCode is OK. Create output message
128        log.trace(" (RcvHandler) Start message with opcode: {}", Integer.toHexString(opCode)); // NOI18N
129        LocoNetMessage msg = null;
130        while (msg == null) {
131           try {
132              // Capture 2nd byte, always present
133              int byte2 = readByteProtected(inpipe) & 0xFF;
134              log.trace("Byte2: {}", Integer.toHexString(byte2)); // NOI18N
135              int len = 2;
136              switch ((opCode & 0x60) >> 5) {
137                 case 0:
138                    /* 2 byte message */
139                    len = 2;
140                    break;
141                 case 1:
142                    /* 4 byte message */
143                    len = 4;
144                    break;
145                 case 2:
146                    /* 6 byte message */
147
148                    len = 6;
149                    break;
150                 case 3:
151                    /* N byte message */
152                    if (byte2 < 2) {
153                        log.error("LocoNet message length invalid: {} opcode: {}", byte2, Integer.toHexString(opCode)); // NOI18N
154                    }
155                    len = byte2;
156                    break;
157                 default:
158                    log.warn("Unhandled code: {}", (opCode & 0x60) >> 5);
159                 break;
160              }
161              msg = new LocoNetMessage(len);
162              // message exists, now fill it
163              msg.setOpCode(opCode);
164              msg.setElement(1, byte2);
165              log.trace("len: {}", len); // NOI18N
166              for (int i = 2; i < len; i++) {
167                 // check for message-blocking error
168                 int b = readByteProtected(inpipe) & 0xFF;
169                 log.trace("char {} is: {}", i, Integer.toHexString(b)); // NOI18N
170                 if ((b & 0x80) != 0) {
171                     log.warn("LocoNet message with opCode: {} ended early. Expected length: {} seen length: {} unexpected byte: {}", Integer.toHexString(opCode), len, i, Integer.toHexString(b)); // NOI18N
172                    opCode = b;
173                    throw new LocoNetMessageException();
174                 }
175                 msg.setElement(i, b);
176              }
177           } catch (LocoNetMessageException e) {
178              // retry by destroying the existing message
179              // opCode is set for the newly-started packet
180              msg = null;
181           }
182        }
183        // check parity
184        if (!msg.checkParity()) {
185           log.warn("Ignore LocoNet packet with bad checksum: {}", msg);
186           throw new LocoNetMessageException();
187        }
188        // message is complete, dispatch it !!
189        return msg;
190    }
191
192    /**
193     * Read a single byte, protecting against various timeouts, etc.
194     * <p>
195     * When a port is set to have a receive timeout (via the
196     * enableReceiveTimeout() method), some will return zero bytes or an
197     * EOFException at the end of the timeout. In that case, the read should be
198     * repeated to get the next real character.
199     */
200    private byte readByteProtected(DataInputStream istream) throws java.io.IOException {
201        byte[] rcvBuffer = new byte[1];
202        while (true) { // loop will repeat until character found
203            int nchars;
204            nchars = istream.read(rcvBuffer, 0, 1);
205            if (nchars > 0) {
206                return rcvBuffer[0];
207            }
208        }
209    }
210
211    // Z21Listener interface methods.
212
213    /**
214     * Member function that will be invoked by a z21Interface implementation to
215     * forward a z21 message from the layout.
216     *
217     * @param msg The received z21 message. Note that this same object may be
218     *            presented to multiple users. It should not be modified here.
219     */
220    @Override
221    public void reply(Z21Reply msg) {
222        // This funcction forwards the payload of an LocoNet message
223        // tunneled in a z21 message and forwards it to the XpressNet
224        // implementation's input stream.
225        if (msg.isLocoNetTunnelMessage()) {
226            LocoNetMessage reply = msg.getLocoNetMessage();
227            log.debug("Z21 Reply {} forwarded to XpressNet implementation as {}",
228                    msg, reply);
229            for (int i = 0; i < reply.getNumDataElements(); i++) {
230                try {
231                    outpipe.writeByte(reply.getElement(i));
232                } catch (java.io.IOException ioe) {
233                    log.error("Error writing XpressNet Reply to XpressNet input stream.");
234                }
235            }
236        }
237    }
238
239    /**
240     * Member function that will be invoked by a z21Interface implementation to
241     * forward a z21 message sent to the layout. Normally, this function will do
242     * nothing.
243     *
244     * @param msg The received z21 message. Note that this same object may be
245     *            presented to multiple users. It should not be modified here.
246     */
247    @Override
248    public void message(Z21Message msg) {
249        // this function does nothing.
250    }
251
252    // LocoNetListener Interface methods.
253
254    /**
255     * Member function that will be invoked by a LocoNet Interface implementation to
256     * forward a LocoNet message sent to the layout. Normally, this function will
257     * do nothing.
258     *
259     * @param msg The received LocoNet message. Note that this same object may be
260     *            presented to multiple users. It should not be modified here.
261     */
262    @Override
263    public void message(LocoNetMessage msg) {
264        // when an LocoNet message shows up here, package it in a Z21Message
265        Z21Message message = new Z21Message(msg);
266        log.debug("LocoNet Message {} forwarded to z21 Interface as {}",
267                    msg, message);
268        // and send the z21 message to the interface
269        _memo.getTrafficController().sendz21Message(message, this);
270    }
271
272    /**
273     * Package protected method to retrieve the stream port controller
274     * associated with this tunnel.
275     * @return PortController for this connection
276     */
277    jmri.jmrix.loconet.streamport.LnStreamPortController getStreamPortController() {
278       return lsc;
279    }
280
281    /**
282     * Package protected method to set the stream port controller
283     * associated with this tunnel.
284     * @param x PortController for this connection
285     */
286    void setStreamPortController(LnStreamPortController x){
287        lsc = x;
288
289        // configure the XpressNet connections properties.
290        lsc.getSystemConnectionMemo().setSystemPrefix("L");
291        lsc.getSystemConnectionMemo().setUserName(_memo.getUserName() + "LocoNet");
292
293    }
294
295    @SuppressWarnings("deprecation") // Thread.stop
296    public void dispose(){
297        if(lsc != null){
298            lsc.dispose();
299        }
300        if( _memo != null ) {
301            Z21TrafficController tc = _memo.getTrafficController();
302            if ( tc != null ) {
303                tc.removez21Listener(this);
304            }
305           _memo.dispose();
306        }
307        try {
308            inpipe.close();
309        } catch (IOException ex) {
310            // Ignore IO error
311        }
312    }
313
314    private final static Logger log = LoggerFactory.getLogger(Z21LocoNetTunnel.class);
315
316}