001package jmri.jmrix.tams; 002 003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 004import java.util.LinkedList; 005import java.util.Queue; 006import jmri.DccLocoAddress; 007import jmri.LocoAddress; 008import jmri.jmrix.AbstractThrottle; 009import jmri.util.StringUtil; 010import org.slf4j.Logger; 011import org.slf4j.LoggerFactory; 012 013/** 014 * An implementation of DccThrottle with code specific to a TAMS connection. 015 * <p> 016 * Based on Glen Oberhauser's original LnThrottle implementation and work by 017 * Kevin Dickerson 018 * 019 * @author Jan Boen 020 */ 021public class TamsThrottle extends AbstractThrottle implements TamsListener { 022 023 //Create a local TamsMessage Queue which we will use in combination with TamsReplies 024 private final Queue<TamsMessage> tmq = new LinkedList<>(); 025 026 //This dummy message is used in case we expect a reply from polling 027 static private TamsMessage myDummy() { 028 log.trace("*** myDummy ***"); 029 TamsMessage m = new TamsMessage(2); 030 m.setElement(0, TamsConstants.POLLMSG & TamsConstants.MASKFF); 031 m.setElement(1, TamsConstants.XEVTLOK & TamsConstants.MASKFF); 032 m.setBinary(true); 033 m.setReplyOneByte(false); 034 m.setReplyType('L'); 035 return m; 036 } 037 038 public TamsThrottle(TamsSystemConnectionMemo memo, DccLocoAddress address) { 039 super(memo); 040 super.speedStepMode = jmri.SpeedStepMode.NMRA_DCC_128; 041 tc = memo.getTrafficController(); 042 043 // cache settings. It would be better to read the 044 // actual state, but I don't know how to do this 045 synchronized(this) { 046 this.speedSetting = 0; 047 } 048 // Functions default to false 049 this.address = address; 050 this.isForward = true; 051 052 //get the status if known of the current loco 053 TamsMessage tm = new TamsMessage("xL " + address.getNumber()); 054 tm.setTimeout(10000); 055 tm.setBinary(false); 056 tm.setReplyType('L'); 057 tc.sendTamsMessage(tm, this); 058 tmq.add(tm); 059 //tc.addPollMessage(m, this); 060 061 tm = new TamsMessage("xF " + address.getNumber()); 062 tm.setBinary(false); 063 tm.setReplyType('L'); 064 tc.sendTamsMessage(tm, this); 065 tmq.add(tm); 066 //tc.addPollMessage(tm, this); 067 068 tm = new TamsMessage("xFX " + address.getNumber()); 069 tm.setBinary(false); 070 tm.setReplyType('L'); 071 tc.sendTamsMessage(tm, this); 072 tmq.add(tm); 073 //tc.addPollMessage(tm, this); 074 075 //Add binary polling message 076 tm = TamsMessage.getXEvtLok(); 077 tc.sendTamsMessage(tm, this); 078 tmq.add(tm); 079 tc.addPollMessage(tm, this); 080 081 } 082 083 /** 084 * Send the message to set the state of functions F0, F1, F2, F3, F4. To 085 * send function group 1 we have to also send speed, direction etc. 086 */ 087 @Override 088 protected void sendFunctionGroup1() { 089 090 StringBuilder sb = new StringBuilder(); 091 sb.append("xL "); 092 sb.append(address.getNumber()); 093 sb.append(","); 094 sb.append(","); 095 sb.append((getFunction(0) ? "1" : "0")); 096 sb.append(","); 097 sb.append(","); 098 sb.append((getFunction(1) ? "1" : "0")); 099 sb.append(","); 100 sb.append((getFunction(2) ? "1" : "0")); 101 sb.append(","); 102 sb.append((getFunction(3) ? "1" : "0")); 103 sb.append(","); 104 sb.append((getFunction(4) ? "1" : "0")); 105 TamsMessage tm = new TamsMessage(sb.toString()); 106 tm.setBinary(false); 107 tm.setReplyType('L'); 108 tc.sendTamsMessage(tm, this); 109 tmq.add(tm); 110 } 111 112 /** 113 * Send the message to set the state of functions F5, F6, F7, F8. 114 */ 115 @Override 116 protected void sendFunctionGroup2() { 117 StringBuilder sb = new StringBuilder(); 118 sb.append("xF "); 119 sb.append(address.getNumber()); 120 sb.append(","); 121 sb.append(","); 122 sb.append(","); 123 sb.append(","); 124 sb.append(","); 125 sb.append((getFunction(5) ? "1" : "0")); 126 sb.append(","); 127 sb.append((getFunction(6) ? "1" : "0")); 128 sb.append(","); 129 sb.append((getFunction(7) ? "1" : "0")); 130 sb.append(","); 131 sb.append((getFunction(8) ? "1" : "0")); 132 133 TamsMessage tm = new TamsMessage(sb.toString()); 134 tm.setBinary(false); 135 tm.setReplyType('T'); 136 tc.sendTamsMessage(tm, this); 137 tmq.add(tm); 138 } 139 140 @Override 141 protected void sendFunctionGroup3() { 142 StringBuilder sb = new StringBuilder(); 143 sb.append("xFX "); 144 sb.append(address.getNumber()); 145 sb.append(","); 146 sb.append((getFunction(9) ? "1" : "0")); 147 sb.append(","); 148 sb.append((getFunction(10) ? "1" : "0")); 149 sb.append(","); 150 sb.append((getFunction(11) ? "1" : "0")); 151 sb.append(","); 152 sb.append((getFunction(12) ? "1" : "0")); 153 154 TamsMessage tm = new TamsMessage(sb.toString()); 155 tm.setBinary(false); 156 tm.setReplyType('L'); 157 tc.sendTamsMessage(tm, this); 158 tmq.add(tm); 159 } 160 161 /** 162 * Set the speed and direction. 163 * <p> 164 * This intentionally skips the emergency stop value of 1. 165 * 166 * @param speed Number from 0 to 1; less than zero is emergency stop 167 */ 168 @SuppressFBWarnings(value = "FE_FLOATING_POINT_EQUALITY") // OK to compare floating point, notify on any change 169 @Override 170 public synchronized void setSpeedSetting(float speed) { 171 float oldSpeed = this.speedSetting; 172 this.speedSetting = speed; 173 174 int value = Math.round((127 - 1) * this.speedSetting); // -1 for rescale to avoid estop 175 if (this.speedSetting > 0 && value == 0) { 176 value = 1; // ensure non-zero input results in non-zero output 177 } 178 if (value > 0) { 179 value = value + 1; // skip estop 180 } 181 if (value > 127) { 182 value = 127; // max possible speed 183 } 184 if (value < 0) { 185 value = 1; // emergency stop 186 } 187 StringBuilder sb = new StringBuilder(); 188 sb.append("xL "); 189 sb.append(address.getNumber()); 190 sb.append(","); 191 sb.append(value); 192 sb.append(","); 193 sb.append(","); 194 sb.append((isForward ? "f" : "r")); 195 sb.append(","); 196 sb.append(","); 197 sb.append(","); 198 sb.append(","); 199 200 TamsMessage tm = new TamsMessage(sb.toString()); 201 tm.setBinary(false); 202 tm.setReplyType('L'); 203 tc.sendTamsMessage(tm, this); 204 tmq.add(tm); 205 206 firePropertyChange(SPEEDSETTING, oldSpeed, this.speedSetting); 207 record(speed); 208 } 209 210 @Override 211 public void setIsForward(boolean forward) { 212 boolean old = isForward; 213 isForward = forward; 214 synchronized(this) { 215 setSpeedSetting(speedSetting); // send the command 216 } 217 firePropertyChange(ISFORWARD, old, isForward); 218 } 219 220 private final DccLocoAddress address; 221 222 TamsTrafficController tc; 223 224 @Override 225 public LocoAddress getLocoAddress() { 226 return address; 227 } 228 229 @Override 230 public void throttleDispose() { 231 active = false; 232 TamsMessage tm = TamsMessage.getXEvtLok(); 233 tc.removePollMessage(tm, this); 234 finishRecord(); 235 } 236 237 @Override 238 public void message(TamsMessage m) { 239 // messages are ignored 240 } 241 242 /** 243 * Convert a Tams speed integer to a float speed value. 244 * 245 * @param lSpeed Tams speed 246 * @return speed as -1 or number between 0 and 1, inclusive 247 */ 248 protected float floatSpeed(int lSpeed) { 249 if (lSpeed == 0) { 250 return 0.f; 251 } else if (lSpeed == 1) { 252 return -1.f; // estop 253 } else if (super.speedStepMode == jmri.SpeedStepMode.NMRA_DCC_128) { 254 return ((lSpeed - 1) / 126.f); 255 } else { 256 // pretty sure this is wrong, it's definitely never going to be < 1. 257 return (int) (lSpeed * 27.f + 0.5) + 1; 258 } 259 } 260 261 @Override 262 public void reply(TamsReply tr) { 263 log.trace("*** Loco reply ***"); 264 TamsMessage tm = tmq.isEmpty() ? myDummy() : tmq.poll(); 265 if (tm.isBinary()) {//Binary reply 266 //The binary logic as created by Jan 267 //Complete Loco status is given by: 268 // element(0) = Speed: 0..127, 0 = Stop, 1 = not used, 2 = min. Speed, 127 = max. Speed 269 // element(1) = F1..F8 (bit #0..7) 270 // element(2) = low byte of Loco# (A7..A0) 271 // element(3) = high byte of Loco#, plus Dir and Light status as in: 272 // bit# 7 6 5 4 3 2 1 0 273 // +-----+-----+-----+-----+-----+-----+-----+-----+ 274 // | Dir | FL | A13 | A12 | A11 | A10 | A9 | A8 | 275 // +-----+-----+-----+-----+-----+-----+-----+-----+ 276 // where: 277 // Dir Loco direction (1 = forward) 278 // FL Light status 279 // A13..8 high bits of Loco# 280 // element(4) = 'real' Loco speed (in terms of the Loco type/configuration) 281 // (please check XLokSts in P50X_LT.TXT for doc on 'real' speed) 282 //Decode address 283 int msb = tr.getElement(3) & 0x3F; 284 int lsb = tr.getElement(2) & 0xFF; 285 int receivedAddress = msb * 256 + lsb; 286 if (log.isTraceEnabled()) { // avoid overhead of StringUtil calls 287 log.trace("reply for loco = {}", receivedAddress); 288 log.trace("reply = {} {} {} {} {}", StringUtil.appendTwoHexFromInt(tr.getElement(4) & 0xFF, ""), StringUtil.appendTwoHexFromInt(tr.getElement(3) & 0xFF, ""), StringUtil.appendTwoHexFromInt(tr.getElement(2) & 0xFF, ""), StringUtil.appendTwoHexFromInt(tr.getElement(1) & 0xFF, ""), StringUtil.appendTwoHexFromInt(tr.getElement(0) & 0xFF, "")); 289 } 290 if (receivedAddress == address.getNumber()) {//If correct address then decode the content 291 log.trace("Is my address"); 292 try { 293 StringBuilder sb = new StringBuilder(); 294 Float newSpeed = floatSpeed(tr.getElement(0)); 295 super.setSpeedSetting(newSpeed); 296 log.trace("f0 = {}", tr.getElement(3) & 0x40); 297 298 appendFuncString(0,sb,((tr.getElement(3) & 0x40) == 64)); 299 300 if (((tr.getElement(3) & 0x80) == 0) && isForward) { 301 isForward = false; 302 firePropertyChange(ISFORWARD, true, isForward); 303 } 304 if (((tr.getElement(3) & 0x80) == 128) && !isForward) { 305 isForward = true; 306 firePropertyChange(ISFORWARD, false, isForward); 307 } 308 309 appendFuncString(1,sb,((tr.getElement(1) & 0x01) == 0x01)); 310 appendFuncString(2,sb,((tr.getElement(1) & 0x02) == 0x02)); 311 appendFuncString(3,sb,((tr.getElement(1) & 0x04) == 0x04)); 312 appendFuncString(4,sb,((tr.getElement(1) & 0x08) == 0x08)); 313 appendFuncString(5,sb,((tr.getElement(1) & 0x10) == 0x10)); 314 appendFuncString(6,sb,((tr.getElement(1) & 0x20) == 0x20)); 315 appendFuncString(7,sb,((tr.getElement(1) & 0x40) == 0x40)); 316 appendFuncString(8,sb,((tr.getElement(1) & 0x80) == 0x80)); 317 318 log.trace("Functions: {}", sb ); 319 } catch (RuntimeException ex) { 320 log.error("Error handling reply from MC", ex); 321 } 322 } 323 324 } else {//ASCII reply 325 //The original logic as provided by Kevin 326 if (tr.match("WARNING") >= 0) { 327 return; 328 } 329 if (tr.match("L " + address.getNumber()) >= 0) { 330 try { 331 log.trace("ASCII address = {}", address.getNumber()); 332 String[] lines = tr.toString().split(" "); 333 Float newSpeed = floatSpeed(Integer.parseInt(lines[2])); 334 super.setSpeedSetting(newSpeed); 335 updateFunction(0,lines[3].equals("1")); 336 337 if (lines[4].equals("r") && isForward) { 338 isForward = false; 339 firePropertyChange(ISFORWARD, true, isForward); 340 } else if (lines[4].equals("f") && !isForward) { 341 isForward = true; 342 firePropertyChange(ISFORWARD, false, isForward); 343 } 344 345 updateFunction(1,lines[5].equals("1")); 346 updateFunction(2,lines[6].equals("1")); 347 updateFunction(3,lines[7].equals("1")); 348 updateFunction(4,lines[8].equals("1")); 349 } catch (NumberFormatException ex) { 350 log.error("Error phrasing reply from MC", ex); 351 } 352 } else if (tr.match("FX " + address.getNumber()) >= 0) { 353 String[] lines = tr.toString().split(" "); 354 try { 355 updateFunction(9,lines[2].equals("1")); 356 updateFunction(10,lines[3].equals("1")); 357 updateFunction(11,lines[4].equals("1")); 358 updateFunction(12,lines[5].equals("1")); 359 updateFunction(13,lines[6].equals("1")); 360 updateFunction(14,lines[7].equals("1")); 361 } catch (RuntimeException ex) { 362 log.error("Error phrasing reply from MC", ex); 363 } 364 } else if (tr.match("F " + address.getNumber()) >= 0) { 365 String[] lines = tr.toString().split(" "); 366 try { 367 updateFunction(1,lines[2].equals("1")); 368 updateFunction(2,lines[3].equals("1")); 369 updateFunction(3,lines[4].equals("1")); 370 updateFunction(4,lines[5].equals("1")); 371 updateFunction(5,lines[6].equals("1")); 372 updateFunction(6,lines[7].equals("1")); 373 updateFunction(7,lines[8].equals("1")); 374 updateFunction(8,lines[9].equals("1")); 375 } catch (RuntimeException ex) { 376 log.error("Error phrasing reply from MC", ex); 377 } 378 } else if (tr.toString().equals("ERROR: no data.")) { 379 log.debug("Loco has no data"); 380 } 381 } 382 } 383 384 private void appendFuncString(int Fn, StringBuilder sb, boolean value){ 385 updateFunction(Fn,value); 386 if (getFunction(Fn)){ 387 sb.append("f"); 388 sb.append(String.valueOf(Fn)); 389 } else { 390 sb.append(String.valueOf(Fn)); 391 sb.append("f"); 392 } 393 if (Fn<8){ 394 sb.append(" "); 395 } 396 } 397 398 // initialize logging 399 private final static Logger log = LoggerFactory.getLogger(TamsThrottle.class); 400 401}