001package jmri.jmrit.z21server; 002 003import jmri.DccThrottle; 004 005import java.net.InetAddress; 006import java.time.Duration; 007import java.util.Date; 008import java.util.HashMap; 009import java.beans.PropertyChangeListener; 010import java.beans.PropertyChangeEvent; 011 012import static jmri.jmrit.z21server.ClientManager.speedMultiplier; 013 014import java.util.List; 015import jmri.DccLocoAddress; 016import jmri.jmrit.roster.Roster; 017import jmri.jmrit.roster.RosterEntry; 018 019import org.slf4j.Logger; 020import org.slf4j.LoggerFactory; 021 022/** 023 * This class represents a connected and registered client, e.g. a Z21 app or a WlanMaus 024 * JMRI throttles a bound to this client. 025 * 026 * @author Jean-Yves Roda (C) 2023 027 * @author Eckart Meyer (C) 2025 (enhancements, WlanMaus support) 028 */ 029 030public class AppClient implements PropertyChangeListener { 031 032 private final static Logger log = LoggerFactory.getLogger(AppClient.class); 033 private final InetAddress address; 034 private final HashMap<Integer, DccThrottle> throttles; //list of throttles the client uses 035 private final PropertyChangeListener changeListener; //a throttle change event will be forwarded to this listener 036 private DccThrottle activeThrottle = null; //last modified throttle 037 private RosterEntry activeRosterEntry = null; //cached roster entry for activeThrottle 038 039 private Date timestamp; 040 041 private static final int packetLenght = 14; 042 043 044/** 045 * Constructor. 046 * 047 * @param address of the connected client 048 * @param changeListener to be called if one of the throttles has changed 049 */ 050 public AppClient(InetAddress address, PropertyChangeListener changeListener) { 051 this.address = address; 052 this.changeListener = changeListener; 053 throttles = new HashMap<>(); 054 heartbeat(); 055 } 056 057/** 058 * Add a throttle to the clients list of throttles. 059 * The throttle instance is created by the caller. 060 * 061 * @param locoAddress - the loco address 062 * @param throttle - the throttle to be added 063 */ 064 public void addThrottle(int locoAddress, DccThrottle throttle) { 065 if (!throttles.containsKey(locoAddress)) { 066 throttles.put(locoAddress, throttle); 067 throttle.addPropertyChangeListener(this); 068 } 069 log.trace("addThrottle: list: {}", throttles.keySet()); 070 } 071 072/** 073 * Get the last used throttle 074 * 075 * @return last used throttle 076 */ 077 public DccThrottle getActiveThrottle() { 078 return activeThrottle; 079 } 080 081/** 082 * Get the roster ID for the last used throttle 083 * 084 * @return roster ID as String 085 */ 086 public String getActiveRosterIdString() { 087 return (activeRosterEntry != null) ? activeRosterEntry.getId() : null; 088 } 089 090/** 091 * Set last used throttle 092 * 093 * @param t is the throttle 094 */ 095 public void setActiveThrottle(DccThrottle t) { 096 activeThrottle = t; 097 activeRosterEntry = findRosterEntry(t); 098 } 099 100/** 101 * Remove the listener from all throttles and clear the list of throttles. 102 */ 103 public void clear() { 104 log.trace("clear: list: {}", throttles.keySet()); 105 for (DccThrottle t: throttles.values()) { 106 t.removePropertyChangeListener(this); 107 } 108 throttles.clear(); 109 } 110 111/** 112 * Get a throttle by loco address 113 * 114 * @param locoAddress - the loco address 115 * @return the throttle 116 */ 117 public DccThrottle getThrottleFromLocoAddress(int locoAddress) { 118 if (throttles.containsKey(locoAddress)) { 119 return throttles.get(locoAddress); 120 } else { 121 return null; 122 } 123 } 124 125/** 126 * Get clients IP address 127 * 128 * @return the InetAddress 129 */ 130 public InetAddress getAddress() { 131 return address; 132 } 133 134/** 135 * The heartbeat for client expire 136 */ 137 public void heartbeat() { 138 timestamp = new Date(); 139 } 140 141/** 142 * Check if the client has not been seen (see heartbeat()) for at least 60 seconds. 143 * 144 * @return true if not seen for more than 60 seconds 145 */ 146 public boolean isTimestampExpired() { 147 Duration duration = Duration.between(timestamp.toInstant(), new Date().toInstant()); 148 log.trace("Duration without heartbeat: {}", duration); 149 return (duration.toSeconds() >= 60); 150 //return (duration.toSeconds() >= 5); //debug 151 /* Per Z21 Spec, clients are deemed lost after one minute of inactivity. */ 152 } 153 154/** 155 * Return a Z21 LAN_X_LOCO_INFO packet for a given loco address 156 * 157 * @param locoAddress - the loco address 158 * @return Z21 LAN_X_LOCO_INFO packet 159 */ 160 @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "PZLA_PREFER_ZERO_LENGTH_ARRAYS", 161 justification = "Messages can be of any length, null is used to indicate absence of message for caller") 162 public byte[] getLocoStatusMessage(Integer locoAddress) { 163 if (throttles.containsKey(locoAddress)) { 164 return buildLocoPacket(throttles.get(locoAddress)); 165 } else { 166 return null; 167 } 168 } 169 170/** 171 * Listener for throttle events. 172 * Will call the changeListener (in MainServer) with the Z21 LAN_X_LOCO_INFO packet as new value. 173 * 174 * @param pce - throttle change event 175 */ 176 @Override 177 public void propertyChange(PropertyChangeEvent pce) { 178 if (changeListener != null) { 179 log.trace("AppClient: Throttle change event: loco: {}, {}", ((DccThrottle)pce.getSource()).getLocoAddress(), pce); 180 changeListener.propertyChange(new PropertyChangeEvent(pce.getSource(), "throttle-change", null, buildLocoPacket( ((DccThrottle)pce.getSource()) ))); 181 } 182 } 183 184/** 185 * Find the roster entry from a given throttle instance. 186 * 187 * @param t - the throttle instance 188 * @return the roster entry 189 */ 190 public static RosterEntry findRosterEntry(DccThrottle t) { 191 RosterEntry re = null; 192 if (t.getLocoAddress() != null) { 193 List<RosterEntry> l = Roster.getDefault().matchingList(null, null, "" + ((DccLocoAddress) t.getLocoAddress()).getNumber(), null, null, null, null); 194 if (l.size() > 0) { 195 log.debug("Roster Loco found: {}", l.get(0).getDccAddress()); 196 re = l.get(0); 197 } 198 } 199 return re; 200 } 201 202/** 203 * Build a Z21 LAN_X_LOCO_INFO packet from a given throttle instance 204 * 205 * @param t - the throttle instance 206 * @return the Z21 LAN_X_LOCO_INFO packet 207 */ 208 private byte[] buildLocoPacket(DccThrottle t) { 209 byte[] locoPacket = new byte[packetLenght]; 210 211 // Header 212 locoPacket[0] = (byte) (7 + 7); 213 locoPacket[1] = (byte) 0x00; 214 locoPacket[2] = (byte) 0x40; 215 locoPacket[3] = (byte) 0x00; 216 locoPacket[4] = (byte) 0xEF; 217 // Loco address 218 int locoAddress = t.getLocoAddress().getNumber(); 219 locoPacket[5] = (byte) (locoAddress >> 8); 220 locoPacket[6] = (byte) locoAddress; 221 // set upper two bits of loco address MSB if loco address >= 128 222 // see Z21 spec. 223 if (locoAddress >= 128) { 224 locoPacket[5] |= 0xC0; 225 } 226 //Loco drive and speed data 227 locoPacket[7] = (byte) 0x04; 228 float speed = t.getSpeedSetting(); 229 int packetspeed = Math.round(speed / speedMultiplier); 230 if (speed < 0) packetspeed = 0; 231 if (packetspeed > 128) packetspeed = 128; 232 locoPacket[8] = (byte) ((t.getIsForward() ? (byte) 0x80 : 0) + ((byte) packetspeed)); 233 // Loco functions data 234 locoPacket[9] = (byte) ((byte) 235 (t.getFunction(0) ? 0x10 : 0) + 236 (t.getFunction(4) ? 0x08 : 0) + 237 (t.getFunction(3) ? 0x04 : 0) + 238 (t.getFunction(2) ? 0x02 : 0) + 239 (t.getFunction(1) ? 0x01 : 0) 240 ); 241 locoPacket[10] = (byte) ((byte) 242 (t.getFunction(12) ? 0x80 : 0) + 243 (t.getFunction(11) ? 0x40 : 0) + 244 (t.getFunction(10) ? 0x20 : 0) + 245 (t.getFunction(9) ? 0x10 : 0) + 246 (t.getFunction(8) ? 0x08 : 0) + 247 (t.getFunction(7) ? 0x04 : 0) + 248 (t.getFunction(6) ? 0x02 : 0) + 249 (t.getFunction(5) ? 0x01 : 0) 250 ); 251 locoPacket[11] = (byte) ((byte) 252 (t.getFunction(20) ? 0x80 : 0) + 253 (t.getFunction(19) ? 0x40 : 0) + 254 (t.getFunction(18) ? 0x20 : 0) + 255 (t.getFunction(17) ? 0x10 : 0) + 256 (t.getFunction(16) ? 0x08 : 0) + 257 (t.getFunction(15) ? 0x04 : 0) + 258 (t.getFunction(14) ? 0x02 : 0) + 259 (t.getFunction(13) ? 0x01 : 0) 260 ); 261 locoPacket[12] = (byte) ((byte) 262 (t.getFunction(28) ? 0x80 : 0) + 263 (t.getFunction(27) ? 0x40 : 0) + 264 (t.getFunction(26) ? 0x20 : 0) + 265 (t.getFunction(25) ? 0x10 : 0) + 266 (t.getFunction(24) ? 0x08 : 0) + 267 (t.getFunction(23) ? 0x04 : 0) + 268 (t.getFunction(22) ? 0x02 : 0) + 269 (t.getFunction(21) ? 0x01 : 0) 270 ); 271 locoPacket[13] = ClientManager.xor(locoPacket); 272 273 return locoPacket; 274 } 275 276 277}