001package jmri.jmrit.withrottle; 002 003import java.util.ArrayList; 004import java.util.Arrays; 005import java.util.List; 006import java.util.concurrent.ConcurrentHashMap; 007 008import org.slf4j.Logger; 009import org.slf4j.LoggerFactory; 010 011/** 012 * Keeps track of what locos are being controlled by a throttle, and passes the 013 * control messages on to them. Creates a new MultiThrottleController for each 014 * loco requested on this throttle. Each loco will then be able to be controlled 015 * individually. '*' is a wildcard loco key. Forwards to all locos on this 016 * MultiThrottle. 017 * <p> 018 * Sample messages:<ul> 019 * <li> {@literal MT+L757<;>L757} On T throttle, add loco L757. 020 * <li> {@literal MT+L1234<;>L1234} On T throttle, add loco L1234. 021 * <li> {@literal MTSL1234<;>L1234} On T throttle, steal loco L1234. 022 * <li> {@literal MTAL757<;>R1} On T throttle, loco L757, set direction to 023 * forward. 024 * <li> {@literal MTAL1234<;>R0} On T throttle, loco L1234, set direction to 025 * reverse. 026 * <li> {@literal MTAL757<;>V42} On T throttle, loco L757, set speed to 42. 027 * <li> {@literal MTAL1234<;>V42} On T throttle, loco L1234, set speed to 42. 028 * <li> {@literal MTA*<;>V16} On T throttle, all locos, set speed to 16. 029 * <li> {@literal MT-L757<;>L757} On T throttle, remove loco L757. (Still has 030 * L1234) 031 * </ul> 032 * 033 * @author Brett Hoffman Copyright (C) 2011 034 */ 035public class MultiThrottle { 036 037 private ThrottleControllerListener parentTCL = null; 038 private ControllerInterface parentController = null; 039 char whichThrottle; 040 ConcurrentHashMap<String, MultiThrottleController> throttles; 041 042 public MultiThrottle(char id, ThrottleControllerListener tcl, ControllerInterface ci) { 043 if (log.isDebugEnabled()) { 044 log.debug("Creating new MultiThrottle for id: {}", id); 045 } 046 whichThrottle = id; 047 parentTCL = tcl; 048 parentController = ci; 049 } 050 051 /** 052 * Handle a message sent from the device. A key is used to send an action to 053 * the correct loco. '*' is a wildcard key, sends action to all locos in 054 * this MultiThrottle. 055 * 056 * @param message Consists of a control character, the loco's key, a 057 * separator {@literal "<;>"}, and the action to forward to 058 * the MultiThrottleController. 059 */ 060 public void handleMessage(String message) { 061 log.debug("MT handleMessage: {}", message); 062 List<String> unit = Arrays.asList(message.substring(1).split("<;>")); 063 String key = unit.get(0); 064 String action = unit.get(1); 065 if ((key == null) || (action == null)) { 066 return; 067 } 068 069 switch (message.charAt(0)) { 070 case 'A': // 'A'ction 071 passActionsToControllers(key, action); 072 break; 073 case '+': // add loco 074 addThrottleController(key, action); 075 break; 076 case '-': // remove loco 077 removeThrottleController(key, action); 078 break; 079 case 'S': // Steal loco 080 stealThrottleController(key, action); 081 break; 082 default: 083 log.warn("Unhandled code: {}", message.charAt(0)); 084 break; 085 } // end switch 086 087 } 088 089 private MultiThrottleController createThrottleController(String key) { 090 if (!isValidAddr(key) ) { //make sure address is acceptable before proceeding 091 return null; 092 } 093 if (throttles == null) { 094 throttles = new ConcurrentHashMap<>(1); 095 } 096 097 if (throttles.containsKey(key)) { 098 log.debug("Throttle: {} already in MultiThrottle consist.", key); 099 return null; 100 } 101 MultiThrottleController mtc = new MultiThrottleController(whichThrottle, key, parentTCL, parentController); 102 throttles.put(key, mtc); 103 log.debug("Throttle: {} added to MultiThrottle consist.", key); 104 return mtc; 105 } 106 107 protected void addThrottleController(String key, String action) { // key is address format L#### or S## 108 MultiThrottleController mtc = createThrottleController(key); 109 if (mtc != null) { 110 // This will request the loco as a DccTrottle 111 mtc.sort(action); 112 } 113 } 114 115 protected void stealThrottleController(String key, String action) { 116 MultiThrottleController mtc = createThrottleController(key); 117 if (mtc != null) { 118 // This will request the loco as a DccTrottle 119 mtc.isStealAddress = true; 120 mtc.sort(action); 121 } 122 log.debug("Throttle: {} stolen to MultiThrottle consist.", key); 123 } 124 125 /** 126 * Validate that address is going to be allowed by throttle controller. 127 * If not, send an error string to client. 128 * 129 * @param key address to be validated, of form Lnnnn or Snnn 130 */ 131 private boolean isValidAddr(String key) { 132 if (key.length() < 2) { 133 String msg = Bundle.getMessage("ErrorInvalidAddressFormat", key); 134 log.warn(msg); 135 parentController.sendAlertMessage(msg); 136 return false; 137 } 138 try { 139 int addr = Integer.parseInt(key.substring(1)); 140 if (key.charAt(0) == 'L') { 141 if (jmri.InstanceManager.throttleManagerInstance().canBeLongAddress(addr)) { 142 return true; 143 } else { 144 String msg = Bundle.getMessage("ErrorLongAddress", key); 145 log.warn(msg); 146 parentController.sendAlertMessage(msg); 147 return false; 148 } 149 } else if (key.charAt(0) == 'S') { 150 if (jmri.InstanceManager.throttleManagerInstance().canBeShortAddress(addr)) { 151 return true; 152 } else { 153 String msg = Bundle.getMessage("ErrorShortAddress", key); 154 log.warn(msg); 155 parentController.sendAlertMessage(msg); 156 return false; 157 } 158 } 159 String msg = Bundle.getMessage("ErrorInvalidAddressFormat", key); 160 parentController.sendAlertMessage(msg); 161 log.warn(msg); 162 return false; 163 } catch (NumberFormatException e) { 164 String msg = Bundle.getMessage("ErrorInvalidAddressFormat", key); 165 parentController.sendAlertMessage(msg); 166 log.warn(msg); 167 return false; 168 } 169 } 170 171 protected boolean removeThrottleController(String key, String action) { 172 173 if (throttles == null) { 174 log.debug("No MultiThrottle to remove {} from.", key); 175 return false; 176 } 177 if (key.equals("*")) { 178 ArrayList<String> throttleKeys = new ArrayList<String>(throttles.keySet()); //copy to avoid concurrentModificationException 179 throttleKeys.forEach((throttle) -> { 180 removeThrottleController(throttle, action); 181 // Runs each loco through this method individually 182 }); 183 return true; 184 } 185 if (!throttles.containsKey(key)) { 186 if (log.isDebugEnabled()) { 187 log.debug("Throttle: {} not in MultiThrottle.", key); 188 } 189 return false; 190 } 191 MultiThrottleController mtc = throttles.get(key); 192 mtc.sort(action); 193 mtc.shutdownThrottle(); 194 mtc.removeControllerListener(parentController); 195 mtc.removeThrottleControllerListener(parentTCL); 196 throttles.remove(key); 197 if (log.isDebugEnabled()) { 198 log.debug("Throttle: {} removed from MultiThrottle.", key); 199 } 200 return true; 201 } 202 203 protected void passActionsToControllers(String key, String action) { 204 if (throttles == null) { 205 log.debug("No throttles in MultiThrottle to receive action."); 206 return; 207 } 208 if (log.isDebugEnabled()) { 209 log.debug("MultiThrottle key: {}, action: {}", key, action); 210 } 211 212 if (key.equals("*")) { 213 ArrayList<String> throttleKeys = new ArrayList<String>(throttles.keySet()); //copy to avoid concurrentModificationException 214 throttleKeys.forEach((throttle) -> { 215 passActionsToControllers(throttle, action); 216 // Runs each loco through this method individually 217 }); 218 return; 219 } 220 if (throttles.containsKey(key)) { 221 throttles.get(key).sort(action); 222 } 223 } 224 225 public void dispose() { 226 if (throttles == null) { 227 return; 228 } 229 ArrayList<String> throttleKeys = new ArrayList<String>(throttles.keySet()); //copy to avoid concurrentModificationException 230 throttleKeys.forEach((throttle) -> { 231 removeThrottleController(throttle, "r"); 232 }); 233 } 234 235 public void eStop() { 236 if (throttles == null) { 237 return; 238 } 239 ArrayList<String> throttleKeys = new ArrayList<String>(throttles.keySet()); //copy to avoid concurrentModificationException 240 throttleKeys.forEach((throttle) -> { 241 passActionsToControllers(throttle, "X"); 242 }); 243 } 244 245 /** 246 * A request for a this address has been cancelled, clean up the waiting 247 * MultiThrottleController. If the MTC is marked as a steal, this cancel needs 248 * to not happen. 249 * 250 * @param key The string to use as a key to remove the proper 251 * MultiThrottleController 252 */ 253 public void canceledThrottleRequest(String key) { 254 if (throttles == null) { 255 log.warn("No MultiThrottle to remove {} from.", key); 256 return; 257 } 258 if (!throttles.containsKey(key)) { 259 if (log.isDebugEnabled()) { 260 log.debug("Throttle: {} not in MultiThrottle.", key); 261 } 262 return; 263 } 264 MultiThrottleController mtc = throttles.get(key); 265 if (!mtc.isStealAddress) { 266 mtc.removeControllerListener(parentController); 267 throttles.remove(key); 268 log.debug("Throttle: {} cancelled from MultiThrottle.", key); 269 } 270 } 271 272 private final static Logger log = LoggerFactory.getLogger(MultiThrottle.class); 273}