001package jmri.jmrit.z21server;
002
003import org.slf4j.Logger;
004import org.slf4j.LoggerFactory;
005
006import java.net.InetAddress;
007import java.util.Arrays;
008import java.beans.PropertyChangeListener;
009import java.beans.PropertyChangeEvent;
010import java.util.Iterator;
011
012import jmri.InstanceManager;
013import jmri.JmriException;
014import jmri.PowerManager;
015import jmri.jmrit.throttle.ThrottleFrame;
016import jmri.jmrit.throttle.ThrottleFrameManager;
017import jmri.DccThrottle;
018
019/**
020 * Handle X-BUS Protokoll (header type 0x40).
021 * Only function to handle a loco throttle have been implemented.
022 * 
023 * @author Jean-Yves Roda (C) 2023
024 * @author Eckart Meyer (C) 2025 (enhancements, WlanMaus support)
025 */
026
027public class Service40 {
028    private static final String moduleIdent = "[Service 40] ";
029    private static PropertyChangeListener changeListener = null;
030
031    private final static Logger log = LoggerFactory.getLogger(Service40.class);
032
033/**
034 * Set a listener to be called on track power manager events.
035 * The listener is called with the Z21 LAN_X_BC_TRACK_POWER_ON/OFF packet to
036 * be sent to the client.
037 * 
038 * Note that throttle changes are handled in the AppClient class.
039 * 
040 * @param cl - listener class
041 */
042    public static void setChangeListener(PropertyChangeListener cl) {
043        changeListener = cl;
044        PowerManager powerMgr = InstanceManager.getNullableDefault(PowerManager.class);
045        if (powerMgr != null) {
046            powerMgr.addPropertyChangeListener( (PropertyChangeEvent pce) -> {
047                if (changeListener != null) {
048                    log.trace("Service40: power change event: {}", pce);
049                    changeListener.propertyChange(new PropertyChangeEvent(pce.getSource(), "trackpower-change", null, buildTrackPowerPacket()));
050                }
051            });
052        }
053    }
054
055/**
056 * Handle a X-Bus command.
057 * 
058 * @param data - the Z21 packet bytes without data length and header.
059 * @param clientAddress - the sending client's InetAddress
060 * @return a response packet to be sent to the client or null if nothing is to sent (yet).
061 */
062    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "PZLA_PREFER_ZERO_LENGTH_ARRAYS",
063    justification = "Messages can be of any length, null is used to indicate absence of message for caller")
064    public static byte[] handleService(byte[] data, InetAddress clientAddress) {
065        int command = data[0];
066        switch (command){
067            case (byte)0x21:
068                return handleHeader21(data[1]);
069            case (byte)0xE3:
070                return handleHeaderE3(Arrays.copyOfRange(data, 1, 4), clientAddress);
071            case (byte)0xE4:
072                return handleHeaderE4(Arrays.copyOfRange(data, 1, 5), clientAddress);
073            case (byte)0x43:
074                return handleHeader43(Arrays.copyOfRange(data, 1, 3), clientAddress);
075            case (byte)0x53:
076                return handleHeader53(Arrays.copyOfRange(data, 1, 4), clientAddress);
077            case (byte)0x80:
078                return handleHeader80();
079            case (byte)0xF1:
080                return handleHeaderF1();
081            default:
082                log.debug("{} Header {} not yet supported", moduleIdent, Integer.toHexString(command & 0xFF));
083                break;
084        }
085        return null;
086    }
087
088/**
089 * Handle a LAN_X_GET_* commands.
090 * 
091 * @param db0 - X-Bus subcommand
092 * @return a response packet to be sent to the client or null if nothing is to sent (yet).
093 */
094    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "PZLA_PREFER_ZERO_LENGTH_ARRAYS",
095    justification = "Messages can be of any length, null is used to indicate absence of message for caller")
096    private static byte[] handleHeader21(int db0){
097        switch (db0){
098            case 0x21:
099                // Get z21 version
100                break;
101            case 0x24:
102                // Get z21 status
103                byte[] answer = new byte[8];
104                answer[0] = (byte) 0x08;
105                answer[1] = (byte) 0x00;
106                answer[2] = (byte) 0x40;
107                answer[3] = (byte) 0x00;
108                answer[4] = (byte) 0x62;
109                answer[5] = (byte) 0x22;
110                answer[6] = (byte) 0x00;
111                PowerManager powerMgr = InstanceManager.getNullableDefault(PowerManager.class);
112                if (powerMgr != null) {
113                    if (powerMgr.getPower() != PowerManager.ON) {
114                        answer[6] |= 0x02;
115                    }
116                }
117                answer[7] = ClientManager.xor(answer);
118                return answer;
119            case (byte) 0x80:
120                log.info("{} Set track power to off", moduleIdent);
121                return setTrackPower(false);
122            case (byte) 0x81:
123                log.info("{} Set track power to on", moduleIdent);
124                return setTrackPower(true);
125            default:
126                break;
127        }
128        return null;
129    }
130    
131/**
132 * Set track power on to JMRI.
133 * 
134 * @param state - true to switch ON, false to switch OFF
135 * @return a response packet to be sent to the client or null if nothing is to sent (yet).
136 */
137    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "PZLA_PREFER_ZERO_LENGTH_ARRAYS",
138    justification = "Messages can be of any length, null is used to indicate absence of message for caller")
139    private static byte[] setTrackPower(boolean state) {
140        PowerManager powerMgr = InstanceManager.getNullableDefault(PowerManager.class);
141        if (powerMgr != null) {
142            try {
143                powerMgr.setPower(state ? PowerManager.ON : PowerManager.OFF);
144            } catch (JmriException ex) {
145                log.error("Cannot set power from z21");
146                return buildTrackPowerPacket(); //return power off
147            }
148        }
149        // response packet is sent from the property change event
150        //return buildTrackPowerPacket();
151        return null;
152    }
153
154/**
155 * Build a LAN_X_BC_TRACK_POWER_ON or LAN_X_BC_TRACK_POWER_OFF packet.
156 * @return the packet
157 */
158    private static byte[] buildTrackPowerPacket() {
159        // LAN_X_BC_TRACK_POWER_ON/OFF
160        byte[] trackPowerPacket =  new byte[7];
161        trackPowerPacket[0] = (byte) 0x07;
162        trackPowerPacket[1] = (byte) 0x00;
163        trackPowerPacket[2] = (byte) 0x40;
164        trackPowerPacket[3] = (byte) 0x00;
165        trackPowerPacket[4] = (byte) 0x61;
166        trackPowerPacket[5] = (byte) 0x00; //preset power off
167        PowerManager powerMgr = InstanceManager.getNullableDefault(PowerManager.class);
168        if (powerMgr != null) {
169            trackPowerPacket[5] = (byte) (powerMgr.getPower() == PowerManager.ON ? 0x01 : 0x00);
170        }
171        trackPowerPacket[6] = ClientManager.xor(trackPowerPacket);
172        return trackPowerPacket;
173    }
174
175    
176/**
177 * Handle a LAN_X_GET_LOCO_INFO command
178 * 
179 * @param data - the Z21 packet bytes without data length, header and X-header
180 * @param clientAddress - the sending client's InetAddress
181 * @return a response packet to be sent to the client or null if nothing is to sent (yet).
182 */
183    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "PZLA_PREFER_ZERO_LENGTH_ARRAYS",
184    justification = "Messages can be of any length, null is used to indicate absence of message for caller")
185    private static byte[] handleHeaderE3(byte[] data, InetAddress clientAddress) {
186        int db0 = data[0];
187        if (db0 == (byte)0xF0) {
188            // Get loco status command
189            int locomotiveAddress = (((data[1] & 0xFF) & 0x3F) << 8) + (data[2] & 0xFF);
190            log.debug("{} Get loco no {} status", moduleIdent, locomotiveAddress);
191
192            ClientManager.getInstance().registerLocoIfNeeded(clientAddress, locomotiveAddress);
193
194            return ClientManager.getInstance().getLocoStatusMessage(clientAddress, locomotiveAddress);
195
196        } else {
197            log.debug("{} Header E3 with function {} is not supported", moduleIdent,  Integer.toHexString(db0));
198        }
199        return null;
200    }
201
202/**
203 * Handle LAN_X_SET_LOCO_* commands
204 * 
205 * @param data - the Z21 packet bytes without data length, header and X-header
206 * @param clientAddress - the sending client's InetAddress
207 * @return a response packet to be sent to the client or null if nothing is to sent (yet).
208 */
209    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "PZLA_PREFER_ZERO_LENGTH_ARRAYS",
210    justification = "Messages can be of any length, null is used to indicate absence of message for caller")
211    private static byte[] handleHeaderE4(byte[] data, InetAddress clientAddress) {
212        if (data[0] == 0x13) {
213            // handle LAN_X_SET_LOCO_DRIVE - 128 steps only, others are not supported
214            int locomotiveAddress = (((data[1] & 0xFF) & 0x3F) << 8) + (data[2] & 0xFF);
215            int rawSpeedData = data[3] & 0xFF;
216            boolean bForward = ((rawSpeedData & 0x80) >> 7) == 1;
217            int actualSpeed = rawSpeedData & 0x7F;
218            log.debug("Set loco no {} direction {} with speed {}",locomotiveAddress, (bForward ? "FWD" : "RWD"), actualSpeed);
219
220            ClientManager.getInstance().setLocoSpeedAndDirection(clientAddress, locomotiveAddress, actualSpeed, bForward);
221
222            // response packet is sent from the property change event
223            //return ClientManager.getInstance().getLocoStatusMessage(clientAddress, locomotiveAddress);
224        }
225        else if (data[0] == (byte)0xF8) {
226            // handle LAN_X_SET_LOCO_FUNCTION
227            int locomotiveAddress = (((data[1] & 0xFF) & 0x3F) << 8) + (data[2] & 0xFF);
228            // function switch type: 0x00 = OFF, 0x01 = ON, 0x20 = TOGGLE
229            // Z21 app always sends ON or OFF, WLANmaus always TOGGLE
230            // TOGGLE is done in clientManager.setLocoFunction().
231            int functionSwitchType = ((data[3] & 0xFF) & 0xC0) >> 6;
232            int functionNumber = (data[3] & 0xFF) & 0x3F;
233            if (log.isDebugEnabled()) {
234                String cmd = ((functionSwitchType & 0x01) == 0x01) ? "ON" : "OFF";
235                if ((functionSwitchType & 0x03) == 0x02) {
236                    cmd = "TOGGLE";
237                }
238                log.debug("Set loco no {} function no {}: {}", locomotiveAddress, functionNumber, cmd);
239            }
240
241            ClientManager.getInstance().setLocoFunction(clientAddress, locomotiveAddress, functionNumber, functionSwitchType);
242
243            // response packet is sent from the property change event
244            //return ClientManager.getInstance().getLocoStatusMessage(clientAddress, locomotiveAddress);
245        }
246        return null;
247    }
248    
249/**
250 * Handle LAN_X_GET_TURNOUT_INFO command.
251 * Note: JMRI has no concept of turnout numbers as with the Z21 protocol.
252 * So this would only work of mapping tables have already been set by user.
253 * 
254 * @param data - the Z21 packet bytes without data length, header and X-header
255 * @param clientAddress - the sending client's InetAddress
256 * @return a response packet to be sent to the client or null if nothing is to sent (yet).
257 */
258    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "PZLA_PREFER_ZERO_LENGTH_ARRAYS",
259    justification = "Messages can be of any length, null is used to indicate absence of message for caller")
260    private static byte[] handleHeader43(byte[] data, InetAddress clientAddress) {
261        // Get turnout status command
262        int turnoutNumber = ((data[0] & 0xFF) << 8) + (data[1] & 0xFF);
263        log.debug("{} Get turnout no {} status", moduleIdent, turnoutNumber);
264        return ClientManager.getInstance().getTurnoutStatusMessage(clientAddress, turnoutNumber);
265    }
266
267/**
268 * Handle LAN_X_SET_TURNOUT command.
269 * Note: JMRI has no concept of turnout numbers as with the Z21 protocol.
270 * So this would only work of mapping tables have already been set by user.
271 * 
272 * @param data - the Z21 packet bytes without data length, header and X-header
273 * @param clientAddress - the sending client's InetAddress
274 * @return a response packet to be sent to the client or null if nothing is to sent (yet).
275 */
276    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "PZLA_PREFER_ZERO_LENGTH_ARRAYS",
277    justification = "Messages can be of any length, null is used to indicate absence of message for caller")
278    private static byte[] handleHeader53(byte[] data, InetAddress clientAddress) {
279        // Set turnout
280        // WlanMaus sends in bit 0 of data[2]:
281        // 0x00 - Turnout thrown button pressed (diverging, unstraight, not main line)
282        // 0x01 / Turnout closed button pressed (straight, main line)
283        int turnoutNumber = ((data[0] & 0xFF) << 8) + (data[1] & 0xFF);
284        log.debug("{} Set turnout no {} to state {}", moduleIdent, turnoutNumber, data[2] & 0xFF);
285        if ( (data[2] & 0x08) == 0x08) { //only use "activation", ignore "deactivation"
286            ClientManager.getInstance().setTurnout(clientAddress, turnoutNumber, (data[2] & 0x1) == 0x00);
287        }
288        return ClientManager.getInstance().getTurnoutStatusMessage(clientAddress, turnoutNumber);
289    }
290
291/**
292 * Handle LAN_X_SET_STOP command.
293 * Stop the locos for all throttles found in JMRI.
294 * 
295 * @return a response packet to be sent to the client or null if nothing is to sent (yet).
296 */
297    private static byte[] handleHeader80() {
298        log.info("{} Stop all locos", moduleIdent);
299        Iterator<ThrottleFrame> tpi = InstanceManager.getDefault(ThrottleFrameManager.class).getThrottlesListPanel().getTableModel().iterator();
300        while (tpi.hasNext()) {
301            DccThrottle t = tpi.next().getAddressPanel().getThrottle();
302            if (t != null) {
303                t.setSpeedSetting(-1);
304            }
305        }
306        // send LAN_X_BC_STOPPED packet
307        byte[] stoppedPacket =  new byte[7];
308        stoppedPacket[0] = (byte) 0x07;
309        stoppedPacket[1] = (byte) 0x00;
310        stoppedPacket[2] = (byte) 0x40;
311        stoppedPacket[3] = (byte) 0x00;
312        stoppedPacket[4] = (byte) 0x81;
313        stoppedPacket[5] = (byte) 0x00;
314        stoppedPacket[6] = ClientManager.xor(stoppedPacket);
315        return stoppedPacket;
316    }
317
318/**
319 * Handle LAN_X_GET_FIRMWARE_VERSION command.
320 * Of course, since we are not a Z21 command station, the version number
321 * does not make sense. But for the case that the client behaves different
322 * for Z21 command station software version, we just return the
323 * currently newest version 1.43 (January 2025).
324 * 
325 * @return a response packet to be sent to the client or null if nothing is to sent (yet).
326 */
327    private static byte[] handleHeaderF1() {
328        log.info("{} Get Firmware Version", moduleIdent);
329        
330        // send Firmware Version Packet - always return 1.43
331        byte[] fwVersionPacket =  new byte[9];
332        fwVersionPacket[0] = (byte) 0x09;
333        fwVersionPacket[1] = (byte) 0x00;
334        fwVersionPacket[2] = (byte) 0x40;
335        fwVersionPacket[3] = (byte) 0x00;
336        fwVersionPacket[4] = (byte) 0xF3;
337        fwVersionPacket[5] = (byte) 0x0A;
338        fwVersionPacket[6] = (byte) 0x01;
339        fwVersionPacket[7] = (byte) 0x43;
340        fwVersionPacket[8] = ClientManager.xor(fwVersionPacket);
341        return fwVersionPacket;
342    }
343}