001package jmri.jmrix.rps; 002 003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 004import java.io.File; 005import java.io.IOException; 006import javax.vecmath.Point3d; 007import jmri.CommandStation; 008import jmri.jmrit.roster.Roster; 009import jmri.jmrit.roster.RosterEntry; 010import org.jdom2.JDOMException; 011import org.slf4j.Logger; 012import org.slf4j.LoggerFactory; 013 014/** 015 * Engine does basic computations of RPS system. 016 * <p> 017 * Holds all the alignment info. Receivers are indexed by their RPS receiver 018 * number in all cases. 019 * <p> 020 * Gets a reading from the Distributor and passes back a Measurement 021 * <p> 022 * Bound properties: 023 * <ul> 024 * <li>vSound - velocity of sound, in whatever units are in use 025 * </ul> 026 * <p> 027 * This class maintains a collection of "Transmitter" objects representing the 028 * RPS-equipped rolling stock (usually engines) on the layout. This is an 029 * extension to the common Roster, and every entry in this class's collection 030 * must be present in the Roster. 031 * 032 * @author Bob Jacobsen Copyright (C) 2006, 2008 033 */ 034public class Engine implements ReadingListener { 035 036 public Engine() { 037 } 038 039 void loadValues() { 040 // load dummy contents 041 setInitialAlignment(); 042 loadInitialTransmitters(); 043 } 044 045 public void dispose() { 046 } 047 048 public void setVSound(double v) { 049 double oldVal = vsound; 050 vsound = v; 051 log.info("change vsound from {} to {}", oldVal, v); 052 prop.firePropertyChange("vSound", Double.valueOf(oldVal), Double.valueOf(v)); 053 } 054 055 public double getVSound() { 056 return vsound; 057 } 058 private double vsound = 0.013544; // 0.013544 inches/usec, .000345 m/usec, 059 private int offset = 0; 060 061 public void setOffset(int offset) { 062 this.offset = offset; 063 } 064 065 public int getOffset() { 066 return offset; 067 } 068 069 Measurement lastPoint = null; 070 071 Receiver[] receivers; 072 073 /** 074 * Set the maximum receiver number expected. 075 * <p> 076 * If the highest value in the hardware is 5, that's what's needed here. 077 * @param n max receivers. 078 */ 079 public void setMaxReceiverNumber(int n) { 080 log.debug("setReceiverCount to {}", n); 081 if ((receivers != null) && (n == receivers.length + 1)) { 082 return; 083 } 084 Receiver[] oldReceivers = receivers; 085 receivers = new Receiver[n + 1]; // n is highest address, so need n+1 086 if (oldReceivers == null) { 087 return; 088 } 089 // clear new array 090 for (int i = 0; i < receivers.length; i++) { 091 receivers[i] = null; 092 } 093 // copy the existing receivers 094 for (int i = 0; i < Math.min(n + 1, oldReceivers.length); i++) { 095 receivers[i] = oldReceivers[i]; 096 } 097 } 098 099 public int getMaxReceiverNumber() { 100 if (receivers == null) { 101 return 0; 102 } 103 return receivers.length - 1; 104 } 105 106 /** 107 * Set a particular receiver by address (starting at 1). 108 * @param address the receiver address. 109 * @param receiver the receiver. 110 */ 111 public void setReceiver(int address, Receiver receiver) { 112 if (receivers == null) { 113 throw new IllegalArgumentException("Must initialize first"); 114 } 115 if (address >= receivers.length) { 116 throw new IllegalArgumentException("Index " + address + " is larger than expected " + receivers.length); 117 } 118 log.debug("store receiver {} in {}", address, this); 119 receivers[address] = receiver; 120 } 121 122 public Receiver getReceiver(int i) { 123 return receivers[i]; 124 } 125 126 public void setReceiverPosition(int i, Point3d p) { 127 receivers[i].setPosition(p); 128 } 129 130 public Point3d getReceiverPosition(int i) { 131 if (receivers[i] == null) { 132 log.debug("getReceiverPosition of null receiver index i={}", i); 133 return null; 134 } 135 return receivers[i].getPosition(); 136 } 137 138 public void setAlgorithm(String algorithm) { 139 this.algorithm = algorithm; 140 } 141 142 public String getAlgorithm() { 143 return algorithm; 144 } 145 146 String algorithm = "Ash 2.1"; // default value, configured separately 147 148 @Override 149 public void notify(Reading r) { 150 // This implementation creates a new Calculator 151 // each time to ensure that the most recent 152 // receiver positions are used; this should be 153 // replaced with some notification system 154 // to reduce the work done. 155 156 // ok to send next poll 157 log.debug("po false {}", r.getId()); 158 pollOutstanding = false; 159 160 // make a list of receiver positions to provide 161 // to the new Calculator. Missing/unconfigured receivers 162 // are null. 163 Point3d[] list = new Point3d[receivers.length]; 164 for (int i = 0; i < receivers.length; i++) { 165 166 if (receivers[i] == null) { 167 list[i] = null; 168 continue; // skip receivers not present 169 } 170 171 Point3d p = getReceiverPosition(i); 172 if (p != null) { 173 receivers[i].setLastTime((int) r.getValue(i)); // receivers numbered from 1 174 log.debug(" {}th value min {} < time {} < max {} at {}", 175 i, receivers[i].getMinTime(), r.getValue(i), receivers[i].getMaxTime(), p); 176 if (receivers[i].isActive() && (receivers[i].getMinTime() <= r.getValue(i)) 177 && (r.getValue(i) <= receivers[i].getMaxTime())) { 178 list[i] = p; 179 } else { 180 list[i] = null; 181 } 182 } else { 183 list[i] = null; 184 log.error("Unexpected null position from receiver {}", i); 185 } 186 } 187 188 Calculator c = Algorithms.newCalculator(list, getVSound(), 189 getOffset(), getAlgorithm()); 190 191 Measurement m = c.convert(r, lastPoint); 192 193 saveLastMeasurement(r.getId(), m); 194 195 lastPoint = m; 196 Distributor.instance().submitMeasurement(m); 197 } 198 199 // Store the lastMeasurement 200 void saveLastMeasurement(String id, Measurement m) { 201 for (int i = 0; i < getNumTransmitters(); i++) { 202 if (getTransmitter(i).getId().equals(id) && getTransmitter(i).isPolled()) { 203 getTransmitter(i).setLastMeasurement(m); 204 // might be more than one, so don't end here 205 } 206 } 207 } 208 209 // Store alignment info 210 public void storeAlignment(File file) throws IOException { 211 PositionFile pf = new PositionFile(); 212 pf.prepare(); 213 pf.setConstants(getVSound(), getOffset(), getAlgorithm()); 214 215 for (int i = 1; i <= getMaxReceiverNumber(); i++) { 216 if (getReceiver(i) == null) { 217 continue; 218 } 219 pf.setReceiver(i, getReceiver(i)); 220 } 221 pf.store(file); 222 } 223 224 public void loadAlignment(File file) throws org.jdom2.JDOMException, IOException { 225 // start by getting the file 226 PositionFile pf = new PositionFile(); 227 pf.loadFile(file); 228 229 // get VSound 230 setVSound(pf.getVSound()); 231 232 // get offset 233 setOffset(pf.getOffset()); 234 235 // get algorithm 236 setAlgorithm(pf.getAlgorithm()); 237 238 // get receivers 239 setMaxReceiverNumber(pf.maxReceiver()); // count from 1 240 Point3d p; 241 boolean a; 242 int min; 243 int max; 244 for (int i = 1; i <= getMaxReceiverNumber(); i++) { 245 p = pf.getReceiverPosition(i); 246 if (p == null) { 247 continue; 248 } 249 250 a = pf.getReceiverActive(i); 251 min = pf.getReceiverMin(i); 252 max = pf.getReceiverMax(i); 253 254 log.debug("load {} with {}", i, p); 255 Receiver r = new Receiver(p); 256 r.setActive(a); 257 r.setMinTime(min); 258 r.setMaxTime(max); 259 setReceiver(i, r); 260 } 261 262 } 263 264 protected void setInitialAlignment() { 265 File defaultFile = new File(PositionFile.defaultFilename()); 266 try { 267 loadAlignment(defaultFile); 268 } catch (Exception e) { 269 log.debug("load exception ", e); 270 // load dummy values 271 setDefaultAlignment(); 272 } 273 } 274 275 protected void setDefaultAlignment() { 276 setMaxReceiverNumber(2); 277 setReceiver(1, new Receiver(new Point3d(0.0, 0.0, 72.0))); 278 setReceiver(2, new Receiver(new Point3d(72.0, 0.0, 72.0))); 279 } 280 281 //************************************** 282 // Methods to handle polling 283 //************************************** 284 public void setPollingInterval(int pollingInterval) { 285 this.pollingInterval = pollingInterval; 286 } 287 int pollingInterval = 500; 288 289 public int getPollingInterval() { 290 return pollingInterval; 291 } 292 293 boolean polling = false; 294 295 public void setPolling(boolean polling) { 296 this.polling = polling; 297 if (polling) { 298 startPoll(); 299 } else { 300 stopPoll(); 301 } 302 } 303 304 public boolean getPolling() { 305 return polling; 306 } 307 308 java.util.ArrayList<Transmitter> transmitters; 309 310 void loadInitialTransmitters() { 311 transmitters = new java.util.ArrayList<Transmitter>(); 312 // load transmitters from the JMRI roster 313 java.util.List<RosterEntry> l = Roster.getDefault().matchingList(null, null, null, null, null, null, null); 314 log.debug("Got {} roster entries", l.size()); 315 for (int i = 0; i < l.size(); i++) { 316 RosterEntry r = null; 317 try { 318 r = l.get(i); 319 int address = Integer.parseInt(r.getDccAddress()); 320 Transmitter t = new Transmitter(r.getId(), false, address, r.isLongAddress()); 321 t.setRosterName(r.getId()); 322 transmitters.add(t); 323 } catch (NumberFormatException e) { 324 // just skip this entry 325 if (r != null) { 326 log.warn("Skip roster entry: {}", r.getId()); 327 } else { 328 log.warn("Failed roster entry skipped"); 329 } 330 } 331 } 332 333 // load the polling status, custom IDs, etc, from file if possible 334 try { 335 loadPollConfig(new File(PollingFile.defaultFilename())); 336 } catch (IOException | JDOMException e) { 337 log.error("Unable to load {}", PollingFile.defaultFilename(), e); 338 } 339 } 340 341 // Store polling info 342 public void storePollConfig(File file) throws IOException { 343 PollingFile pf = new PollingFile(); 344 pf.prepare(); 345 pf.setPoll(); 346 347 for (int i = 0; i < getNumTransmitters(); i++) { 348 pf.setTransmitter(i); 349 } 350 pf.store(file); 351 } 352 353 public void loadPollConfig(File file) throws org.jdom2.JDOMException, IOException { 354 if (file.exists()) { 355 PollingFile pf = new PollingFile(); 356 pf.loadFile(file); 357 // first make sure transmitters defined 358 pf.getTransmitters(this); 359 // and possibly start polling 360 pf.getPollValues(); 361 } 362 } 363 364 public Transmitter getTransmitterByAddress(int addr) { 365 if (addr < 0) { 366 return null; 367 } 368 if (transmitters == null) { 369 return null; 370 } 371 for (int i = 0; i < getNumTransmitters(); i++) { 372 if (getTransmitter(i).getAddress() == addr) { 373 return getTransmitter(i); 374 } 375 } 376 return null; 377 } 378 379 public Transmitter getTransmitter(int i) { 380 if (i < 0) { 381 return null; 382 } 383 if (transmitters == null) { 384 return null; 385 } 386 return transmitters.get(i); 387 } 388 389 public int getNumTransmitters() { 390 if (transmitters == null) { 391 return 0; 392 } 393 return transmitters.size(); 394 } 395 396 public String getPolledID() { 397 Transmitter t = getTransmitter(pollIndex); 398 if (t == null) { 399 return ""; 400 } 401 return t.getId(); 402 } 403 404 public int getPolledAddress() { 405 Transmitter t = getTransmitter(pollIndex); 406 if (t == null) { 407 return -1; 408 } 409 return t.getAddress(); 410 } 411 412 /** 413 * The real core of the polling, this selects the next one to poll. -1 means 414 * none selected, try again later. 415 * @return index to poll next 416 */ 417 int selectNextPoll() { 418 int startindex = pollIndex; 419 while (++pollIndex < getNumTransmitters()) { 420 if (getTransmitter(pollIndex).isPolled()) { 421 return pollIndex; 422 } 423 } 424 // here, we got to the end without finding somebody to poll 425 // try the start 426 pollIndex = -1; // will autoincrement to 0 427 while (++pollIndex <= startindex) { 428 if (getTransmitter(pollIndex).isPolled()) { 429 return pollIndex; 430 } 431 } 432 // no luck, say so 433 return -1; 434 } 435 436 int pollIndex = -1; // left at last one done 437 boolean bscPoll = false; 438 boolean throttlePoll = false; 439 440 public void setBscPollMode() { 441 bscPoll = true; 442 throttlePoll = false; 443 } 444 445 public void setDirectPollMode() { 446 bscPoll = false; 447 throttlePoll = false; 448 } 449 450 public void setThrottlePollMode() { 451 bscPoll = false; 452 throttlePoll = true; 453 } 454 455 public boolean getBscPollMode() { 456 return bscPoll; 457 } 458 459 public boolean getThrottlePollMode() { 460 return throttlePoll; 461 } 462 463 public boolean getDirectPollMode() { 464 return !(bscPoll || throttlePoll); 465 } 466 467 void startPoll() { 468 // time to start operation 469 pollThread = new Thread() { 470 @Override 471 public void run() { 472 log.debug("Polling starts"); 473 while (true) { 474 try { 475 int i = selectNextPoll(); 476 log.debug("Poll {}", i); 477 setOn(i); 478 log.debug("po true {}", i); 479 pollOutstanding = true; 480 synchronized (this) { 481 wait(20); 482 } 483 setOff(i); 484 log.debug("start wait"); 485 waitBeforeNextPoll(pollingInterval); 486 log.debug("end wait"); 487 } catch (InterruptedException e) { 488 // cancel whatever is happening 489 log.debug("Polling stops"); 490 Thread.currentThread().interrupt(); // retain if needed later 491 return; // end operation 492 } 493 } 494 } 495 }; 496 pollThread.start(); 497 } 498 499 Thread pollThread; 500 boolean pollOutstanding; 501 502 /** 503 * Wait before sending next poll. 504 * <p> 505 * Waits specified time, and then checks to see if response has been 506 * returned. If not, it waits again (twice) by 1/2 the interval, then 507 * finally polls anyway. 508 * @param pollingInterval in milliseconds 509 * @throws InterruptedException in theory, but not in practice. 510 */ 511 void waitBeforeNextPoll(int pollingInterval) throws InterruptedException { 512 synchronized (this) { 513 wait(pollingInterval); 514 } 515 if (!pollOutstanding) { 516 return; 517 } 518 log.debug("--- extra wait"); 519 for (int i = 0; i < 20; i++) { 520 synchronized (this) { 521 wait(pollingInterval / 4); 522 } 523 log.debug("-------------extra wait"); 524 if (!pollOutstanding) { 525 return; 526 } 527 } 528 } 529 530 void stopPoll() { 531 if (pollThread != null) { 532 pollThread.interrupt(); 533 } 534 } 535 536 void setOn(int i) { 537 Transmitter t = getTransmitter(i); 538 byte[] packet; 539 if (bscPoll) { 540 // poll using BSC instruction 541 packet = jmri.NmraPacket.threeBytePacket( 542 t.getAddress(), t.isLongAddress(), 543 (byte) 0xC0, (byte) 0xA5, (byte) 0xFE); 544 if (jmri.InstanceManager.getNullableDefault(CommandStation.class) != null) { 545 jmri.InstanceManager.getDefault(CommandStation.class).sendPacket(packet, 1); 546 } 547 } else { 548 // poll using F2 549 if (throttlePoll) { 550 // use throttle; first, get throttle 551 if (t.checkInit()) { 552 // now send F2 553 t.getThrottle().setFunction(2, true); 554 } else { 555 return; // bail if not ready 556 } 557 } else { 558 // send packet direct 559 packet = jmri.NmraPacket.function0Through4Packet( 560 t.getAddress(), t.isLongAddress(), 561 false, false, true, false, false); 562 if (jmri.InstanceManager.getNullableDefault(CommandStation.class) != null) { 563 jmri.InstanceManager.getDefault(CommandStation.class).sendPacket(packet, 1); 564 } 565 } 566 } 567 } 568 569 void setOff(int i) { 570 if (!bscPoll) { 571 // have to turn off F2 since not using BSC 572 Transmitter t = getTransmitter(i); 573 if (throttlePoll) { 574 // use throttle; first, get throttle 575 if (t.checkInit()) { 576 // now send F2 577 t.getThrottle().setFunction(2, false); 578 } else { 579 return; // bail if not ready 580 } 581 } else { 582 // send direct 583 byte[] packet = jmri.NmraPacket.function0Through4Packet( 584 t.getAddress(), t.isLongAddress(), 585 false, false, false, false, false); 586 if (jmri.InstanceManager.getNullableDefault(CommandStation.class) != null) { 587 jmri.InstanceManager.getDefault(CommandStation.class).sendPacket(packet, 1); 588 } 589 } 590 } 591 } 592 593 // for now, we only allow one Engine 594 @SuppressFBWarnings(value = "MS_PKGPROTECT") // for tests 595 static volatile protected Engine _instance = null; 596 597 @SuppressFBWarnings(value = "LI_LAZY_INIT_UPDATE_STATIC") // see comment in method 598 static public Engine instance() { 599 if (_instance == null) { 600 // NOTE: _instance has to be initialized before loadValues() 601 // is called, because it invokes instance() indirectly. 602 _instance = new Engine(); 603 _instance.loadValues(); 604 } 605 return _instance; 606 } 607 608 // handle outgoing parameter notification 609 java.beans.PropertyChangeSupport prop = new java.beans.PropertyChangeSupport(this); 610 611 public void removePropertyChangeListener(java.beans.PropertyChangeListener p) { 612 prop.removePropertyChangeListener(p); 613 } 614 615 public void addPropertyChangeListener(java.beans.PropertyChangeListener p) { 616 prop.addPropertyChangeListener(p); 617 } 618 619 private final static Logger log = LoggerFactory.getLogger(Engine.class); 620 621}