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