001package jmri.jmrit.z21server; 002 003import java.beans.PropertyChangeEvent; 004import jmri.DccThrottle; 005import jmri.LocoAddress; 006import jmri.ThrottleListener; 007import jmri.Turnout; 008 009import org.slf4j.Logger; 010import org.slf4j.LoggerFactory; 011 012import java.net.InetAddress; 013import java.util.HashMap; 014import java.util.Iterator; 015import java.beans.PropertyChangeListener; 016 017/** 018 * Register and unregister clients, set loco throttle 019 * 020 * @author Jean-Yves Roda (C) 2023 021 * @author Eckart Meyer (C) 2025 (enhancements, WlanMaus support) 022 */ 023 024public class ClientManager implements ThrottleListener { 025 026 private static ClientManager instance; 027 private static final HashMap<InetAddress, AppClient> registeredClients = new HashMap<>(); 028 private static final HashMap<Integer, InetAddress> requestedThrottlesList = new HashMap<>(); //temporary store client InetAddress 029 private PropertyChangeListener changeListener = null; 030 private PropertyChangeListener clientListener = null; //the listener will be notified if a client is registered or unregistered 031 public static float speedMultiplier = 1.0f / 128.0f; 032 033 private final static Logger log = LoggerFactory.getLogger(ClientManager.class); 034 035 private ClientManager() { 036 } 037 038/** 039 * Return the one running instance of the client manager. 040 * If there is no instance, create it. 041 * 042 * @return the client manager instance 043 */ 044 synchronized public static ClientManager getInstance() { 045 if (instance == null) { 046 instance = new ClientManager(); 047 } 048 return instance; 049 } 050 051/** 052 * Set the throttle change listener. 053 * 054 * @param changeListener - the property change listener instance 055 */ 056 public void setChangeListener(PropertyChangeListener changeListener) { 057 this.changeListener = changeListener; 058 } 059 060/** 061 * Set the client change listener. 062 * The listener is called if a new is registered or a registered client is 063 * unregistered. 064 * 065 * @param clientListener - the property change listener instance 066 */ 067 public void setClientListener(PropertyChangeListener clientListener) { 068 this.clientListener = clientListener; 069 } 070 071/** 072 * Get a hash map of the registered clients, indexed by their InetAddress 073 * 074 * @return the hash map of registered clients 075 */ 076 public HashMap<InetAddress, AppClient> getRegisteredClients() { 077 return registeredClients; 078 } 079 080// Loco handling 081 082/** 083 * Register a client if not already registered and add a throttle for the given 084 * loco address to the clients list of throttles. 085 * 086 * @param clientAddress - InetAddress of the client 087 * @param locoAddress - address of a loco 088 */ 089 synchronized public void registerLocoIfNeeded(InetAddress clientAddress, int locoAddress) { 090 if (!registeredClients.containsKey(clientAddress)) { 091 AppClient client = new AppClient(clientAddress, changeListener); 092 registeredClients.put(clientAddress, client); 093 if (clientListener != null) { 094 clientListener.propertyChange(new PropertyChangeEvent(this, "client-registered", null, null)); 095 } 096 } 097 if (registeredClients.get(clientAddress).getThrottleFromLocoAddress(locoAddress) == null) { 098 // save loco address and client address temporary, so that notifyThrottleFound() knows the client for the Throttle 099 requestedThrottlesList.put(locoAddress, clientAddress); 100 jmri.InstanceManager.throttleManagerInstance().requestThrottle(locoAddress, ClientManager.getInstance()); //results in notifyThrottleFound() (hopefully) 101 } 102 } 103 104/** 105 * Set a JMRI throttle to new speed and direction. 106 * Called when a Z21 client's user changes speed and/or direction. 107 * 108 * @param clientAddress - the client's InetAddress 109 * @param locoAddress - the loco address 110 * @param speed - the speed to set 111 * @param forward - true of forward, false if reverse 112 */ 113 synchronized public void setLocoSpeedAndDirection(InetAddress clientAddress, int locoAddress, int speed, boolean forward) { 114 AppClient client = registeredClients.get(clientAddress); 115 if (client != null) { 116 DccThrottle throttle = client.getThrottleFromLocoAddress(locoAddress); 117 if (throttle != null) { 118 if (throttle.getIsForward() != forward) throttle.setIsForward(forward); 119 throttle.setSpeedSetting(speed * speedMultiplier); 120 setActiveThrottle(client, throttle); 121 } else { 122 log.info("Unable to find throttle for loco {} from client {}", locoAddress, clientAddress); 123 } 124 } else { 125 log.info("App client {} is not registered", clientAddress); 126 } 127 } 128 129/** 130 * Set a JMRI throttle to new function state. 131 * Called when a Z21 client's user changes function status. 132 * 133 * @param clientAddress - the client's InetAddress 134 * @param locoAddress - the loco address 135 * @param functionNumber - the function number to set 136 * @param functionState - the new state of the function 137 */ 138 synchronized public void setLocoFunction(InetAddress clientAddress, int locoAddress, int functionNumber, int functionState) { 139 AppClient client = registeredClients.get(clientAddress); 140 if (client != null) { 141 DccThrottle throttle = client.getThrottleFromLocoAddress(locoAddress); 142 if (throttle != null) { 143 boolean bOn = (functionState & 0x01) == 0x01; 144 if ( (functionState & 0x03) == 0x02) { 145 log.trace("Toggle! old state: {}", throttle.getFunction(functionNumber)); 146 bOn = !throttle.getFunction(functionNumber); 147 } 148 log.trace("set function {} to value: {}", functionNumber, bOn); 149 throttle.setFunction(functionNumber, bOn); 150 setActiveThrottle(client, throttle); 151 } else { 152 log.info("Unable to find throttle for loco {} from client {}", locoAddress, clientAddress); 153 } 154 } else { 155 log.info("App client {} is not registered", clientAddress); 156 } 157 } 158 159/** 160 * Return a Z21 LAN_X_LOCO_INFO packet for a given client and loco address 161 * 162 * @param address - client InetAddress 163 * @param locoAddress - the loco address 164 * @return Z21 LAN_X_LOCO_INFO packet 165 */ 166 @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "PZLA_PREFER_ZERO_LENGTH_ARRAYS", 167 justification = "Messages can be of any length, null is used to indicate absence of message for caller") 168 synchronized public byte[] getLocoStatusMessage(InetAddress address, Integer locoAddress) { 169 if (registeredClients.containsKey(address)) { 170 AppClient client = registeredClients.get(address); 171 return client.getLocoStatusMessage(locoAddress); 172 } else { 173 return null; 174 } 175 } 176 177/** 178 * Set the active (last used) throttle of a client. 179 * 180 * @param client - the client's AppClient instance 181 * @param throttle - the throttle instance 182 */ 183 private void setActiveThrottle(AppClient client, DccThrottle throttle) { 184 if (client.getActiveThrottle() != throttle) { 185 client.setActiveThrottle(throttle); 186 if (clientListener != null) { 187 clientListener.propertyChange(new PropertyChangeEvent(this, "active-throttle", null, null)); 188 } 189 } 190 } 191 192// Turnout handling 193 194/** 195 * Set a JMRI component to new state. 196 * The component may be a JMRI turnout, light, route, signal mast, signal head or sensor, 197 * depending on a property entry for the component containing the Z21 turnout number. 198 * 199 * Called when a Z21 client's user changes state of a turnout. 200 * 201 * @param clientAddress - client's InetAddress 202 * @param turnoutNumber - the Z21 turnout number, starting from 1 as seen on the WlanMaus display (in the Z21 protocol turnouts start with 0). 203 * @param state - true if turnout should be THROWN, false if CLOSED. 204 */ 205 synchronized public void setTurnout(InetAddress clientAddress, int turnoutNumber, boolean state) { 206 // state: 207 // false: set turnout closed 208 // true: set turnout thrown 209 int turnoutState = state ? Turnout.THROWN : Turnout.CLOSED; 210 TurnoutNumberMapHandler.getInstance().setStateForNumber(turnoutNumber + 1, turnoutState); 211 } 212 213/** 214 * Get a Z21 LAN_X_TURNOUT_INFO packet to be sent to the client fpr a given turnout number. 215 * 216 * @param address - client's InetAdress 217 * @param turnoutNumber - the Z21 Turnout Number 218 * @return a Z21 LAN_X_TURNOUT_INFO packet 219 */ 220 @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "PZLA_PREFER_ZERO_LENGTH_ARRAYS", 221 justification = "Messages can be of any length, null is used to indicate absence of message for caller") 222 synchronized public byte[] getTurnoutStatusMessage(InetAddress address, Integer turnoutNumber) { 223 int state = TurnoutNumberMapHandler.getInstance().getStateForNumber(turnoutNumber + 1); 224 if (state >= 0) { 225 // return LAN_X_TURNOUT_INFO packet 226 // state in byte 7, bits 0 and 1 - WlanMaus displays the state according to byte 7 227 // 0x02 - turnout closed (straight, main line) 228 // 0x01 - turnout thrown (diverging line) 229 // 0x00 - unknown (WlanMaus displays both legs in the turnout symbol) 230 byte[] turnoutPacket = new byte[9]; 231 turnoutPacket[0] = (byte) 0x09; 232 turnoutPacket[1] = (byte) 0x00; 233 turnoutPacket[2] = (byte) 0x40; 234 turnoutPacket[3] = (byte) 0x00; 235 turnoutPacket[4] = (byte) 0x43; 236 turnoutPacket[5] = (byte) (turnoutNumber >> 8); //MSB 237 turnoutPacket[6] = (byte) (turnoutNumber & 0xFF); //LSB 238 turnoutPacket[7] = (byte) 0x00; //preset UNKNOWN 239 if (state == Turnout.CLOSED) { 240 turnoutPacket[7] = (byte) 0x02; 241 } 242 if (state == Turnout.THROWN) { 243 turnoutPacket[7] = (byte) 0x01; 244 } 245 turnoutPacket[8] = ClientManager.xor(turnoutPacket); 246 return turnoutPacket; 247 } 248 return null; 249 } 250 251// client handling 252 253/** 254 * Send a heartbeat() to the AppClient instance. 255 * 256 * @param clientAddress - the client's InetAdress 257 */ 258 synchronized public void heartbeat(InetAddress clientAddress) { 259 AppClient client = registeredClients.get(clientAddress); 260 if (client != null) client.heartbeat(); 261 } 262 263 /** 264 * Check all clients if they have not sent anything for a time peroid (60 seconds). 265 * If the client has expired, remove it from the list. 266 * 267 * @param removeAll - if true, remove all clients regardless of their expiry time. 268 */ 269 synchronized public void handleExpiredClients(boolean removeAll) { 270 HashMap<InetAddress, AppClient> tempMap = new HashMap<>(registeredClients); // to avoid concurrent modification 271 for (AppClient c : tempMap.values()) { 272 if (c.isTimestampExpired() || removeAll) { 273 log.debug("Remove expired client [{}]",c.getAddress()); 274 unregisterClient(c.getAddress()); 275 } 276 } 277 } 278 279/** 280 * Unregister a client. 281 * Clean up the AppClient instance to remove listeners from throttles, 282 * Remove client from hash map, 283 * Call client listener to inform about removing the client 284 * 285 * @param clientAddress - client's InetAddress 286 */ 287 synchronized public void unregisterClient(InetAddress clientAddress) { 288 log.info("Remove client [{}]", clientAddress); 289 if (registeredClients.containsKey(clientAddress)) { 290 registeredClients.get(clientAddress).clear(); 291 } 292 registeredClients.remove(clientAddress); 293 if (clientListener != null) { 294 clientListener.propertyChange(new PropertyChangeEvent(this, "client-unregistered", null, null)); 295 } 296 297 // the list should definitly be empty, so just in case... 298 for (Iterator<HashMap.Entry<Integer, InetAddress>> it = requestedThrottlesList.entrySet().iterator(); it.hasNext(); ) { 299 HashMap.Entry<Integer, InetAddress> e = it.next(); 300 if (e.getValue().equals(clientAddress)) { 301 log.error("The list requestedThrottlesList should be empty, but is not. Remove {}", e); 302 it.remove(); 303 } 304 } 305 } 306 307// ThrottleListener implementation 308 309/** 310 * Called from the throttle manager when a requested throttle for a given loco address was found. 311 * The thottle is then added to the list of throttles in the AppClient instance. 312 * 313 * @param t - the (new) throttle bound to the loco. 314 */ 315 @Override 316 synchronized public void notifyThrottleFound(DccThrottle t) { 317 int locoAddress = t.getLocoAddress().getNumber(); 318 // add the new throttle to the AppClient instance, which is identified by the clients InetAddress 319 InetAddress client = requestedThrottlesList.get(locoAddress); 320 if (client != null) { 321 registeredClients.get(client).addThrottle(locoAddress, t); 322 requestedThrottlesList.remove(locoAddress); //not needed any more, remove entry 323 } 324 } 325 326/** 327 * Called from the throttle manager when no throttle can be created for a loco address. 328 * 329 * @param address - loco address 330 * @param reason - a message from the throttle manager 331 */ 332 @Override 333 synchronized public void notifyFailedThrottleRequest(LocoAddress address, String reason) { 334 log.info("Unable to get Throttle for loco address {}, reason : {}", address.getNumber(), reason); 335 requestedThrottlesList.remove(address.getNumber()); 336 } 337 338/** 339 * Called from the throttle manager to ask if the throttle should be shared or the previous should be disconnected. 340 * For now, we always use shared throttles. 341 * 342 * @param address - loco address 343 * @param question - STEAL, SHARE or both 344 */ 345 @Override 346 synchronized public void notifyDecisionRequired(LocoAddress address, DecisionType question) { 347 jmri.InstanceManager.throttleManagerInstance().responseThrottleDecision(address, ClientManager.getInstance(), ThrottleListener.DecisionType.SHARE); 348 } 349 350 351/** 352 * Helper to construct the Z21 protocol XOR byte 353 * 354 * @param packet - Z21 packet 355 * @return the XOR byte 356 */ 357 public static byte xor(byte[] packet) { 358 byte xor = (byte) (packet[0] ^ packet[1]); 359 for (int i = 2; i < (packet.length - 1); i++) { 360 xor = (byte) (xor ^ packet[i]); 361 } 362 return xor; 363 } 364 365}