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}