001package jmri.jmrit.withrottle; 002 003import java.beans.PropertyChangeEvent; 004 005import jmri.*; 006import jmri.jmrit.roster.RosterEntry; 007import jmri.jmrit.throttle.ThrottlesPreferences; 008 009/** 010 * @author Brett Hoffman Copyright (C) 2011 011 */ 012public class MultiThrottleController extends ThrottleController { 013 014 protected boolean isStealAddress; 015 016 public MultiThrottleController(char id, String key, ThrottleControllerListener tcl, ControllerInterface ci) { 017 super(id, tcl, ci); 018 log.debug("New MT controller"); 019 locoKey = key; 020 isStealAddress = false; 021 } 022 023 /** 024 * Builds a header to send to the wi-fi device for use in a message. 025 * Includes a separator - {@literal <;>} 026 * 027 * @param chr the character indicating what action is performed 028 * @return a pre-assembled header for this DccThrottle 029 */ 030 public String buildPacketWithChar(char chr) { 031 return ("M" + whichThrottle + chr + locoKey + "<;>"); 032 } 033 034 035 /* 036 * Send a message to the wi-fi device that a bound property of a DccThrottle 037 * has changed. Currently only handles function state. 038 * Current Format: Header + F(0 or 1) + function number 039 * 040 * Event may be from regular throttle or consist throttle, but is handled the same. 041 * 042 * Bound params: SpeedSteps, IsForward, SpeedSetting, F##, F##Momentary 043 */ 044 @Override 045 public void propertyChange(PropertyChangeEvent event) { 046 String eventName = event.getPropertyName(); 047 log.debug("property change: {}",eventName); 048 if (eventName.startsWith("F")) { 049 if (eventName.contains("Momentary")) { 050 return; 051 } 052 StringBuilder message = new StringBuilder(buildPacketWithChar('A')); 053 054 try { 055 if ((Boolean) event.getNewValue()) { 056 message.append("F1"); 057 } else { 058 message.append("F0"); 059 } 060 message.append(eventName.substring(1)); 061 } catch (ClassCastException cce) { 062 log.debug("Invalid event value. {}", cce.getMessage()); 063 } catch (IndexOutOfBoundsException oob) { 064 log.debug("Invalid event name. {}", oob.getMessage()); 065 } 066 067 for (ControllerInterface listener : controllerListeners) { 068 listener.sendPacketToDevice(message.toString()); 069 } 070 } 071 if (eventName.matches(Throttle.SPEEDSTEPS)) { 072 StringBuilder message = new StringBuilder(buildPacketWithChar('A')); 073 message.append("s"); 074 message.append(encodeSpeedStepMode((SpeedStepMode)event.getNewValue())); 075 for (ControllerInterface listener : controllerListeners) { 076 listener.sendPacketToDevice(message.toString()); 077 } 078 } 079 if (eventName.matches(Throttle.ISFORWARD)) { 080 StringBuilder message = new StringBuilder(buildPacketWithChar('A')); 081 message.append("R"); 082 message.append((Boolean) event.getNewValue() ? "1" : "0"); 083 for (ControllerInterface listener : controllerListeners) { 084 listener.sendPacketToDevice(message.toString()); 085 } 086 } 087 if (eventName.matches(Throttle.SPEEDSETTING)) { 088 float currentSpeed = ((Float) event.getNewValue()).floatValue(); 089 log.debug("Speed Setting: {} head of queue {}",currentSpeed, lastSentSpeed.peek()); 090 if(lastSentSpeed.isEmpty()) { 091 StringBuilder message = new StringBuilder(buildPacketWithChar('A')); 092 message.append("V"); 093 message.append(Math.round(currentSpeed / speedMultiplier)); 094 for (ControllerInterface listener : controllerListeners) { 095 listener.sendPacketToDevice(message.toString()); 096 } 097 } else { 098 if( Math.abs(lastSentSpeed.peek().floatValue()-currentSpeed)<0.0005 ) { 099 Float f = lastSentSpeed.poll(); // remove the value from the list. 100 log.debug("removed value {} from queue",f); 101 } 102 } 103 } 104 } 105 106 /** 107 * This replaces the previous method of sending a string of function labels. 108 * 109 * Checks for labels across all possible functions of this roster entry. 110 * 111 * Example: 112 * {@code MTLL1234<;>]\[Light]\[Bell]\[Horn]\[]\[]\[]\[]\[]\[Mute]\[]\[]\[]\[} etc. 113 */ 114 @Override 115 public void sendFunctionLabels(RosterEntry re) { 116 117 if (re != null) { 118 StringBuilder functionString = new StringBuilder(buildPacketWithChar('L')); 119 120 int i; 121 for (i = 0; i < (re.getMaxFnNumAsInt()+1); i++) { 122 functionString.append("]\\["); 123 if ((re.getFunctionLabel(i) != null)) { 124 functionString.append(re.getFunctionLabel(i)); 125 } 126 } 127 for (ControllerInterface listener : controllerListeners) { 128 listener.sendPacketToDevice(functionString.toString()); 129 } 130 } 131 } 132 133 /** 134 * This replaces the previous method of sending a string of function states, 135 * and now sends them individually, the same as a property change would. 136 * 137 * @param t the throttle to send the states of. 138 */ 139 @Override 140 public void sendAllFunctionStates(DccThrottle t) { 141 log.debug("Sending state of all functions"); 142 for (int cnt = 0; cnt < t.getFunctions().length; cnt++) { 143 StringBuilder message = new StringBuilder(buildPacketWithChar('A')); 144 message.append( t.getFunction(cnt) ? "F1" : "F0" ); 145 message.append(cnt); 146 controllerListeners.forEach(listener -> { 147 listener.sendPacketToDevice(message.toString()); 148 }); 149 } 150 } 151 152 /** 153 * {@inheritDoc} 154 */ 155 @Override 156 synchronized protected void sendCurrentSpeed(DccThrottle t) { 157 float currentSpeed = t.getSpeedSetting(); 158 StringBuilder message = new StringBuilder(buildPacketWithChar('A')); 159 message.append("V"); 160 int outSpeed = Math.round(currentSpeed / speedMultiplier); 161 if(currentSpeed < 0) { 162 outSpeed = -126; // ensure estop is not rounded to zero 163 } 164 if(currentSpeed > 0 && outSpeed == 0) { 165 outSpeed = 1; // ensure non-zero throttle speed is sent 166 // as non-zero speed to wiThrottle 167 } 168 message.append(outSpeed); 169 for (ControllerInterface listener : controllerListeners) { 170 listener.sendPacketToDevice(message.toString()); 171 } 172 } 173 174 /** 175 * {@inheritDoc} 176 */ 177 @Override 178 protected void sendCurrentDirection(DccThrottle t) { 179 StringBuilder message = new StringBuilder(buildPacketWithChar('A')); 180 message.append("R"); 181 message.append(t.getIsForward() ? "1" : "0"); 182 for (ControllerInterface listener : controllerListeners) { 183 listener.sendPacketToDevice(message.toString()); 184 } 185 } 186 187 /** 188 * {@inheritDoc} 189 */ 190 @Override 191 protected void sendSpeedStepMode(DccThrottle t) { 192 StringBuilder message = new StringBuilder(buildPacketWithChar('A')); 193 message.append("s"); 194 message.append(encodeSpeedStepMode(throttle.getSpeedStepMode())); 195 for (ControllerInterface listener : controllerListeners) { 196 listener.sendPacketToDevice(message.toString()); 197 } 198 } 199 200 /** 201 * {@inheritDoc} 202 */ 203 @Override 204 protected void sendAllMomentaryStates(DccThrottle t) { 205 log.debug("Sending momentary state of all functions"); 206 for (int cnt = 0; cnt < t.getFunctionsMomentary().length; cnt++) { 207 StringBuilder message = new StringBuilder(buildPacketWithChar('A')); 208 message.append( t.getFunctionMomentary(cnt) ? "m1" : "m0" ); 209 message.append(cnt); 210 controllerListeners.forEach(listener -> { 211 listener.sendPacketToDevice(message.toString()); 212 }); 213 } 214 } 215 216 /** 217 * {@inheritDoc} A + indicates the address was acquired, - indicates 218 * released 219 */ 220 @Override 221 public void sendAddress() { 222 for (ControllerInterface listener : controllerListeners) { 223 if (isAddressSet) { 224 listener.sendPacketToDevice(buildPacketWithChar('+')); 225 } else { 226 listener.sendPacketToDevice(buildPacketWithChar('-')); 227 } 228 } 229 } 230 231 /** 232 * Send a message to a device that steal is needed. This message can be sent 233 * back to JMRI verbatim to complete a steal. 234 */ 235 public void sendStealAddress() { 236 StringBuilder message = new StringBuilder(buildPacketWithChar('S')); 237 message.append(locoKey); 238 for (ControllerInterface listener : controllerListeners) { 239 listener.sendPacketToDevice(message.toString()); 240 } 241 } 242 243 /** 244 * A decision is required for Throttle creation to continue. 245 * <p> 246 * Steal / Cancel, Share / Cancel, or Steal / Share Cancel 247 * <p> 248 * Callback of a request for an address that is in use. 249 * Will initiate a steal only if this MTC is flagged to do so. 250 * Otherwise, it will remove the request for the address. 251 * 252 * {@inheritDoc} 253 */ 254 @Override 255 public void notifyDecisionRequired(LocoAddress address, DecisionType question) { 256 log.debug("Hardware needs a {} decision for {}", question, address); 257 if ( question == DecisionType.STEAL ){ 258 if (isStealAddress) { 259 // Address is now staged in ThrottleManager and has been requested as a steal 260 // Complete the process 261 InstanceManager.throttleManagerInstance().responseThrottleDecision(address, this, DecisionType.STEAL); 262 isStealAddress = false; 263 } else { 264 // Address has not been requested as a steal yet 265 sendStealAddress(); 266 notifyFailedThrottleRequest(address, "Steal Required"); 267 } 268 } 269 else if ( question == DecisionType.STEAL_OR_SHARE ){ 270 if ( InstanceManager.getDefault(ThrottlesPreferences.class).isSilentShare() ){ 271 InstanceManager.throttleManagerInstance().responseThrottleDecision(address, this, DecisionType.SHARE ); 272 return; 273 } 274 if ( InstanceManager.getDefault(ThrottlesPreferences.class).isSilentSteal()){ 275 InstanceManager.throttleManagerInstance().responseThrottleDecision(address, this, DecisionType.STEAL ); 276 return; 277 } 278 // using the same process as a Steal as we cannot ask a Share question 279 if (isStealAddress) { 280 // Address is now staged in ThrottleManager and has been requested as a steal OR share. 281 // Complete the process 282 InstanceManager.throttleManagerInstance().responseThrottleDecision(address, this, DecisionType.STEAL); 283 isStealAddress = false; 284 } else { 285 // Address has not been requested as a steal yet 286 sendStealAddress(); 287 notifyFailedThrottleRequest(address, "Steal Required"); 288 } 289 } 290 else if ( question == DecisionType.SHARE ){ 291 if ( InstanceManager.getDefault(ThrottlesPreferences.class).isSilentShare() ){ 292 InstanceManager.throttleManagerInstance().responseThrottleDecision(address, this, DecisionType.SHARE ); 293 return; 294 } 295 log.info("Share? question not supported by WiThrottle."); 296 log.info("Enable silent sharing in main Throttle preferences to auto-share sessions."); 297 } 298 299 } 300 301 /** 302 * Add option to not silently share ("steal") the requested address 303 * 304 * {@inheritDoc} 305 */ 306 @Override 307 protected void setAddress(int number, boolean isLong) { 308 if(isStealAddress 309 || InstanceManager.throttleManagerInstance().getThrottleUsageCount(number, isLong) == 0 310 || ! InstanceManager.getDefault(WiThrottlePreferences.class).isExclusiveUseOfAddress()) { 311 super.setAddress(number, isLong); 312 } 313 else { 314 log.debug("Loco address {} already controlled by another JMRI throttle.", number); 315 sendStealAddress(); 316 notifyFailedThrottleRequest(new DccLocoAddress(number, isLong), "Steal from other WiThrottle or JMRI throttle Required"); 317 } 318 319 } 320 321 // Encode a SpeedStepMode to a string. 322 private static String encodeSpeedStepMode(SpeedStepMode mode) { 323 switch(mode) { 324 // NOTE: old speed step modes use the original numeric values 325 // from when speed step modes were in DccThrottle. New speed step 326 // modes use the mode name. 327 case NMRA_DCC_128: 328 return "1"; 329 case NMRA_DCC_28: 330 return "2"; 331 case NMRA_DCC_27: 332 return "4"; 333 case NMRA_DCC_14: 334 return "8"; 335 case MOTOROLA_28: 336 return "16"; 337 default: 338 return mode.name; 339 } 340 } 341 342 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(MultiThrottleController.class); 343 344}