001package jmri.jmrix.dcc4pc; 002 003import java.util.ArrayList; 004import java.util.Arrays; 005import java.util.Enumeration; 006import java.util.HashMap; 007import java.util.List; 008import java.util.concurrent.ConcurrentHashMap; 009import javax.annotation.Nonnull; 010import jmri.JmriException; 011import jmri.Sensor; 012 013/** 014 * Implement SensorManager for Dcc4Pc systems. The Manager handles all the state 015 * changes. 016 * <p> 017 * System names are "DSnn:yy", where D is the user configurable system prefix, 018 * nn is the board id and yy is the port on that board. 019 * 020 * @author Kevin Dickerson Copyright (C) 2009 021 */ 022public class Dcc4PcSensorManager extends jmri.managers.AbstractSensorManager 023 implements Dcc4PcListener { 024 025 public Dcc4PcSensorManager(Dcc4PcTrafficController tc, Dcc4PcSystemConnectionMemo memo) { 026 super(memo); 027 this.tc = tc; 028 this.reportManager = (Dcc4PcReporterManager) memo.get(jmri.ReporterManager.class); 029 jmri.InstanceManager.store(Dcc4PcSensorManager.this, Dcc4PcSensorManager.class); 030 this.boardManager = new Dcc4PcBoardManager(tc, this); 031 // Finally, create and register a shutdown task to ensure clean exit 032 this.pollShutDownTask = this::stopPolling; 033 jmri.InstanceManager.getDefault(jmri.ShutDownManager.class).register(pollShutDownTask); 034 tc.setReconnectHandler(this::interfaceReconnected); 035 startPolling(); 036 } 037 038 /** 039 * The interface's port has been reopened after a failure (its USB cable 040 * was unplugged and plugged back in). The interface has restarted at 8 041 * kbps but the boards may not have, so set the bus up as at startup, then 042 * bring each board back to 62.5 kbps and resynchronise it. 043 */ 044 void interfaceReconnected() { 045 boardManager.rediscover(); 046 for (ActiveBoard board : activeBoards.values()) { 047 if (!board.doNotPoll()) { 048 board.restartAfterReconnect(); 049 } 050 } 051 } 052 053 Dcc4PcReporterManager reportManager; 054 Runnable pollShutDownTask; 055 Dcc4PcBoardManager boardManager; 056 057 Dcc4PcTrafficController tc; 058 059 @Override 060 public Dcc4PcSensor getSensor(@Nonnull String name) { 061 return (Dcc4PcSensor) super.getSensor(name); 062 063 } 064 065 /** 066 * {@inheritDoc} 067 */ 068 @Override 069 @Nonnull 070 public Dcc4PcSystemConnectionMemo getMemo() { 071 return (Dcc4PcSystemConnectionMemo) memo; 072 } 073 074 /** 075 * {@inheritDoc} 076 */ 077 @Override 078 @Nonnull 079 protected Sensor createNewSensor(@Nonnull String systemName, String userName) throws IllegalArgumentException { 080 Sensor s = new Dcc4PcSensor(systemName, userName); 081 s.setUserName(userName); 082 extractBoardID(systemName); 083 return s; 084 } 085 086 /** 087 * This extracts the board id out from the system name. 088 * @param systemName including system prefix and type letter. 089 */ 090 void extractBoardID(String systemName) { 091 if (systemName.contains(":")) { 092 int indexOfSplit = systemName.indexOf(":"); 093 systemName = systemName.substring(0, indexOfSplit); 094 indexOfSplit = getSystemPrefix().length() + 1; // +1 includes the typeletter which is a char 095 systemName = systemName.substring(indexOfSplit); 096 int boardNo; 097 try { 098 boardNo = Integer.parseInt(systemName); 099 } catch (NumberFormatException ex) { 100 log.error("Unable to find the board address from system name {}", systemName); 101 return; 102 } 103 addBoard(boardNo); 104 } 105 } 106 107 @Override 108 public boolean allowMultipleAdditions(@Nonnull String systemName) { 109 return true; 110 } 111 112 // we want the system name to be in the format of board:input 113 @Override 114 @Nonnull 115 public String createSystemName(@Nonnull String curAddress, @Nonnull String prefix) throws JmriException { 116 String iName; 117 if (curAddress.contains(":")) { 118 int board = 0; 119 int channel; 120 // Address format passed is in the form of board:channel or T:turnout address 121 int seperator = curAddress.indexOf(":"); 122 try { 123 board = Integer.parseInt(curAddress.substring(0, seperator)); 124 } catch (NumberFormatException ex) { 125 throw new JmriException("Unable to convert "+curAddress+" into the cab and channel format of nn:xx"); 126 } 127 128 try { 129 channel = Integer.parseInt(curAddress.substring(seperator + 1)); 130 if ((channel > 16) || (channel < 1)) { 131 throw new JmriException("In Address "+curAddress+" Channel number should be in the range of 1 to 16"); 132 } 133 } catch (NumberFormatException ex) { 134 throw new JmriException("Unable to convert "+curAddress+" into the cab and channel format of nn:xx"); 135 } 136 iName = curAddress; 137 addBoard(board); 138 } else { 139 throw new JmriException("Unable to convert "+curAddress+" into the cab and channel format of nn:xx"); 140 } 141 return prefix + typeLetter() + iName; 142 } 143 144 public void notifyReply(Dcc4PcReply m) { 145 // is this a list of sensors? 146 } 147 148 public void notifyMessage(Dcc4PcMessage m) { 149 // messages are ignored 150 } 151 152 Thread pollThread; 153 boolean stopPolling = true; 154 155 protected void stopPolling() { 156 synchronized (this) { 157 stopPolling = true; 158 notifyAll(); // don't sit out the rest of a poll wait 159 } 160 if (pollThread != null) { 161 // we want to wait for the polling thread to finish what it is currently working on. 162 try { 163 pollThread.join(5000); 164 } catch (InterruptedException e) { 165 // Don't need to worry 166 } 167 } 168 } 169 170 protected final void startPolling() { 171 if (stopPolling && pollThread != null) { 172 pollThread = null; 173 } 174 stopPolling = false; 175 176 if (pollThread == null) { 177 pollThread = new Thread(this::pollManager, "DCC4PC Sensor Poll"); 178 pollThread.start(); 179 } 180 } 181 182 void addBoard(int newBoard) { 183 boardManager.addBoard(newBoard); 184 } 185 186 @Override 187 public void reply(Dcc4PcReply r) { 188 if (log.isDebugEnabled()) { 189 log.debug("Reply details sm: {}", r.toHexString()); 190 } 191 192 if (r.getMessageType() == Dcc4PcMessage.INFO) { 193 // the check that follows a speed change 194 ActiveBoard board = activeBoards.get(r.getBoard()); 195 if (board != null) { 196 board.speedCheckDone(!r.isError() && r.getNumDataElements() > 0); 197 } 198 return; 199 } 200 if (r.getMessageType() == Dcc4PcMessage.CHILDRESET) { 201 ActiveBoard board = activeBoards.get(r.getBoard()); 202 if (board != null) { 203 board.resetReplied(!r.isError()); 204 } 205 return; 206 } 207 if (r.getNumDataElements() == 0 && r.getElement(0) == 0x00) { 208 //Simple acknowledgement reply, no further action required 209 return; 210 } 211 if (r.getBoard() == -1) { 212 log.debug("Message is not for a detection board so ignore"); 213 return; 214 } 215 if (r.isError()) { 216 log.debug("Reply is in error {}", r.toHexString()); 217 synchronized (this) { 218 awaitingReply = false; 219 this.notify(); 220 } 221 if (r.getMessageType() == Dcc4PcMessage.CHILDPOLL) { 222 // the reader has moved on without us, so the stream is broken 223 ActiveBoard board = activeBoards.get(r.getBoard()); 224 if (board != null) { 225 board.requestResync("Get Data failed"); 226 } 227 } 228 } else if (!r.isUnsolicited()) { 229 synchronized (this) { 230 awaitingReply = false; 231 this.notify(); 232 } 233 if (log.isDebugEnabled()) { 234 log.debug("Get Data inputs {}", r.toHexString()); 235 } 236 ActiveBoard board = activeBoards.get(r.getBoard()); 237 if (board == null) { 238 log.error("Do not know who this board message is for"); 239 return; 240 } 241 // the raw encoding is a stateful stream, so replies are decoded one at a time, in order 242 packetProcessor.execute(() -> board.processRawReply(r)); 243 } 244 } 245 246 private final java.util.concurrent.ExecutorService packetProcessor = 247 java.util.concurrent.Executors.newSingleThreadExecutor(runnable -> { 248 Thread t = new Thread(runnable, "Dcc4PCSensor Process Packet"); 249 t.setDaemon(true); 250 return t; 251 }); 252 253 public static String decodeInputState(int state) { 254 String rtr; 255 switch (state) { 256 case Sensor.INACTIVE: 257 rtr = "UnOccupied"; 258 break; 259 case Sensor.ACTIVE: 260 rtr = "Occupied No RailCom Data"; 261 break; 262 case Dcc4PcSensor.ORIENTA: 263 rtr = "Occupied RailCom Orientation A"; 264 break; 265 case Dcc4PcSensor.ORIENTB: 266 rtr = "Occupied RailCom Orientation B"; 267 break; 268 default: 269 rtr = "Unknown"; 270 } 271 return rtr; 272 } 273 274 public static final int NO_ADDRESS = 0x00; 275 public static final int SHORT_ADDRESS = 0x02; 276 public static final int LONG_ADDRESS = 0x04; 277 public static final int CONSIST_ADDRESS = 0x08; 278 279 private final int shortCycleInterval = 1; // Time to wait between sending out poll messages 280 private final int pollTimeout = 3500; // in case of lost response; longer than the Get Data timeout 281 private boolean awaitingReply = false; 282 283 void pollManager() { 284 for (ActiveBoard board : activeBoards.values()) { 285 if (!board.doNotPoll() && board.speedChecked) { 286 board.requestResync("polling started"); 287 } 288 } 289 while (!stopPolling) { 290 if (activeBoards.isEmpty()) { 291 //If we have no boards to poll then wait a second. 292 try { 293 Thread.sleep(1000); 294 } catch (java.lang.InterruptedException e) { 295 } 296 } else { 297 for (int boardAddress : activeBoards.keySet()) { 298 ActiveBoard board = activeBoards.get(boardAddress); 299 if (!board.doNotPoll() && !board.isSynced()) { 300 // Get Data is useless until a Reset Encoding State has been acknowledged 301 board.checkSpeedTimeout(); 302 board.resendResetIfNeeded(); 303 try { 304 Thread.sleep(50); 305 } catch (InterruptedException e) { 306 Thread.currentThread().interrupt(); 307 } 308 } else if (!board.doNotPoll()) { 309 board.retrySpeedIfDue(); 310 if (!board.isSynced()) { 311 continue; // a speed change has just started 312 } 313 log.debug("Poll board {}", boardAddress); 314 Dcc4PcMessage m = Dcc4PcMessage.pollBoard(boardAddress); 315 log.debug("queueing poll request for board {}", boardAddress); 316 tc.sendDcc4PcMessage(m, this); 317 synchronized (this) { 318 awaitingReply = true; 319 try { 320 wait(pollTimeout); 321 } catch (InterruptedException e) { 322 Thread.currentThread().interrupt(); // retain if needed later 323 } 324 } 325 int delay = shortCycleInterval; 326 synchronized (this) { 327 if (awaitingReply && !stopPolling) { // stopPolling() wakes us early on purpose 328 log.warn("timeout awaiting poll response for board {}", boardAddress); 329 delay = pollTimeout; 330 } 331 try { 332 wait(delay); 333 } catch (InterruptedException e) { 334 Thread.currentThread().interrupt(); // retain if needed later 335 } finally { 336 /*awaitingDelay = false;*/ 337 } 338 } 339 } else { 340 log.debug("Board set to Do Not Poll {}", boardAddress); 341 } 342 synchronized (this) { 343 if (stopPolling) { 344 log.debug("Polling stopped {}", stopPolling); 345 Thread.currentThread().interrupt(); 346 return; 347 } 348 } 349 } 350 } 351 } 352 } 353 354 ConcurrentHashMap<Integer, ActiveBoard> activeBoards = new ConcurrentHashMap<>(5); 355 356 /** 357 * Generate all the sensor and reporter details based upon 358 * the reply message from the board. 359 * 360 * @param r Reply that we build the information from 361 */ 362 protected void createSensorsFromReply(Dcc4PcReply r) { 363 int boardAddress = r.getBoard(); 364 log.debug("createSensorsFromReply: Get enabled inputs {}", r.toHexString()); 365 366 String sensorPrefix = getSystemPrefix() + typeLetter() + boardAddress + ":"; 367 String reporterPrefix = getSystemPrefix() + "R" + boardAddress + ":"; 368 369 int x = 1; 370 List<Integer> enabled = new ArrayList<>(); 371 for (int i = 0; i < r.getNumDataElements(); i++) { 372 373 for (int j = 0; j < 8; j++) { 374 Dcc4PcSensor s = (Dcc4PcSensor) createNewSensor(sensorPrefix + (j + x), null); 375 register(s); 376 s.setInput(j + x); 377 activeBoards.get(boardAddress).addSensor(j + x, s); 378 // one bit per input, the first input in bit 0 379 if (((r.getElement(i) >> j) & 0x01) == 0x01) { 380 s.setEnabled(true); 381 enabled.add(j + x); 382 } 383 Dcc4PcReporter report = (Dcc4PcReporter) reportManager.createNewReporter(reporterPrefix + (j + x), null); 384 activeBoards.get(boardAddress).addReporter(j + x, report); 385 386 } 387 x = x + 8; 388 } 389 log.info("Board {} has inputs {} enabled", boardAddress, enabled); 390 activeBoards.get(boardAddress).setEnabledInputs(enabled); 391 activeBoards.get(boardAddress).setDoNotPoll(false); 392 log.debug(" created {} sensors", x); 393 } 394 395 @Override 396 public void handleTimeout(Dcc4PcMessage m) { 397 log.debug("timeout received to our last message {}", m); 398 ActiveBoard board = activeBoards.get(m.getBoard()); 399 if (board != null) { 400 if (m.getMessageType() == Dcc4PcMessage.INFO) { 401 board.speedCheckDone(false); 402 } else if (m.getMessageType() == Dcc4PcMessage.CHILDRESET) { 403 board.resetReplied(false); 404 } else if (m.getMessageType() == Dcc4PcMessage.CHILDPOLL) { 405 // the reply may have been lost part way, so the stream can't be trusted 406 board.requestResync("Get Data timed out"); 407 } 408 } 409 if (!stopPolling) { 410 synchronized (this) { 411 awaitingReply = false; 412 this.notify(); 413 } 414 } 415 } 416 417 @Override 418 public void message(Dcc4PcMessage m) { 419 420 } 421 422 public void changeBoardAddress(int oldAddress, int newAddress) { 423 // Block polling on this board 424 ActiveBoard board = activeBoards.get(oldAddress); 425 board.setDoNotPoll(true); 426 Dcc4PcMessage m = new jmri.jmrix.dcc4pc.Dcc4PcMessage(new byte[]{(byte) 0x0b, (byte) oldAddress, (byte) 0x03, (byte) newAddress}); 427 tc.sendDcc4PcMessage(m, null); 428 // Need to stop polling otherwise we get a concurrent modification exception 429 stopPolling(); 430 activeBoards.remove(oldAddress); 431 activeBoards.put(newAddress, board); 432 board.setAddress(newAddress); 433 startPolling(); 434 String sensorPrefix = getSystemPrefix() + typeLetter() + newAddress + ":"; 435 String reporterPrefix = getSystemPrefix() + "R" + newAddress + ":"; 436 // We create a new set of sensors and reporters, but leave the old ones. 437 for (int i = 1; i < board.getNumEnabledSensors() + 1; i++) { 438 board.getSensorAtIndex(i).setEnabled(false); 439 int input = board.getSensorAtIndex(i).getInput(); 440 Dcc4PcSensor s = (Dcc4PcSensor) createNewSensor(sensorPrefix + (input), null); 441 register(s); 442 s.setInput(input); 443 s.setEnabled(true); 444 board.addSensor(input, s); 445 Dcc4PcReporter report = (Dcc4PcReporter) reportManager.createNewReporter(reporterPrefix + (input), null); 446 board.addReporter(input, report); 447 } 448 // Need to update the sensors used. 449 board.setDoNotPoll(false); 450 } 451 452 class ActiveBoard { 453 454 ActiveBoard(int address, String version, int inputs, int encoding) { 455 this.version = version; 456 this.inputs = inputs; 457 this.encoding = encoding; 458 this.address = address; 459 } 460 461 void setAddress(int address) { 462 this.address = address; 463 } 464 465 int inputs; 466 int encoding; 467 String version; 468 String description; 469 int address; 470 471 int failedRequests = 0; 472 473 void addFailedRequests() { 474 failedRequests++; 475 } 476 477 void clearFailedRequests() { 478 failedRequests = 0; 479 } 480 481 boolean doNotPoll = true; 482 483 void setDoNotPoll(boolean poll) { 484 doNotPoll = poll; 485 if (!poll) { 486 if (speedChecked) { 487 requestResync("polling enabled"); 488 } else { 489 startSpeedChange(); 490 } 491 } 492 } 493 494 boolean doNotPoll() { 495 return doNotPoll; 496 } 497 498 HashMap<Integer, Dcc4PcSensor> inputPorts = new HashMap<>(16); 499 500 void addSensor(int port, Dcc4PcSensor sensor) { 501 inputPorts.put(port, sensor); 502 } 503 504 String getEncodingAsString() { 505 if ((encoding & 0x01) == 0x01) { 506 return "Supports Cooked RailCom Encoding"; 507 508 } else { 509 return "Supports Raw RailCom Encoding"; 510 } 511 } 512 513 void setDescription(String description) { 514 this.description = description; 515 } 516 517 Dcc4PcSensor getSensorAtIndex(int i) { 518 if (!inputPorts.containsKey(i)) { 519 return null; 520 } 521 return inputPorts.get(i); 522 } 523 524 int getNumEnabledSensors() { 525 return inputPorts.size(); 526 } 527 528 HashMap<Integer, Dcc4PcReporter> inputReportersPorts = new HashMap<>(16); 529 530 void addReporter(int port, Dcc4PcReporter reporter) { 531 inputReportersPorts.put(port, reporter); 532 } 533 534 Dcc4PcReporter getReporterAtIndex(int i) { 535 if (!inputReportersPorts.containsKey(i)) { 536 return null; 537 } 538 return inputReportersPorts.get(i); 539 } 540 541 Dcc4PcRawDecoder decoder = new Dcc4PcRawDecoder(new int[0]); 542 volatile boolean synced = false; 543 volatile boolean resetPending = false; 544 private final HashMap<Integer, Integer> appliedStates = new HashMap<>(16); 545 546 void setEnabledInputs(List<Integer> enabled) { 547 int[] inputs = new int[enabled.size()]; 548 for (int i = 0; i < inputs.length; i++) { 549 inputs[i] = enabled.get(i); 550 } 551 decoder = new Dcc4PcRawDecoder(inputs); 552 blockLocos = new Dcc4PcBlockLocos(inputs); 553 shownStates = new int[inputs.length]; 554 pendingStates = new int[inputs.length]; 555 pendingCounts = new int[inputs.length]; 556 pendingSince = new long[inputs.length]; 557 } 558 559 Dcc4PcBlockLocos blockLocos = new Dcc4PcBlockLocos(new int[0]); 560 561 /** 562 * A new input state is only shown once it has lasted this many packets 563 * in a row, so a one-packet glitch (seen: orientation B with garbage 564 * data on a block holding a loco without RailCom) doesn't reach the 565 * sensors, reporters or loco tracking. At about 70 packets a second 566 * this adds roughly 15 ms. 567 */ 568 static final int STATE_DEBOUNCE_PACKETS = 2; 569 570 /** 571 * Going unoccupied must also last this long. A loco crossing a block 572 * gap or dirty rail drops out for a moment (seen: J5 unoccupied and 573 * back three times in half a second), and each drop would otherwise 574 * remove its RailCom tags from the block. 575 */ 576 static final long UNOCCUPIED_HOLD_MS = 300; 577 578 private int[] shownStates = new int[0]; 579 private int[] pendingStates = new int[0]; 580 private int[] pendingCounts = new int[0]; 581 private long[] pendingSince = new long[0]; 582 583 /** @return the debounced states for this packet, in the decoder's input order */ 584 private int[] debounce(int[] raw, long now) { 585 for (int i = 0; i < raw.length; i++) { 586 if (raw[i] == shownStates[i]) { 587 pendingCounts[i] = 0; 588 } else if (pendingCounts[i] > 0 && raw[i] == pendingStates[i]) { 589 pendingCounts[i]++; 590 boolean held = raw[i] != Dcc4PcRawDecoder.STATE_UNOCCUPIED 591 || now - pendingSince[i] >= UNOCCUPIED_HOLD_MS; 592 if (pendingCounts[i] >= STATE_DEBOUNCE_PACKETS && held) { 593 shownStates[i] = raw[i]; 594 pendingCounts[i] = 0; 595 } 596 } else { 597 pendingStates[i] = raw[i]; 598 pendingCounts[i] = 1; 599 pendingSince[i] = now; 600 } 601 } 602 return shownStates.clone(); 603 } 604 605 boolean isSynced() { 606 return synced; 607 } 608 609 /** 610 * Stop decoding and ask the reader to start its Raw encoding stream 611 * again. Decoding resumes once the reader acknowledges the Reset 612 * Encoding State. 613 * 614 * @param reason why, for the log 615 */ 616 void requestResync(String reason) { 617 log.debug("Resynchronising board {}: {}", address, reason); // routine: several a minute with busy RailCom 618 synced = false; 619 sendReset(); 620 } 621 622 /** Send another Reset Encoding State unless one is already waiting for its reply. */ 623 void resendResetIfNeeded() { 624 if (!synced && !resetPending && !speedPending) { 625 sendReset(); 626 } 627 } 628 629 volatile boolean speedChecked = false; 630 volatile boolean speedPending = false; 631 /** True after the board stopped answering at all, until a reset succeeds again. */ 632 volatile boolean lostContact = false; 633 volatile long speedStarted = 0; 634 int failedResets = 0; 635 636 /** Give up on a speed change that never got an answer, so polling can start at 8 kbps. */ 637 void checkSpeedTimeout() { 638 if (speedPending && System.currentTimeMillis() - speedStarted > 10000) { 639 log.warn("No answer to the speed change for board {}", address); 640 speedCheckDone(false); 641 } 642 } 643 int busSpeed = 0; 644 645 /** 646 * Move this board to 62.5 kbps: at 8 kbps its buffer overflows between 647 * polls and every Get Data takes over half a second. The board's Set 648 * Speed comes first, then the interface's, then a Get Info to check the 649 * board still answers; polling starts once that is settled. 650 */ 651 private void startSpeedChange() { 652 speedPending = true; 653 speedStarted = System.currentTimeMillis(); 654 synced = false; 655 log.info("Switching board {} to 62.5 kbps", address); 656 sendWakeUp(); 657 tc.sendDcc4PcMessage(Dcc4PcMessage.setBoardSpeed(address, Dcc4PcMessage.SPEED_62K5), null); 658 tc.sendDcc4PcMessage(Dcc4PcMessage.busSetSpeed(address, Dcc4PcMessage.SPEED_62K5), null); 659 Dcc4PcMessage check = Dcc4PcMessage.getInfo(address); 660 check.setTimeout(1000); 661 tc.sendDcc4PcMessage(check, Dcc4PcSensorManager.this); 662 } 663 664 /** 665 * The Get Info that follows a speed change has been answered or has 666 * failed. 667 * 668 * @param ok true if the board answered at the new speed 669 */ 670 void speedCheckDone(boolean ok) { 671 if (!speedPending) { 672 return; 673 } 674 if (ok) { 675 busSpeed = Dcc4PcMessage.SPEED_62K5; 676 speedRetryDelay = FIRST_SPEED_RETRY; 677 log.info("Board {} is running at 62.5 kbps", address); 678 } else { 679 busSpeed = 0; 680 // try again later: the board may only have been without track power 681 nextSpeedRetry = System.currentTimeMillis() + speedRetryDelay; 682 log.warn("Board {} did not answer at 62.5 kbps, going back to 8 kbps; trying again in {} s", 683 address, speedRetryDelay / 1000); 684 speedRetryDelay = Math.min(speedRetryDelay * 2, MAX_SPEED_RETRY); 685 tc.sendDcc4PcMessage(Dcc4PcMessage.busResetSpeeds(), null); 686 tc.sendDcc4PcMessage(Dcc4PcMessage.busSetSpeed(address, 0), null); 687 } 688 speedChecked = true; 689 speedPending = false; 690 requestResync("bus speed set"); 691 } 692 693 /** 694 * A board that has just powered up (it runs on track power) ignores 695 * every command except Get Info until it has answered a Get Info, so 696 * send one before anything else. No listener: the reply is not the 697 * speed check. 698 */ 699 private void sendWakeUp() { 700 tc.sendDcc4PcMessage(Dcc4PcMessage.getInfo(address), null); 701 } 702 703 private void sendReset() { 704 resetPending = true; 705 sendWakeUp(); 706 tc.sendDcc4PcMessage(Dcc4PcMessage.selectRawEncoding(address), null); 707 Dcc4PcMessage m = Dcc4PcMessage.resetBoardData(address); 708 m.setTimeout(2000); 709 tc.sendDcc4PcMessage(m, Dcc4PcSensorManager.this); 710 } 711 712 /** 713 * The reader answered a Reset Encoding State. 714 * 715 * @param ok true if it was accepted 716 */ 717 void resetReplied(boolean ok) { 718 if (!ok) { 719 failedResets++; 720 log.debug("Reset Encoding State failed for board {} ({} in a row)", address, failedResets); 721 if (failedResets >= 3 && !speedPending) { 722 // The board is powered from the track, so a short or power cut restarts 723 // it at 8 kbps while the interface is still on the faster speed. Wait at 724 // 8 kbps until it answers again before trying 62.5 kbps: trying while 725 // track power was still off failed each time and doubled the retry 726 // delay, so after a 70 s outage JMRI stayed at 8 kbps for 5 minutes. 727 if (!lostContact) { 728 log.warn("Board {} not answering, waiting for it at 8 kbps", address); 729 } 730 lostContact = true; 731 failedResets = 0; 732 busSpeed = 0; 733 tc.sendDcc4PcMessage(Dcc4PcMessage.busResetSpeeds(), null); 734 tc.sendDcc4PcMessage(Dcc4PcMessage.busSetSpeed(address, 0), null); 735 } 736 resetPending = false; 737 return; 738 } 739 failedResets = 0; 740 if (lostContact) { 741 // it's back (track power restored): change speed again straight away 742 lostContact = false; 743 log.info("Board {} is answering again", address); 744 speedRetryDelay = FIRST_SPEED_RETRY; 745 nextSpeedRetry = System.currentTimeMillis(); 746 } 747 // queued behind any Get Data replies that were already on their way, which are dropped 748 packetProcessor.execute(() -> { 749 decoder.reset(); 750 resetPending = false; 751 synced = true; 752 log.debug("Board {} synchronised", address); 753 }); 754 } 755 756 /** Forget the old link state and change speed again from 8 kbps, after the port was reopened. */ 757 void restartAfterReconnect() { 758 log.info("Setting up board {} again after the interface reconnected", address); 759 failedResets = 0; 760 resetPending = false; 761 speedPending = false; 762 lostContact = false; 763 busSpeed = 0; 764 nextSpeedRetry = 0; 765 speedRetryDelay = FIRST_SPEED_RETRY; 766 startSpeedChange(); 767 } 768 769 static final long FIRST_SPEED_RETRY = 5000; 770 static final long MAX_SPEED_RETRY = 300000; 771 long speedRetryDelay = FIRST_SPEED_RETRY; 772 volatile long nextSpeedRetry = 0; 773 774 /** 775 * Called from the poll loop: if a speed change fell back to 8 kbps 776 * (often because track power was still off), try 62.5 kbps again once 777 * the retry time has come. 778 */ 779 void retrySpeedIfDue() { 780 if (synced && speedChecked && !speedPending && busSpeed == 0 781 && nextSpeedRetry != 0 && System.currentTimeMillis() >= nextSpeedRetry) { 782 nextSpeedRetry = 0; 783 startSpeedChange(); 784 } 785 } 786 787 /** 788 * Decode one Get Data reply and update the sensors and reporters. 789 * Called on the packet processor thread, in the order the replies 790 * arrived. 791 * 792 * @param r the reply 793 */ 794 void processRawReply(Dcc4PcReply r) { 795 if (!synced) { 796 log.debug("Board {} not synchronised, dropping {}", address, r.toHexString()); 797 return; 798 } 799 int[] data = new int[r.getNumDataElements()]; 800 for (int i = 0; i < data.length; i++) { 801 data[i] = r.getElement(i) & 0xFF; 802 } 803 for (Dcc4PcRawDecoder.Packet p : decoder.decode(data)) { 804 switch (p.kind) { 805 case OVERFLOW: 806 log.debug("Board {} buffer overflowed, packets lost", address); 807 forgetPomPackets(); 808 break; 809 case UNKNOWN_COMMAND: 810 requestResync("unknown command in Raw encoding stream"); 811 return; 812 default: 813 applyPacket(p); 814 break; 815 } 816 } 817 } 818 819 private void applyPacket(Dcc4PcRawDecoder.Packet p) { 820 int[] inputs = decoder.getInputs(); 821 long now = System.currentTimeMillis(); 822 int[] states = debounce(p.states, now); 823 for (int i = 0; i < inputs.length; i++) { 824 int state = sensorState(states[i]); 825 Integer old = appliedStates.put(inputs[i], state); 826 if (old != null && old == state) { 827 continue; 828 } 829 Dcc4PcSensor s = getSensorAtIndex(inputs[i]); 830 if (s != null) { 831 s.setOwnState(state); 832 } 833 Dcc4PcReporter rep = getReporterAtIndex(inputs[i]); 834 if (rep != null) { 835 rep.setRailComState(state); 836 } 837 } 838 // which locos are in which block, from channel 2 replies and clean channel 1 addresses 839 for (Dcc4PcBlockLocos.Change c : blockLocos.apply(p, states, now)) { 840 if (!c.refresh) { 841 log.debug("Board {}: {}", address, c); // each time a loco enters or leaves a block 842 } 843 Dcc4PcReporter rep = getReporterAtIndex(c.input); 844 if (rep == null) { 845 continue; 846 } 847 int a = c.address & ~Dcc4PcBlockLocos.LONG; 848 if (c.refresh) { 849 rep.locoHeard(a); 850 } else if (c.added) { 851 rep.locoEntered(a, (c.address & Dcc4PcBlockLocos.LONG) != 0); 852 } else { 853 rep.locoExited(a); 854 } 855 } 856 for (Dcc4PcRawDecoder.InputData d : p.railCom) { 857 if (d.data == null || d.sourceDcc == null) { 858 continue; // a duplicate of data lost to an overflow 859 } 860 Dcc4PcReporter rep = getReporterAtIndex(d.input); 861 if (rep == null) { 862 continue; 863 } 864 try { 865 decodeDCCPacket(d.sourceDcc, now); 866 } catch (RuntimeException ex) { 867 log.error("decodeDCCPacket Exception", ex); 868 continue; 869 } 870 rep.setPacket(d.data, dcc_addr_type, addr, cvNumber, speed, 0); 871 } 872 } 873 874 private int sensorState(int rawState) { 875 switch (rawState) { 876 case Dcc4PcRawDecoder.STATE_UNOCCUPIED: 877 return Sensor.INACTIVE; 878 case Dcc4PcRawDecoder.STATE_OCCUPIED: 879 return Sensor.ACTIVE; 880 case Dcc4PcRawDecoder.STATE_ORIENTATION_A: 881 return Dcc4PcSensor.ORIENTA; 882 default: 883 return Dcc4PcSensor.ORIENTB; 884 } 885 } 886 887 int addr = 0; 888 int dcc_addr_type = NO_ADDRESS; 889 int cvNumber = 0; 890 int speed = 0; 891 892 int[] lastDCCPacketSeen = new int[0]; 893 894 /** 895 * How long after a POM packet is seen a POM answer that follows 896 * another packet to the same loco is still credited to that POM 897 * packet's CV. Answers normally come 160-270 ms after the packet. 898 */ 899 static final long POM_ANSWER_WINDOW = 1000; 900 901 /** 902 * How many cutouts to a loco, starting with the one after the first 903 * POM packet for a new CV, may still carry the decoder's previous 904 * answer and so aren't credited. Usually only the first does, but 905 * the decoder can take longer, or the reader can miss a POM packet 906 * (2026-10-16: 1.1.284 was given CV 283's value once with 1). 907 */ 908 static final int STALE_CUTOUTS = 2; 909 910 /** 911 * Per loco (see {@link #locoKey}): the CV of the last POM packet seen, 912 * when, and how many more of its cutouts aren't credited. 913 */ 914 private final HashMap<Integer, long[]> lastPom = new HashMap<>(); 915 916 /** When the POM packet whose CV {@link #cvNumber} was credited from was seen; -1 if none. */ 917 private long creditedPomTime = -1; 918 919 private int locoKey() { 920 return (dcc_addr_type << 16) | addr; 921 } 922 923 /** 924 * Forget the POM packets seen so far, after packets were lost to an 925 * overflow. A lost packet may have been a newer POM packet, so a 926 * later answer may be for a CV we never saw; crediting it to the 927 * last one seen would give that CV the wrong answer (2026-10-15). 928 * Answers count again from the next POM packet seen. 929 */ 930 void forgetPomPackets() { 931 lastPom.clear(); 932 if (creditedPomTime >= 0) { 933 cvNumber = 0; 934 creditedPomTime = -1; 935 } 936 } 937 938 void decodeDCCPacket(int[] packet) { 939 decodeDCCPacket(packet, System.currentTimeMillis()); 940 } 941 942 /** 943 * Decode the DCC packet a RailCom cutout followed: sets 944 * {@link #addr}, {@link #dcc_addr_type} and {@link #cvNumber}. 945 * <p> 946 * A decoder may answer a POM packet in the cutout after a later 947 * packet to it, not the POM packet itself. With only one loco in 948 * the command station's refresh that is nearly always the case 949 * (2026-10-15: the answer to "write CV 31" followed a speed packet). 950 * So after any packet to a loco, {@link #cvNumber} is the CV of the 951 * last POM packet seen for it, if that was within 952 * {@link #POM_ANSWER_WINDOW}. 953 * <p> 954 * Except the first {@link #STALE_CUTOUTS} cutouts to the loco from the 955 * first POM packet for a new CV: there the decoder may still send its 956 * previous answer (2026-10-16, ESU LokSound 5: usually exactly one 957 * stale cutout). Crediting them gave every CV the value of the one 958 * read before. After an overflow, any POM packet counts as new, as a 959 * newer one may have been lost. 960 * 961 * @param packet the DCC packet bytes 962 * @param now the current time in ms 963 */ 964 void decodeDCCPacket(int[] packet, long now) { 965 if (Arrays.equals(packet, lastDCCPacketSeen)) { 966 if (creditedPomTime >= 0 && now - creditedPomTime > POM_ANSWER_WINDOW) { 967 cvNumber = 0; // the same packet again, but too long after the POM packet 968 } 969 return; 970 } 971 lastDCCPacketSeen = packet.clone(); 972 speed = 0; 973 addr = 0; 974 cvNumber = 0; // only a POM packet, or a recent one to the same loco, sets it 975 creditedPomTime = -1; 976 dcc_addr_type = NO_ADDRESS; 977 if (packet.length < 2) { 978 return; // too short to hold an address and an instruction 979 } 980 if (log.isDebugEnabled()) { 981 982 StringBuilder buf = new StringBuilder(); 983 for (int i = 0; i < packet.length; ++i) { 984 buf.append(Integer.toHexString(packet[i])).append(","); 985 } 986 String s = buf.toString(); 987 log.debug("bytes to process {}", s); 988 } 989 // Basic Accessory Decoder packet 10aaaaaa 1aaacddd eeeeeeee 990 // Extended Accessory decoder packet starts 10aaaaaa 0aaa0aa1 000xxxxx eeeeeeee 991 int i = 0; 992 // Skip accessory decoder packets at this point 993 if ((packet[i] & 0xFF) == 0x00) { 994 return; 995 //Broadcast packet 996 } else if ((packet[i] & 0xff) == 0xff) { 997 // log.debug("Idle Packet"); 998 return; 999 } else if ((packet[i] & 0xC0) == 0x80) { 1000 // Accessory decoder packet (10aaaaaa). Testing only the top bit also 1001 // caught long loco addresses (11aaaaaa), so their POM replies and DYN 1002 // data never reached the loco. 1003 return; 1004 } else { 1005 //The way that this determins of there is a two part extended address packet isn't great./ 1006 int addr_1 = packet[i] & 0xFF; // packets shorter than 2 bytes were dropped above 1007 i++; 1008 if (addr_1 == 0) { 1009 dcc_addr_type = NO_ADDRESS; 1010 } else if ((addr_1 >= 1) && (addr_1 <= 127)) { //Short address 1011 dcc_addr_type = SHORT_ADDRESS; 1012 addr = addr_1; 1013 } else if ((addr_1 >= 128) && (addr_1 <= 191)) { 1014 dcc_addr_type = NO_ADDRESS; //this is an accessory decoder address should have already of been filtered out 1015 } else if ((addr_1 >= 192) && (addr_1 <= 231)) { //14 bit address 1016 dcc_addr_type = LONG_ADDRESS; 1017 addr = (((addr_1 & 0x3F) << 8) | (packet[i] & 0xFF)); 1018 i++; 1019 } else { 1020 dcc_addr_type = NO_ADDRESS; 1021 } 1022 String addt; 1023 switch (dcc_addr_type) { 1024 case LONG_ADDRESS: 1025 addt = "Long"; 1026 break; 1027 case SHORT_ADDRESS: 1028 addt = "Short"; 1029 break; 1030 default: 1031 addt = "No Address"; 1032 break; 1033 } 1034 1035 log.debug("DCC address type {} addr {}", addt, addr); 1036 if ((dcc_addr_type != NO_ADDRESS)) { 1037 long[] pom = lastPom.get(locoKey()); 1038 boolean isPom = (packet[i] & 0xE0) == 0xE0; 1039 if (!isPom && pom != null && now - pom[1] <= POM_ANSWER_WINDOW) { 1040 if (pom[2] > 0) { 1041 pom[2]--; // may still be the previous CV's answer 1042 } else { 1043 cvNumber = (int) pom[0]; 1044 creditedPomTime = pom[1]; 1045 } 1046 } 1047 log.debug("Current index {} value {}", i, packet[i] & 0xFF); 1048 if (isPom) { 1049 // 1110CCVV VVVVVVVV: the instruction byte holds the CV number's top two bits 1050 int cvHigh = packet[i] & 0x03; 1051 i++; 1052 int pomCv = ((cvHigh << 8) | (packet[i] & 0xff)) + 1; 1053 creditedPomTime = -1; 1054 if (pom == null || pom[0] != pomCv) { 1055 // the decoder answers this cutout, and maybe the next, before it 1056 // has acted on the packet 1057 lastPom.put(locoKey(), new long[]{pomCv, now, STALE_CUTOUTS - 1}); 1058 log.debug("CV Access cv:{}, first packet; its answer is the previous CV's", pomCv); 1059 } else { 1060 pom[1] = now; 1061 if (pom[2] > 0) { 1062 pom[2]--; 1063 } else { 1064 cvNumber = pomCv; 1065 } 1066 log.debug("CV Access cv:{}", pomCv); 1067 } 1068 // two byte instruction when 1111 1069 // three byte instuction when 1110 1070 } else if ((packet[i] & 0xC0) == 0xC0) { 1071 log.debug("Future"); 1072 i++; 1073 if ((packet[i] & 0x1F) == 0x1F) { 1074 // F21-F28 Two byte instruction 1075 i++; 1076 } else if ((packet[i] & 0x1E) == 0x1E) { 1077 // F13-F20 Two byte instruction 1078 i++; 1079 } else if ((packet[i] & 0x1D) == 0x1D) { 1080 // Two byte instruction 1081 i++; 1082 1083 } else if ((packet[i] >> 3) == 0x00) { 1084 // This should fall through to 00000 three byte instruction 1085 // i = i + 2; 1086 1087 } else { 1088 // Remainder are reserved 1089 } 1090 // Two or three byte instruction 1091 } else if ((packet[i] & 0xA0) == 0xa0) { 1092 log.debug("For Function Group 2"); 1093 if ((packet[i] & 0x10) == 0x10) { 1094 log.debug("Functions 5 to 8"); 1095 if ((packet[i] & 0x08) == 0x08) { 1096 log.debug("Function 8 on"); 1097 } else { 1098 log.debug("Function 8 off"); 1099 } 1100 if ((packet[i] & 0x04) == 0x04) { 1101 log.debug("Function 7 on"); 1102 } else { 1103 log.debug("Function 7 off"); 1104 } 1105 if ((packet[i] & 0x02) == 0x02) { 1106 log.debug("Function 6 on"); 1107 } else { 1108 log.debug("Function 6 off"); 1109 } 1110 if ((packet[i] & 0x01) == 0x01) { 1111 log.debug("Function 5 on"); 1112 } else { 1113 log.debug("Function 5 off"); 1114 } 1115 } else { 1116 log.debug("Functions 9 to 12"); 1117 if ((packet[i] & 0x08) == 0x08) { 1118 log.debug("Function 12 on"); 1119 } else { 1120 log.debug("Function 12 off"); 1121 } 1122 if ((packet[i] & 0x04) == 0x04) { 1123 log.debug("Function 11 on"); 1124 } else { 1125 log.debug("Function 11 off"); 1126 } 1127 if ((packet[i] & 0x02) == 0x02) { 1128 log.debug("Function 10 on"); 1129 } else { 1130 log.debug("Function 10 off"); 1131 } 1132 if ((packet[i] & 0x01) == 0x01) { 1133 log.debug("Function 9 on"); 1134 } else { 1135 log.debug("Function 9 off"); 1136 } 1137 } 1138 // Single byte instruction 1139 // i++; 1140 } else if ((packet[i] & 0x80) == 0x80) { 1141 log.debug("For Function Group 1"); 1142 } else if ((packet[i] & 0x60) == 0x60) { 1143 speed = (packet[i] & 0xff) - 0x60; 1144 log.debug("Speed for forward 14 speed steps {}", speed); 1145 //Only a single byte instruction 1146 } else if ((packet[i] & 0x40) == 0x40) { 1147 speed = ((packet[i] & 0xff) - 0x40); 1148 log.debug("Speed for reverse 14 speed steps {}", speed); 1149 //Only a single byte instruction 1150 } else if ((packet[i] & 0x20) == 0x20) { 1151 log.debug("Advanced Op"); 1152 //Two byte instruction 1153 if ((packet[i] & 0x1f) == 0x1f) { 1154 i++; 1155 log.debug("128 speed step control"); 1156 if ((packet[i] & 0x80) == 0x80) { 1157 speed = ((packet[i]) & 0xff) - 0x80; 1158 log.debug("Forward {}", speed); 1159 } else { 1160 speed = ((packet[i]) & 0xff); 1161 log.debug("Reverse {}", speed); 1162 } 1163 } 1164 } else { 1165 log.debug("Decoder and Consist Instruction"); 1166 // decoder control is 0000 1167 // consist control is 0001 1168 } 1169 } 1170 1171 } 1172 log.debug("---End Decode DCC Packet---"); 1173 } 1174 1175 int getInputs() { 1176 return inputs; 1177 } 1178 1179 String getDescription() { 1180 return description; 1181 } 1182 1183 String getVersion() { 1184 return version; 1185 } 1186 } 1187 1188 public List<Integer> getBoards() { 1189 ArrayList<Integer> list = new ArrayList<>(); 1190 Enumeration<Integer> keys = activeBoards.keys(); 1191 while (keys.hasMoreElements()) { 1192 Integer key = keys.nextElement(); 1193 list.add(key); 1194 } // end while 1195 return list; 1196 } 1197 1198 public int getBoardInputs(int board) { 1199 if (!activeBoards.containsKey(board)) { 1200 return -1; 1201 } 1202 return activeBoards.get(board).getInputs(); 1203 } 1204 1205 public String getBoardEncodingAsString(int board) { 1206 if (!activeBoards.containsKey(board)) { 1207 return "unknown"; 1208 } 1209 return activeBoards.get(board).getEncodingAsString(); 1210 } 1211 1212 public String getBoardVersion(int board) { 1213 if (!activeBoards.containsKey(board)) { 1214 return "unknown"; 1215 } 1216 return activeBoards.get(board).getVersion(); 1217 } 1218 1219 public String getBoardDescription(int board) { 1220 if (!activeBoards.containsKey(board)) { 1221 return "unknown"; 1222 } 1223 return activeBoards.get(board).getDescription(); 1224 } 1225 1226 protected boolean isBoardCreated(int address) { 1227 return activeBoards.containsKey(address); 1228 } 1229 1230 protected void addActiveBoard(int address, String version, int inputs, int encoding) { 1231 activeBoards.put(address, new ActiveBoard(address, version, inputs, encoding)); 1232 } 1233 1234 protected void setBoardDescription(int address, String description) { 1235 ActiveBoard board = activeBoards.get(address); 1236 board.setDescription(description); 1237 } 1238 1239 protected void createSensorsForBoard(Dcc4PcReply r) { 1240 if (r.getBoard() == -1) { 1241 log.debug("Reply has no board associated with it"); 1242 return; 1243 } 1244 class SensorMaker implements Runnable { 1245 1246 Dcc4PcReply reply; 1247 1248 SensorMaker(Dcc4PcReply r) { 1249 reply = r; 1250 } 1251 1252 @Override 1253 public void run() { 1254 createSensorsFromReply(reply); 1255 } 1256 } 1257 1258 Thread thr = new Thread(new SensorMaker(r), "Dcc4PCSensor Maker board " + r.getBoard()); 1259 try { 1260 thr.start(); 1261 } catch (java.lang.IllegalThreadStateException ex) { 1262 log.error("Exception {} {}", thr.getName(), ex.getMessage()); 1263 } 1264 } 1265 1266 /** 1267 * Validates to contain at least 1 number . . . 1268 * <p> 1269 * TODO: Custom validation for Dcc4PcSensorManager could be improved. 1270 * {@inheritDoc} 1271 */ 1272 @Override 1273 @Nonnull 1274 public String validateSystemNameFormat(@Nonnull String name, @Nonnull java.util.Locale locale) throws jmri.NamedBean.BadSystemNameException { 1275 return validateTrimmedMin1NumberSystemNameFormat(name,locale); 1276 } 1277 1278 @Override 1279 public void dispose() { 1280 jmri.InstanceManager.getDefault(jmri.ShutDownManager.class).deregister(pollShutDownTask); 1281 stopPolling(); 1282 super.dispose(); 1283 } 1284 1285 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Dcc4PcSensorManager.class); 1286 1287}