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}