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}