001package jmri.jmrit.z21server; 002 003import org.slf4j.Logger; 004import org.slf4j.LoggerFactory; 005 006import java.net.DatagramPacket; 007import java.net.DatagramSocket; 008import java.net.InetAddress; 009import java.net.SocketException; 010import java.util.Arrays; 011import java.util.HashMap; 012import java.beans.PropertyChangeListener; 013import java.beans.PropertyChangeEvent; 014 015/** 016 * This is a server for Z21 clients like the Z21 App or the Roco Z21 WlanMaus. It is not meant to be a 017 * fully equipped Z21 server. 018 * 019 * @author Jean-Yves Roda (C) 2023 020 * @author Eckart Meyer (C) 2025 (enhancements, WlanMaus support) 021 */ 022 023// TODO: 024// - handle MultiPacket datagrams (though neither the Z21 App not the WlanMaus seem to use them) 025// - implement CV programming 026// - create unit test classes 027 028public class MainServer implements Runnable, PropertyChangeListener { 029 030 private final static Logger log = LoggerFactory.getLogger(MainServer.class); 031 public final static int port = 21105; 032 DatagramSocket mySS; 033 034/** 035 * The main server running in a separate thread. 036 * Do some setup and then read from the network in loop. 037 */ 038 @Override 039 public void run() { 040 try { 041 mySS = new DatagramSocket(port); 042 043 byte[] buf = new byte[256]; 044 DatagramPacket packet = new DatagramPacket(buf, buf.length); 045 046 log.info("Created socket, listening for connections"); 047 048 ClientManager.getInstance().setChangeListener(this); 049 Service40.setChangeListener(this); 050 051 while (true) { 052 053 if (Thread.interrupted()) break; 054 boolean bReceivedData; 055 056 log.trace("Loop ****"); 057 ClientManager.getInstance().handleExpiredClients(false); 058 059 //mySS.setSoTimeout(500); 060 mySS.setSoTimeout(2000);// since almost everything is asynchroneous now, we need the forced exec only for expired clients 061 try { 062 mySS.receive(packet); 063 bReceivedData = true; 064 } catch (Exception e) { 065 bReceivedData = false; 066 } 067 068 if (!bReceivedData) continue; 069 070 071 InetAddress clientAddress = packet.getAddress(); 072 073 ClientManager.getInstance().heartbeat(clientAddress); 074 075 byte[] rawData = packet.getData(); 076 int dataLenght = rawData[0]; 077 byte[] actualData = Arrays.copyOf(rawData, dataLenght); 078 String ident = "[" + clientAddress + "] "; 079 log.debug("{}: recv raw frame {} ", ident, bytesToHex(actualData)); 080 081 if (actualData.length < 3) { 082 log.debug("error, frame : {}", bytesToHex(actualData)); 083 } 084 085 byte[] response = null; 086 087 // parse header byte. 088 switch (actualData[2]) { 089 case 0x50: 090 // LAN_SET_BROADCASTFLAGS - currently ignored, but accepted 091 byte[] maskArray = Arrays.copyOfRange(actualData, HEADER_SIZE, dataLenght); 092 int mask = fromByteArrayLittleEndian(maskArray); 093 log.debug("{} Broadcast request with mask : {}", ident, Integer.toBinaryString(mask)); 094 break; 095 case 0x30: 096 // LAN_LOGOFF - the client wants to exit 097 log.debug("{} Disconnect frame", ident); 098 ClientManager.getInstance().unregisterClient(clientAddress); 099 break; 100 case 0x10: 101 // LAN_GET_SERIAL_NUMBER - send a serial number as 32bit number - we always send 0x00000000 102 log.debug("Send 32bit serialnumber"); 103 response = new byte[] {0x08, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00}; 104 break; 105 case 0x40: 106 // X-Bus commands - here is the real work 107 byte[] payloadData = Arrays.copyOfRange(actualData, HEADER_SIZE, dataLenght); 108 response = Service40.handleService(payloadData, clientAddress); 109 break; 110 111 default: 112 log.debug("{} Service not yet implemented : 0x{}", ident, Integer.toHexString(actualData[2] & 0xFF)); 113 } 114 115 if (response != null) { 116 // send back a response packet to the sending client 117 sendResponse(clientAddress, response); 118 } 119 } 120 121 log.info("Z21 App Server shut down."); 122 123 } catch (SocketException e) { 124 log.info("Z21 App Server encountered an error, exiting.", e); 125 } 126 127 if (mySS != null) { 128 mySS.close(); 129 } 130 131 } 132 133/** 134 * Send a Z21 packet to all registered clients. 135 * 136 * @param response - a Z21 packet 137 */ 138 public void sendResponseToRegisteredClients(byte[] response) { 139 HashMap<InetAddress, AppClient> registeredClients = ClientManager.getInstance().getRegisteredClients(); //send to all registered clients 140 log.trace("Sending to all registered clients (broadcast)"); 141 for (HashMap.Entry<InetAddress, AppClient> entry : registeredClients.entrySet()) { 142 sendResponse(entry.getKey(), response); 143 } 144 } 145 146/** 147 * Send a Z21 packet to a single client. 148 * 149 * @param respAddress - client's InetAdress 150 * @param response - a Z21 packet 151 */ 152 public void sendResponse(InetAddress respAddress, byte[] response) { 153 if (response != null) { 154 DatagramPacket responsePacket = new DatagramPacket(response, response.length, respAddress, port); 155 if (log.isTraceEnabled()) { 156 String sendIdent = "[-> " + respAddress + "] "; 157 log.trace("{}: send raw frame {} ", sendIdent, bytesToHex(response)); 158 } 159 try { 160 mySS.send(responsePacket); 161 } catch (Exception e) { 162 log.debug("Unable to send packet to client {}", respAddress.toString()); 163 } 164 } 165 } 166 167/** 168 * Change listener. 169 * If new value contains a Z21 packet, send it to all registered clients. 170 * 171 * @param pce - property change event from the caller 172 */ 173 @Override 174 public void propertyChange(PropertyChangeEvent pce) { 175 log.trace("change event: {}", pce); 176 if (pce.getNewValue() != null) { 177 sendResponseToRegisteredClients( (byte [])pce.getNewValue() ); 178 } 179 } 180 181// helper functions 182 183 private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); 184 185 private static String bytesToHex(byte[] bytes) { 186 char[] hexChars = new char[bytes.length * 2]; 187 for (int j = 0; j < bytes.length; j++) { 188 int v = bytes[j] & 0xFF; 189 hexChars[j * 2] = HEX_ARRAY[v >>> 4]; 190 hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; 191 } 192 return new String(hexChars); 193 } 194 195 196 private static int fromByteArrayLittleEndian(byte[] bytes) { 197 return ((bytes[2] & 0xFF) << 24) | 198 ((bytes[3] & 0xFF) << 16) | 199 ((bytes[0] & 0xFF) << 8) | 200 ((bytes[1] & 0xFF) << 0); 201 } 202 203 private static final short HEADER_SIZE = 4; 204 205 206}