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     * <p>
663     * @param message
664     * Format: HMmessage
665     */
666    @Override
667    public void sendAlertMessage(String message) {
668        sendPacketToDevice("HM" + message);
669    }
670
671    /**
672     * Send an Info message (simple text string) to this client
673     * <p>
674     * @param message
675     * Format: Hmmessage
676     */
677    @Override
678    public void sendInfoMessage(String message) {
679        sendPacketToDevice("Hm" + message);
680    }
681
682
683
684    /**
685     * Add a DeviceListener
686     *
687     * @param l handle for listener to add
688     *
689     */
690    public void addDeviceListener(DeviceListener l) {
691        if (!listeners.contains(l)) {
692            listeners.add(l);
693        }
694    }
695
696    /**
697     * Remove a DeviceListener
698     *
699     * @param l listener to remove
700     *
701     */
702    public void removeDeviceListener(DeviceListener l) {
703        if (listeners.contains(l)) {
704            listeners.remove(l);
705        }
706    }
707
708    @Override
709    public void notifyControllerAddressFound(ThrottleController TC) {
710
711        for (int i = 0; i < listeners.size(); i++) {
712            DeviceListener l = listeners.get(i);
713            l.notifyDeviceAddressChanged(this);
714            if (log.isDebugEnabled()) {
715                log.debug("Notify DeviceListener: {} address: {}", l.getClass(), TC.getCurrentAddressString());
716            }
717        }
718    }
719
720    @Override
721    public void notifyControllerAddressReleased(ThrottleController TC) {
722
723        for (int i = 0; i < listeners.size(); i++) {
724            DeviceListener l = listeners.get(i);
725            l.notifyDeviceAddressChanged(this);
726            if (log.isDebugEnabled()) {
727                log.debug("Notify DeviceListener: {} address: {}", l.getClass(), TC.getCurrentAddressString());
728            }
729        }
730
731    }
732
733    /**
734     * System has declined the address request, may be an in-use address. Need
735     * to clear the address from the proper multiThrottle.
736     *
737     * @param tc      The throttle controller that was listening for a response
738     *                to an address request
739     * @param address The address to send a cancel to
740     * @param reason  The reason the request was declined, to be sent back to client
741     */
742    @Override
743    public void notifyControllerAddressDeclined(ThrottleController tc, DccLocoAddress address, String reason) {
744        log.warn("notifyControllerAddressDeclined: {}", reason);
745        sendAlertMessage(reason); // let the client know why the request failed
746        if (multiThrottles != null) {   //  Should exist by this point
747            jmri.InstanceManager.throttleManagerInstance().cancelThrottleRequest(address, tc);
748            multiThrottles.get(tc.whichThrottle).canceledThrottleRequest(tc.locoKey);
749        }
750    }
751
752    /**
753     * Format a package to be sent to the device for roster list selections.
754     *
755     * @return String containing a formatted list of some of each RosterEntry's
756     *         info. Include a header with the length of the string to be
757     *         received.
758     */
759    public String sendRoster() {
760        List<RosterEntry> rosterList;
761        rosterList = Roster.getDefault().getEntriesInGroup(manager.getSelectedRosterGroup());
762        StringBuilder rosterString = new StringBuilder(rosterList.size() * 25);
763        for (RosterEntry entry : rosterList) {
764            StringBuilder entryInfo = new StringBuilder(entry.getId()); //  Start with name
765            entryInfo.append("}|{");
766            entryInfo.append(entry.getDccAddress());
767            if (entry.isLongAddress()) { //  Append length value
768                entryInfo.append("}|{L");
769            } else {
770                entryInfo.append("}|{S");
771            }
772
773            rosterString.append("]\\[");  //  Put this info in as an item
774            rosterString.append(entryInfo);
775
776        }
777        rosterString.trimToSize();
778
779        return ("RL" + rosterList.size() + rosterString);
780    }
781
782    private final static Logger log = LoggerFactory.getLogger(DeviceServer.class);
783
784}