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}