001package jmri.jmrit.withrottle; 002 003/** 004 * WiThrottle 005 * 006 * @author Brett Hoffman Copyright (C) 2009, 2010 007 * @author Created by Brett Hoffman on: 008 * @author 7/20/09. 009 * 010 * Thread with input and output streams for each connected device. Creates an 011 * invisible throttle window for each. 012 * 013 * Sorting codes: 014 * 'T'hrottle - sends to throttleController 015 * 'S'econdThrottle - sends to secondThrottleController 016 * 'C' - Not used anymore except to provide backward compliance, same as 'T' 017 * 'N'ame of device 018 * 'H' hardware info - followed by: 019 * 'U' UDID - unique device identifier 020 * 'P' panel - followed by: 021 * 'P' track power 022 * 'T' turnouts 023 * 'R' routes 024 * 'R' roster - followed by: 025 * 'C' consists 026 * 'Q'uit - device has quit, close its throttleWindow 027 * '*' - heartbeat from client device ('*+' starts, '*-' stops) 028 * 029 * Added in v2.0: 'M'ultiThrottle - forwards to MultiThrottle class, see notes 030 * there for use. Followed by id character to create or control appropriate 031 * DccThrottle. Stored as HashTable for access to 'T' and 'S' throttles. 032 * 033 * 'D'irect byte packet to rails Followed by one digit for repeats, then 034 * followed by hex pairs, (single spaced) including pair for error byte. D200 90 035 * 90 - Send '00 90 90' twice, with error byte '90' 036 * 037 * 038 * Out to client, all newline terminated, cannot have newlines in the message: 039 * 040 * Track power: 'PPA' + '0' (off), '1' (on), '2' (unknown) Minimum package 041 * length of 4 char. 042 * 043 * Send Info on routes to devices, not specific to any one route. Format: 044 * PRT]\[value}|{routeKey]\[value}|{ActiveKey]\[value}|{InactiveKey 045 * 046 * Send list of routes Format: 047 * PRL]\[SysName}|{UsrName}|{CurrentState]\[SysName}|{UsrName}|{CurrentState 048 * States: 1 - UNKNOWN, 2 - ACTIVE, 4 - INACTIVE (based on turnoutsAligned 049 * sensor, if used) 050 * 051 * Send Info on turnouts to devices, not specific to any one turnout. Format: 052 * PTT]\[value}|{turnoutKey]\[value}|{closedKey]\[value}|{thrownKey 053 * Send list of turnouts Format: 054 * PTL]\[SysName}|{UsrName}|{CurrentState]\[SysName}|{UsrName}|{CurrentState 055 * States: 1 - UNKNOWN, 2 - CLOSED, 4 - THROWN 056 * 057 * Send time or time&rate: 058 * 'PFT' + UTCAdjustedTimeSeconds 059 * -OR- 060 * 'PFT' + UTCAdjustedTimeSeconds + "<;>" + RateMultipier 061 * Set rate to 0.0 for stop, float value to run. 062 * 063 * Web server port: 'PW' + {port#} 064 * 065 * Roster is sent formatted: ]\[ separates roster entries, }|{ separates info in 066 * each entry e.g. RL###]\[RVRR1201}|{1201}|{L]\[Limited}|{8165}|{L]\[ 067 * 068 * Function labels: RF## first throttle, or RS## second throttle, each label 069 * separated by ]\[ e.g. RF29]\[Light]\[Bell]\[Horn]\[Short Horn]\[ &etc. 070 * 071 * RSF 'R'oster 'P'roperties 'F'unctions 072 * 073 * Heartbeat send '*0' to tell device to stop heartbeat, '*#' # = number of 074 * seconds until eStop. This class sends initial to device, but does not start 075 * monitoring until it gets a response of '*+' Device should send heartbeat to 076 * server in shorter time than eStop 077 * 078 * Alert message: 'HM' + message to display. 079 * Info message: 'Hm' + message to display. Same as HM, but lower priority. 080 * 081 * Server Type message: 'HT' + type. Always 'JMRI' for this server. 082 * Server Description message: 'Ht' + message. Includes version and railroad name. 083 * 084 */ 085import java.io.BufferedReader; 086import java.io.IOException; 087import java.io.InputStreamReader; 088import java.io.PrintStream; 089import java.net.Socket; 090import java.util.ArrayList; 091import java.util.HashMap; 092import java.util.List; 093import java.util.TimerTask; 094import jmri.CommandStation; 095import jmri.DccLocoAddress; 096import jmri.InstanceManager; 097import jmri.jmrit.roster.Roster; 098import jmri.jmrit.roster.RosterEntry; 099import jmri.util.ThreadingUtil; 100import jmri.web.server.WebServerPreferences; 101import jmri.web.servlet.ServletUtil; 102 103import org.slf4j.Logger; 104import org.slf4j.LoggerFactory; 105 106public class DeviceServer implements Runnable, ThrottleControllerListener, ControllerInterface { 107 108 // Manually increment as features are added 109 private static final String VERSION_NUMBER = "2.0"; 110 111 private Socket device; 112 private final CommandStation cmdStation = jmri.InstanceManager.getNullableDefault(CommandStation.class); 113 String newLine = System.getProperty("line.separator"); 114 BufferedReader in = null; 115 PrintStream out = null; 116 private final ArrayList<DeviceListener> listeners = new ArrayList<>(); 117 String deviceName = "Unknown"; 118 String deviceUDID; 119 120 ThrottleController throttleController; 121 ThrottleController secondThrottleController; 122 HashMap<Character, MultiThrottle> multiThrottles; 123 private boolean keepReading; 124 private boolean isUsingHeartbeat = false; 125 private boolean heartbeat = true; 126 private int pulseInterval = 16; // seconds til disconnect 127 private TimerTask ekgTask; 128 private int stopEKGCount; 129 130 private TrackPowerController trackPower = null; 131 final boolean isTrackPowerAllowed = InstanceManager.getDefault(WiThrottlePreferences.class).isAllowTrackPower(); 132 private TurnoutController turnoutC = null; 133 private RouteController routeC = null; 134 final boolean isTurnoutAllowed = InstanceManager.getDefault(WiThrottlePreferences.class).isAllowTurnout(); 135 final boolean isRouteAllowed = InstanceManager.getDefault(WiThrottlePreferences.class).isAllowRoute(); 136 private ConsistController consistC = null; 137 private boolean isConsistAllowed; 138 private FastClockController fastClockC = null; 139 final boolean isClockDisplayed = InstanceManager.getDefault(WiThrottlePreferences.class).isDisplayFastClock(); 140 final String railroadName = InstanceManager.getDefault(ServletUtil.class).getRailroadName(false); 141 142 private DeviceManager manager; 143 144 DeviceServer(Socket socket, DeviceManager manager) { 145 this.device = socket; 146 this.manager = manager; 147 148 try { 149 if (log.isDebugEnabled()) { 150 log.debug("Creating input stream reader for {}", device.getRemoteSocketAddress()); 151 } 152 in = new BufferedReader(new InputStreamReader(device.getInputStream(), "UTF8")); 153 if (log.isDebugEnabled()) { 154 log.debug("Creating output stream writer for {}", device.getRemoteSocketAddress()); 155 } 156 out = new PrintStream(device.getOutputStream(), true, "UTF8"); 157 158 } catch (IOException e) { 159 log.error("Stream creation failed (DeviceServer)"); 160 return; 161 } 162 sendPacketToDevice("VN" + getWiTVersion()); 163 sendPacketToDevice("HTJMRI"); 164 sendPacketToDevice("HtJMRI " + jmri.Version.getCanonicalVersion() + 165 " " + railroadName); 166 sendPacketToDevice(sendRoster()); 167 addControllers(); 168 sendPacketToDevice("PW" + getWebServerPort()); 169 170 } 171 172 @Override 173 public void run() { 174 for (int i = 0; i < listeners.size(); i++) { 175 DeviceListener l = listeners.get(i); 176 log.debug("Notify Device Add"); 177 l.notifyDeviceConnected(this); 178 179 } 180 String inPackage = null; 181 182 keepReading = true; // Gets set to false when device sends 'Q'uit 183 int consecutiveErrors = 0; 184 185 do { 186 try { 187 inPackage = in.readLine(); 188 189 if (inPackage != null) { 190 heartbeat = true; // Any contact will keep alive 191 consecutiveErrors = 0; //reset error counter 192 if (log.isDebugEnabled()) { 193 String s = inPackage + " "; //pad output so messages form columns 194 s = s.substring(0, Math.max(inPackage.length(), 20)); 195 log.debug("Rcvd: {} from {}{}", s, getName(), device.getRemoteSocketAddress()); 196 } 197 198 switch (inPackage.charAt(0)) { 199 case 'T': { 200 if (throttleController == null) { 201 throttleController = new ThrottleController('T', this, this); 202 } 203 keepReading = throttleController.sort(inPackage.substring(1)); 204 break; 205 } 206 207 case 'S': { 208 if (secondThrottleController == null) { 209 secondThrottleController = new ThrottleController('S', this, this); 210 } 211 keepReading = secondThrottleController.sort(inPackage.substring(1)); 212 break; 213 } 214 215 case 'M': { // MultiThrottle M(id character)('A'ction '+' or '-')(message) 216 if (multiThrottles == null) { 217 multiThrottles = new HashMap<>(1); 218 } 219 char id = inPackage.charAt(1); 220 if (!multiThrottles.containsKey(id)) { // Create a MT if this is a new id 221 multiThrottles.put(id, new MultiThrottle(id, this, this)); 222 } 223 224 // Strips 'M' and id, forwards rest 225 multiThrottles.get(id).handleMessage(inPackage.substring(2)); 226 227 break; 228 } 229 230 case 'D': { 231 if (log.isDebugEnabled()) { 232 log.debug("Sending hex packet: {} to command station.", inPackage.substring(2)); 233 } 234 int repeats = Character.getNumericValue(inPackage.charAt(1)); 235 byte[] packet = jmri.util.StringUtil.bytesFromHexString(inPackage.substring(2)); 236 cmdStation.sendPacket(packet, repeats); 237 break; 238 } 239 240 case '*': { // Heartbeat only 241 242 if (inPackage.length() > 1) { 243 switch (inPackage.charAt(1)) { 244 245 case '+': { // trigger, turns on timed monitoring 246 if (!isUsingHeartbeat) { 247 startEKG(); 248 } 249 break; 250 } 251 252 case '-': { // turns off 253 if (isUsingHeartbeat) { 254 stopEKG(); 255 } 256 break; 257 } 258 default: 259 log.warn("Unhandled code: {}", inPackage.charAt(1)); 260 break; 261 } 262 263 } 264 265 break; 266 } // end heartbeat block 267 268 case 'C': { // Prefix for confirmed package 269 switch (inPackage.charAt(1)) { 270 case 'T': { 271 keepReading = throttleController.sort(inPackage.substring(2)); 272 273 break; 274 } 275 276 default: { 277 log.warn("Received unknown network package: {}", inPackage); 278 279 break; 280 } 281 } 282 283 break; 284 } 285 286 case 'N': { // Prefix for deviceName 287 deviceName = inPackage.substring(1); 288 log.info("Received Name: {}", deviceName); 289 290 if (InstanceManager.getDefault(WiThrottlePreferences.class).isUseEStop()) { 291 pulseInterval = InstanceManager.getDefault(WiThrottlePreferences.class).getEStopDelay(); 292 sendPacketToDevice("*" + pulseInterval); // Turn on heartbeat, if used 293 } 294 break; 295 } 296 297 case 'H': { // Hardware 298 switch (inPackage.charAt(1)) { 299 case 'U': 300 deviceUDID = inPackage.substring(2); 301 for (int i = 0; i < listeners.size(); i++) { 302 DeviceListener l = listeners.get(i); 303 l.notifyDeviceInfoChanged(this); 304 } 305 break; 306 default: 307 log.warn("Unhandled code: {}", inPackage.charAt(1)); 308 break; 309 } 310 311 break; 312 } // end hardware block 313 314 case 'P': { // Start 'P'anel case 315 switch (inPackage.charAt(1)) { 316 case 'P': { 317 if (isTrackPowerAllowed) { 318 trackPower.handleMessage(inPackage.substring(2), this); 319 } 320 break; 321 } 322 case 'T': { 323 if (isTurnoutAllowed) { 324 turnoutC.handleMessage(inPackage.substring(2), this); 325 } 326 break; 327 } 328 case 'R': { 329 if (isRouteAllowed) { 330 routeC.handleMessage(inPackage.substring(2), this); 331 } 332 break; 333 } 334 default: 335 log.warn("Unhandled code {} {}", inPackage.charAt(1), this); 336 break; 337 } 338 break; 339 } // end panel block 340 341 case 'R': { // Start 'R'oster case 342 switch (inPackage.charAt(1)) { 343 case 'C': 344 if (isConsistAllowed) { 345 consistC.handleMessage(inPackage.substring(2), this); 346 } 347 break; 348 default: 349 log.warn("Unhandled code: {}", inPackage.charAt(1)); 350 break; 351 } 352 353 break; 354 } // end roster block 355 356 case 'Q': { 357 keepReading = false; 358 break; 359 } 360 361 default: { // If an unknown makes it through, do nothing. 362 log.warn("Received unknown network package: {}", inPackage); 363 break; 364 } 365 366 } //End of charAt(0) switch block 367 368 inPackage = null; 369 } else { //in.readLine() IS null 370 consecutiveErrors += 1; 371 log.warn("null readLine() from device '{}', consecutive error # {}", getName(), consecutiveErrors); 372 } 373 374 } catch (IOException exa) { 375 consecutiveErrors += 1; 376 log.warn("readLine from device '{}' failed, consecutive error # {}", getName(), consecutiveErrors); 377 } catch (IndexOutOfBoundsException exb) { 378 log.warn("Bad message '{}' from device '{}'", inPackage, getName()); 379 } 380 if (consecutiveErrors > 0) { //a read error was encountered 381 if (consecutiveErrors < 25) { //pause thread to give time for reconnection 382 try { 383 Thread.sleep(200); 384 } catch (java.lang.InterruptedException ex) { 385 } 386 } else { 387 keepReading = false; 388 log.error("readLine failure limit exceeded, ending thread run loop for device '{}'", getName()); 389 } 390 } 391 } while (keepReading); // 'til we tell it to stop 392 log.debug("Ending thread run loop for device '{}'", getName()); 393 closeThrottles(); 394 395 } 396 397 public void closeThrottles() { 398 stopEKG(); 399 if (throttleController != null) { 400 throttleController.shutdownThrottle(); 401 throttleController.removeThrottleControllerListener(this); 402 throttleController.removeControllerListener(this); 403 } 404 if (secondThrottleController != null) { 405 secondThrottleController.shutdownThrottle(); 406 secondThrottleController.removeThrottleControllerListener(this); 407 secondThrottleController.removeControllerListener(this); 408 } 409 if (multiThrottles != null) { 410 for (char key : multiThrottles.keySet()) { 411 log.debug("Closing throttles for key: {} for device: {}", key, getName()); 412 multiThrottles.get(key).dispose(); 413 } 414 } 415 if (multiThrottles != null) { 416 multiThrottles.clear(); 417 multiThrottles = null; 418 } 419 throttleController = null; 420 secondThrottleController = null; 421 if (trackPower != null) { 422 trackPower.removeControllerListener(this); 423 } 424 if (turnoutC != null) { 425 turnoutC.removeControllerListener(this); 426 } 427 if (routeC != null) { 428 routeC.removeControllerListener(this); 429 } 430 if (consistC != null) { 431 consistC.removeControllerListener(this); 432 } 433 if (fastClockC != null) { 434 fastClockC.removeControllerListener(this); 435 } 436 437 closeSocket(); 438 for (int i = 0; i < listeners.size(); i++) { 439 DeviceListener l = listeners.get(i); 440 l.notifyDeviceDisconnected(this); 441 442 } 443 } 444 445 public void closeSocket() { 446 447 keepReading = false; 448 try { 449 if (device.isClosed()) { 450 if (log.isDebugEnabled()) { 451 log.debug("device socket {}{} already closed.", getName(), device.getRemoteSocketAddress()); 452 } 453 } else { 454 device.close(); 455 if (log.isDebugEnabled()) { 456 log.debug("device socket {}{} closed.", getName(), device.getRemoteSocketAddress()); 457 } 458 } 459 } catch (IOException e) { 460 if (log.isDebugEnabled()) { 461 log.debug("device socket {}{} close failed with IOException.", getName(), device.getRemoteSocketAddress()); 462 } 463 } 464 } 465 466 public void startEKG() { 467 log.debug("starting heartbeat EKG for '{}' with interval: {}", getName(), pulseInterval); 468 isUsingHeartbeat = true; 469 stopEKGCount = 0; 470 ekgTask = new TimerTask() { 471 @Override 472 public void run() { // Drops on second pass 473 ThreadingUtil.runOnLayout(() -> { 474 if (!heartbeat) { 475 stopEKGCount++; 476 // Send eStop to each throttle 477 if (log.isDebugEnabled()) { 478 log.debug("Lost signal from: {}, sending eStop", getName()); 479 } 480 if (throttleController != null) { 481 throttleController.sort("X"); 482 } 483 if (secondThrottleController != null) { 484 secondThrottleController.sort("X"); 485 } 486 if (multiThrottles != null) { 487 for (char key : multiThrottles.keySet()) { 488 if (log.isDebugEnabled()) { 489 log.debug("Sending eStop to MT key: {}", key); 490 } 491 multiThrottles.get(key).eStop(); 492 } 493 494 } 495 if (stopEKGCount > 2) { 496 closeThrottles(); 497 } 498 } 499 heartbeat = false; 500 }); 501 } 502 503 }; 504 jmri.util.TimerUtil.scheduleAtFixedRate(ekgTask, pulseInterval * 900L, pulseInterval * 900L); 505 } 506 507 public void stopEKG() { 508 isUsingHeartbeat = false; 509 if (ekgTask != null) { 510 ekgTask.cancel(); 511 } 512 513 } 514 515 private void addControllers() { 516 if (isTrackPowerAllowed) { 517 trackPower = InstanceManager.getDefault(WiThrottleManager.class).getTrackPowerController(); 518 if (trackPower.isValid) { 519 if (log.isDebugEnabled()) { 520 log.debug("Track Power valid."); 521 } 522 trackPower.addControllerListener(this); 523 trackPower.sendCurrentState(); 524 } 525 } 526 if (isTurnoutAllowed) { 527 turnoutC = InstanceManager.getDefault(WiThrottleManager.class).getTurnoutController(); 528 if (turnoutC.verifyCreation()) { 529 if (log.isDebugEnabled()) { 530 log.debug("Turnout Controller valid."); 531 } 532 turnoutC.addControllerListener(this); 533 turnoutC.sendTitles(); 534 turnoutC.sendList(); 535 } 536 } 537 if (isRouteAllowed) { 538 routeC = InstanceManager.getDefault(WiThrottleManager.class).getRouteController(); 539 if (routeC.verifyCreation()) { 540 if (log.isDebugEnabled()) { 541 log.debug("Route Controller valid."); 542 } 543 routeC.addControllerListener(this); 544 routeC.sendTitles(); 545 routeC.sendList(); 546 } 547 } 548 549 // Consists can be selected regardless of pref, as long as there is a ConsistManager. 550 consistC = InstanceManager.getDefault(WiThrottleManager.class).getConsistController(); 551 if (consistC.verifyCreation()) { 552 if (log.isDebugEnabled()) { 553 log.debug("Consist Controller valid."); 554 } 555 isConsistAllowed = InstanceManager.getDefault(WiThrottlePreferences.class).isAllowConsist(); 556 consistC.addControllerListener(this); 557 consistC.setIsConsistAllowed(isConsistAllowed); 558 consistC.sendConsistListType(); 559 560 consistC.sendAllConsistData(); 561 } 562 if (isClockDisplayed) { 563 fastClockC = InstanceManager.getDefault(WiThrottleManager.class).getFastClockController(); 564 if (fastClockC.verifyCreation()) { 565 if (log.isDebugEnabled()) { 566 log.debug("Fast Clock Controller valid."); 567 } 568 fastClockC.addControllerListener(this); 569 fastClockC.sendFastTimeAndRate(); 570 } 571 } 572 } 573 574 public String getUDID() { 575 return deviceUDID; 576 } 577 578 public String getName() { 579 return deviceName; 580 } 581 582 public String getCurrentAddressString() { 583 StringBuilder s = new StringBuilder(""); 584 if (throttleController != null) { 585 s.append(throttleController.getCurrentAddressString()); 586 s.append(" "); 587 } 588 if (secondThrottleController != null) { 589 s.append(secondThrottleController.getCurrentAddressString()); 590 s.append(" "); 591 } 592 if (multiThrottles != null) { 593 for (MultiThrottle mt : multiThrottles.values()) { 594 if (mt.throttles != null) { 595 for (MultiThrottleController mtc : mt.throttles.values()) { 596 s.append(mtc.getCurrentAddressString()); 597 s.append(" "); 598 } 599 } 600 } 601 } 602 return s.toString(); 603 } 604 605 /** 606 * Get the Roster ID String. 607 * 608 * @since 4.15.4 609 * @return roster ID string. 610 */ 611 public String getCurrentRosterIdString() { 612 StringBuilder s = new StringBuilder(""); 613 if (throttleController != null) { 614 s.append(throttleController.getCurrentRosterIdString()); 615 s.append(" "); 616 } 617 if (secondThrottleController != null) { 618 s.append(secondThrottleController.getCurrentRosterIdString()); 619 s.append(" "); 620 } 621 if (multiThrottles != null) { 622 for (MultiThrottle mt : multiThrottles.values()) { 623 if (mt.throttles != null) { 624 for (MultiThrottleController mtc : mt.throttles.values()) { 625 s.append(mtc.getCurrentRosterIdString()); 626 s.append(" "); 627 } 628 } 629 } 630 } 631 return s.toString(); 632 } 633 634 public static String getWiTVersion() { 635 return VERSION_NUMBER; 636 } 637 638 public static String getWebServerPort() { 639 return Integer.toString(InstanceManager.getDefault(WebServerPreferences.class).getPort()); 640 } 641 642 /** 643 * Called by various Controllers to send a string message to a connected 644 * device. Appends a newline to the end. 645 * 646 * @param message The string to send. 647 */ 648 @Override 649 public void sendPacketToDevice(String message) { 650 if (message == null) { 651 return; // Do not send a null. 652 } 653 out.println(message + newLine); 654 if (log.isDebugEnabled()) { 655 String s = message + " "; //pad output so messages form columns 656 s = s.substring(0, Math.max(message.length(), 20)); 657 log.debug("Sent: {} to {}{}", s, getName(), device.getRemoteSocketAddress()); 658 } 659 } 660 /** 661 * Send an Alert message (simple text string) to this client 662 * @param message 663 * Format: HMmessage 664 */ 665 @Override 666 public void sendAlertMessage(String message) { 667 sendPacketToDevice("HM" + message); 668 } 669 670 /** 671 * Send an Info message (simple text string) to this client 672 * 673 * @param message 674 * Format: Hmmessage 675 */ 676 @Override 677 public void sendInfoMessage(String message) { 678 sendPacketToDevice("Hm" + message); 679 } 680 681 682 683 /** 684 * Add a DeviceListener 685 * 686 * @param l handle for listener to add 687 * 688 */ 689 public void addDeviceListener(DeviceListener l) { 690 if (!listeners.contains(l)) { 691 listeners.add(l); 692 } 693 } 694 695 /** 696 * Remove a DeviceListener 697 * 698 * @param l listener to remove 699 * 700 */ 701 public void removeDeviceListener(DeviceListener l) { 702 if (listeners.contains(l)) { 703 listeners.remove(l); 704 } 705 } 706 707 @Override 708 public void notifyControllerAddressFound(ThrottleController TC) { 709 710 for (int i = 0; i < listeners.size(); i++) { 711 DeviceListener l = listeners.get(i); 712 l.notifyDeviceAddressChanged(this); 713 if (log.isDebugEnabled()) { 714 log.debug("Notify DeviceListener: {} address: {}", l.getClass(), TC.getCurrentAddressString()); 715 } 716 } 717 } 718 719 @Override 720 public void notifyControllerAddressReleased(ThrottleController TC) { 721 722 for (int i = 0; i < listeners.size(); i++) { 723 DeviceListener l = listeners.get(i); 724 l.notifyDeviceAddressChanged(this); 725 if (log.isDebugEnabled()) { 726 log.debug("Notify DeviceListener: {} address: {}", l.getClass(), TC.getCurrentAddressString()); 727 } 728 } 729 730 } 731 732 /** 733 * System has declined the address request, may be an in-use address. Need 734 * to clear the address from the proper multiThrottle. 735 * 736 * @param tc The throttle controller that was listening for a response 737 * to an address request 738 * @param address The address to send a cancel to 739 * @param reason The reason the request was declined, to be sent back to client 740 */ 741 @Override 742 public void notifyControllerAddressDeclined(ThrottleController tc, DccLocoAddress address, String reason) { 743 log.warn("notifyControllerAddressDeclined: {}", reason); 744 sendAlertMessage(reason); // let the client know why the request failed 745 if (multiThrottles != null) { // Should exist by this point 746 jmri.InstanceManager.throttleManagerInstance().cancelThrottleRequest(address, tc); 747 multiThrottles.get(tc.whichThrottle).canceledThrottleRequest(tc.locoKey); 748 } 749 } 750 751 /** 752 * Format a package to be sent to the device for roster list selections. 753 * 754 * @return String containing a formatted list of some of each RosterEntry's 755 * info. Include a header with the length of the string to be 756 * received. 757 */ 758 public String sendRoster() { 759 List<RosterEntry> rosterList; 760 rosterList = Roster.getDefault().getEntriesInGroup(manager.getSelectedRosterGroup()); 761 StringBuilder rosterString = new StringBuilder(rosterList.size() * 25); 762 for (RosterEntry entry : rosterList) { 763 StringBuilder entryInfo = new StringBuilder(entry.getId()); // Start with name 764 entryInfo.append("}|{"); 765 entryInfo.append(entry.getDccAddress()); 766 if (entry.isLongAddress()) { // Append length value 767 entryInfo.append("}|{L"); 768 } else { 769 entryInfo.append("}|{S"); 770 } 771 772 rosterString.append("]\\["); // Put this info in as an item 773 rosterString.append(entryInfo); 774 775 } 776 rosterString.trimToSize(); 777 778 return ("RL" + rosterList.size() + rosterString); 779 } 780 781 private final static Logger log = LoggerFactory.getLogger(DeviceServer.class); 782 783}