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}