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