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