001package jmri.jmrix.roco.z21.simulator;
002
003import java.net.*;
004import jmri.JmriException;
005import jmri.jmrix.lenz.XNetMessage;
006import jmri.jmrix.lenz.XNetReply;
007import jmri.jmrix.roco.z21.Z21Adapter;
008import jmri.jmrix.roco.z21.Z21Message;
009import jmri.jmrix.roco.z21.Z21Reply;
010import jmri.jmrix.roco.z21.Z21TrafficController;
011import org.slf4j.Logger;
012import org.slf4j.LoggerFactory;
013
014/**
015 * Provide access to a simulated z21 system.
016 * <p>
017 * Currently, the z21Simulator reacts to commands sent from the user interface
018 * with messages an appropriate reply message.
019 * <p>
020 * NOTE: Some material in this file was modified from other portions of the
021 * support infrastructure.
022 *
023 * @author Paul Bender, Copyright (C) 2015
024 */
025public class Z21SimulatorAdapter extends Z21Adapter implements Runnable {
026
027    private Thread sourceThread;
028    private Z21XNetSimulatorAdapter xnetadapter;
029
030    // simulation state variables
031    private int[] flags = {0x00, 0x00, 0x00, 0x00}; // holds the flags sent by the client.
032
033    public Z21SimulatorAdapter() {
034        super();
035        setHostName("localhost");
036        // start a UDP server that we can connect to.  The server will
037        // produce the appropriate responses.
038        xnetadapter = new Z21XNetSimulatorAdapter();
039    }
040
041    /**
042     * Set up all of the other objects to operate with a z21Simulator connected
043     * to this port.
044     */
045    @Override
046    public void configure() {
047        log.debug("configure called");
048
049        // connect to a packetizing traffic controller that ignores timeouts
050        Z21TrafficController packets = new Z21TrafficController(){
051                @Override
052                protected void warnOnTimeout(jmri.jmrix.AbstractMRMessage msg, jmri.jmrix.AbstractMRListener l) {}
053        };
054        packets.connectPort(this);
055
056        // start operation
057        // packets.startThreads();
058        this.getSystemConnectionMemo().setTrafficController(packets);
059
060        sourceThread = new Thread(this);
061        sourceThread.setName("Z21SimulatorAdapter sourceThread");
062        sourceThread.start();
063
064        this.getSystemConnectionMemo().configureManagers();
065    }
066
067    /**
068     * {@inheritDoc}
069     */
070    @Override
071    public void connect() throws java.io.IOException {
072        log.debug("connect called");
073
074        setHostAddress("localhost"); // always localhost for the simulation.
075        super.connect();
076    }
077
078    /**
079     * Terminate service thread
080     * <p>
081     * This is intended to be used only by testing subclasses.
082     */
083    public void terminateThread() {
084        threadStopRequest = true;
085        if (sourceThread != null) {
086            sourceThread.interrupt();
087            try {
088                sourceThread.join();
089            } catch (InterruptedException ie) {
090                // interrupted during cleanup.
091            }
092        }
093        if (socket != null) {
094            socket.close();
095        }
096    }
097
098    volatile boolean threadStopRequest;
099    volatile DatagramSocket socket;
100
101    static class LogoffException extends JmriException {
102    }
103
104    /**
105     * {@inheritDoc}
106     */
107    @Override
108    public void run() {
109        // The server just opens a DatagramSocket using the specified port number,
110        // and then goes into an infinite loop.
111
112        // try connecting to the port up to three times
113        int retryCount = 0;
114        while (retryCount < 3 && !threadStopRequest) {
115            try (DatagramSocket s = new DatagramSocket(COMMUNICATION_UDP_PORT)) {
116
117                socket = s; // save for later close()
118                s.setSoTimeout(100); // timeout periodically
119                log.debug("socket created, starting loop");
120                while (!threadStopRequest) {
121                    log.debug("simulation loop");
122                    // the server waits for a client to connect, then echos the data sent back.
123                    byte[] input = new byte[100]; // input from network
124                    try {
125
126                        // to receive the data, we create a packet.
127                        DatagramPacket receivePacket = new DatagramPacket(input, 100);
128                        // and wait for the data to arrive.
129                        s.receive(receivePacket);
130                        if (threadStopRequest) {
131                            return;
132                        }
133
134                        Z21Message msg = new Z21Message(receivePacket.getLength());
135                        for (int i = 0; i < receivePacket.getLength(); i++) {
136                            msg.setElement(i, receivePacket.getData()[i]);
137                        }
138
139                        // to echo the data back, we need to find the IP and port to send
140                        // the data to.
141                        InetAddress IPAddress = receivePacket.getAddress();
142                        int port = receivePacket.getPort();
143
144                        log.debug("Received packet: {}, message: {}", receivePacket.getData(), msg);
145
146                        Z21Reply reply;
147                        // and then we create the return packet.
148                        try {
149                            reply = generateReply(msg);
150                        } catch (LogoffException e) {
151                            // the simulation ends here. break out of the loop.
152                            log.debug("error generated by generateReply, exiting simulation");
153                            break;
154                        }
155                        if (reply != null) {
156                            // only attempt to send a reply if there was actually
157                            // a reply generated, since some messages don't do that.
158                            byte[] ba = jmri.util.StringUtil.bytesFromHexString(reply.toString());
159                            DatagramPacket sendPacket = new DatagramPacket(ba, ba.length, IPAddress, port);
160                            // and send it back using our socket
161                            s.send(sendPacket);
162                        }
163                    } catch (java.net.SocketTimeoutException ste) {
164                        // not an error, recheck the condition on the while.
165                        continue;
166                    } catch (java.io.IOException ex3) {
167                        if (!threadStopRequest) {
168                            log.error("IO Exception", ex3);
169                        } else {
170                            return;
171                        }
172                    }
173                    log.debug("Client Disconnect");
174                }
175            } catch (BindException bex) {
176                retryCount++;
177                if (retryCount > 2) {
178                    log.error("Giving up after {} attempts.  Exception binding to port {}", retryCount, COMMUNICATION_UDP_PORT, bex);
179                    return;
180                } else {
181                    log.info("Attempt {}: Exception binding to port {}", retryCount, COMMUNICATION_UDP_PORT);
182                    try {
183                        Thread.sleep(retryCount * 1000L); // wait a few seconds before attempting to bind again.
184                    } catch (InterruptedException ie) {
185                        // the sleep is just to give time for another process
186                        // to exit, so it is ok if it finishes early.
187                    }
188                }
189            } catch (SocketException ex0) {
190                log.error("Exception opening socket", ex0);
191                return; // can't continue from this
192            } catch (RuntimeException rte) {
193                // subclasses of RuntimeException may occur at times other than 
194                // when opening the socket.
195                log.error("Exception performing operation on socket", rte);
196                return; // can't continue from this
197            }
198        } // end of bind retry.
199    } // end of run.
200
201    // generateReply is the heart of the simulation.  It translates an
202    // incoming XNetMessage into an outgoing XNetReply.
203    private Z21Reply generateReply(Z21Message m) throws LogoffException {
204        log.debug("generate Reply called with message {}", m);
205        Z21Reply reply;
206        switch (m.getOpCode()) {
207            case 0x0010:
208                // request for serial number
209                reply = getZ21SerialNumberReply();
210                break;
211            case 0x001a:
212                // request for hardware version info.
213                reply = getHardwareVersionReply();
214                break;
215            case 0x0040:
216                // XpressNet tunnel message.
217                XNetMessage xnm = getXNetMessage(m);
218                log.debug("Received XNet Message: {}", m);
219                XNetReply xnr = xnetadapter.generateReply(xnm);
220                reply = getZ21ReplyFromXNet(xnr);
221                break;
222            case 0x0030:
223                // LAN LOGOFF
224                // this is the end of the simulation, throw an exception
225                // to indicate this.
226                throw (new LogoffException());
227            case 0x0050:
228                // set broadcast flags
229                flags[0] = m.getElement(4) & 0xff;
230                flags[1] = m.getElement(5) & 0xff;
231                flags[2] = m.getElement(6) & 0xff;
232                flags[3] = m.getElement(7) & 0xff;
233                // per the protocol, no reply is generated.
234                reply = null;
235                break;
236            case 0x0051:
237                // get broadcast flags
238                reply = getZ21BroadCastFlagsReply();
239                break;
240            case 0x0089:
241                // Get Railcom Data
242                reply = getZ21RailComDataChangedReply();
243                break;
244            case 0x00A2:
245                // loconet data from lan
246                reply = null; // for now, no reply to this message.
247                break;
248            case 0x00A3:
249                // loconet dispatch address
250                reply = getLocoNetDispatchReply(m);
251                break;
252            case 0x00A4:
253                // get loconet detector status
254                reply = getLocoNetDetectorStatusReply(m);
255                break;
256            case 0x0060:
257            // get loco mode
258            case 0x0061:
259            // set loco mode
260            case 0x0070:
261            // get turnout mode
262            case 0x0071:
263            // set turnout mode
264            case 0x0081:
265            // get RMBus data
266            case 0x0082:
267            // program RMBus module
268            case 0x0085:
269            // get system state
270            default:
271                reply = getXPressNetUnknownCommandReply();
272        }
273        return reply;
274    }
275
276    // canned reply messages;
277    private Z21Reply getHardwareVersionReply() {
278        Z21Reply reply = new Z21Reply();
279        reply.setLength(0x000c);
280        reply.setOpCode(0x001a);
281        reply.setElement(4, 0x00);
282        reply.setElement(5, 0x02);
283        reply.setElement(6, 0x00);
284        reply.setElement(7, 0x00);
285        reply.setElement(8, 0x20);
286        reply.setElement(9, 0x01);
287        reply.setElement(10, 0x00);
288        reply.setElement(11, 0x00);
289        return reply;
290    }
291
292    private Z21Reply getXPressNetUnknownCommandReply() {
293        Z21Reply reply = new Z21Reply();
294        reply.setLength(0x0007);
295        reply.setOpCode(0x0040);
296        reply.setElement(4, 0x61);
297        reply.setElement(5, 0x82);
298        reply.setElement(6, 0xE3);
299        return reply;
300    }
301
302    private Z21Reply getZ21SerialNumberReply() {
303        Z21Reply reply = new Z21Reply();
304        reply.setLength(0x0008);
305        reply.setOpCode(0x0010);
306        reply.setElement(4, 0x00);
307        reply.setElement(5, 0x00);
308        reply.setElement(6, 0x00);
309        reply.setElement(7, 0x00);
310        return reply;
311    }
312
313    private Z21Reply getZ21BroadCastFlagsReply() {
314        Z21Reply reply = new Z21Reply();
315        reply.setLength(0x0008);
316        reply.setOpCode(0x0051);
317        reply.setElement(4, flags[0]);
318        reply.setElement(5, flags[1]);
319        reply.setElement(6, flags[2]);
320        reply.setElement(7, flags[3]);
321        return reply;
322    }
323
324    private Z21Reply getZ21RailComDataChangedReply() {
325        Z21Reply reply = new Z21Reply();
326        reply.setOpCode(0x0088);
327        reply.setLength(0x0004);
328        int offset = 4;
329        for (int i = 0; i < xnetadapter.locoCount; i++) {
330            reply.setElement(offset++, xnetadapter.locoData[i].getAddressLsb());// byte 5, LocoAddress lsb.
331            reply.setElement(offset++, xnetadapter.locoData[i].getAddressMsb());// byte 6, LocoAddress msb.
332            reply.setElement(offset++, 0x00);// bytes 7-10,32 bit reception counter.
333            reply.setElement(offset++, 0x00);
334            reply.setElement(offset++, 0x00);
335            reply.setElement(offset++, 0x01);
336            reply.setElement(offset++, 0x00);// bytes 11-14,32 bit error counter.
337            reply.setElement(offset++, 0x00);
338            reply.setElement(offset++, 0x00);
339            reply.setElement(offset++, 0x00);
340            reply.setElement(offset++, xnetadapter.locoData[i].getSpeed());//currently reserved.Speed in firmware<=1.12
341            reply.setElement(offset++, 0x00);//currently reserved.Options in firmware<=1.12
342            reply.setElement(offset++, 0x00);//currently reserved.Temp in firmware<=1.12
343            reply.setLength(0xffff & offset);
344        }
345        log.debug("output {} offset: {}", reply.toString(), offset);
346        return reply;
347    }
348
349    // utility functions
350    private XNetMessage getXNetMessage(Z21Message m) {
351        if (m == null) {
352            throw new IllegalArgumentException();
353        }
354        XNetMessage xnm = new XNetMessage(m.getLength() - 4);
355        for (int i = 4; i < m.getLength(); i++) {
356            xnm.setElement(i - 4, m.getElement(i));
357        }
358        return xnm;
359    }
360
361    private Z21Reply getZ21ReplyFromXNet(XNetReply m) {
362        if (m == null) {
363            throw new IllegalArgumentException();
364        }
365        Z21Reply r = new Z21Reply();
366        r.setLength(m.getNumDataElements() + 4);
367        r.setOpCode(0x0040);
368        for (int i = 0; i < m.getNumDataElements(); i++) {
369            r.setElement(i + 4, m.getElement(i));
370        }
371        return (r);
372    }
373
374    private Z21Reply getLocoNetDispatchReply(Z21Message m) {
375        if (m == null) {
376            throw new IllegalArgumentException();
377        }
378        Z21Reply r = new Z21Reply();
379        r.setLength(m.getNumDataElements() + 5);
380        r.setOpCode(m.getOpCode());
381        int i;
382        for (i = 0; i < m.getNumDataElements(); i++) {
383            r.setElement(i + 4, m.getElement(i));
384        }
385        r.setElement(i + 4, 0x00);
386        return (r);
387    }
388
389    private Z21Reply getLocoNetDetectorStatusReply(Z21Message m) {
390        if (m == null) {
391            throw new IllegalArgumentException();
392        }
393        Z21Reply r = new Z21Reply();
394        r.setLength(m.getNumDataElements() + 5);
395        r.setOpCode(m.getOpCode());
396        int i;
397        for (i = 0; i < m.getNumDataElements(); i++) {
398            r.setElement(i + 4, m.getElement(i));
399        }
400        r.setElement(i + 4, 0x00);
401        return (r);
402    }
403
404    private final static Logger log = LoggerFactory.getLogger(Z21SimulatorAdapter.class);
405
406}