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