001package jmri.util.usb;
002
003
004import java.awt.event.ActionEvent;
005import java.beans.PropertyChangeEvent;
006import java.beans.PropertyChangeListener;
007import java.io.File;
008import java.io.IOException;
009import java.util.Arrays;
010import java.util.concurrent.TimeUnit;
011
012import javax.annotation.Nonnull;
013import javax.swing.JMenuItem;
014import javax.swing.SwingUtilities;
015
016import jmri.*;
017import jmri.jmrit.roster.swing.RosterEntryComboBox;
018import jmri.jmrit.roster.swing.RosterEntrySelectorPanel;
019import jmri.jmrit.throttle.AddressPanel;
020import jmri.jmrit.throttle.LoadXmlThrottlesLayoutAction;
021import jmri.jmrit.throttle.ThrottleFrame;
022import jmri.jmrit.throttle.ThrottleFrameManager;
023import jmri.jmrit.throttle.ThrottleWindow;
024import jmri.util.MathUtil;
025
026import org.hid4java.*;
027import org.hid4java.event.HidServicesEvent;
028import org.slf4j.Logger;
029import org.slf4j.LoggerFactory;
030
031/**
032 * RailDriver support
033 *
034 * @author George Warner Copyright (c) 2017-2018
035 */
036public class RailDriverMenuItem extends JMenuItem implements HidServicesListener, PropertyChangeListener {
037
038    private static final short VENDOR_ID = 0x05F3;
039    private static final short PRODUCT_ID = 0x00D2;
040    public static final String SERIAL_NUMBER = null; // For later use, if not null, uncomment line 454
041
042    private HidServices hidServices = null;
043    private HidDevice hidDevice = null;
044
045
046    //TODO: Remove this if/when the RailDriver script is removed
047    //private final boolean invokeOnMenuOnly = true;
048
049    private Thread thread = null;
050    private ThrottleWindow throttleWindow = null;
051    private ThrottleFrame activeThrottleFrame = null;
052
053    public RailDriverMenuItem(String name) {
054        super();
055        initGUI(name);
056        setupListeners();
057    }
058
059    public RailDriverMenuItem() {
060        // TODO: remove "(built in)" if/when this replaces Raildriver script
061        this(Bundle.getMessage("RdBuiltIn"));
062    }
063
064    private void initGUI(String name) {
065        setText(name);
066    }
067
068    private void setupListeners() {
069        addPropertyChangeListener(this);
070
071        addActionListener((ActionEvent e) -> {
072            // menu item selected
073            log.info("RailDriverMenuItem Action!");
074
075            setupHidServices();
076
077            // Open the device device by Vendor ID, Product ID and serial number
078            hidDevice = hidServices.getHidDevice(VENDOR_ID, PRODUCT_ID, SERIAL_NUMBER);
079            if (hidDevice != null) {
080                log.info("Got RailDriver hidDevice: {}", hidDevice);
081                // Consider overriding dropReportIdZero on Windows
082                // if you see "The parameter is incorrect"
083                // HidApi.dropReportIdZero = true;
084                setupRailDriver();
085            }
086        });
087    }
088
089    protected void setupHidServices() {
090        try {
091            HidServicesSpecification hidServicesSpecification = new HidServicesSpecification();
092            hidServicesSpecification.setAutoShutdown(true);
093            hidServicesSpecification.setScanInterval(500);
094            hidServicesSpecification.setPauseInterval(5000);
095            hidServicesSpecification.setScanMode(ScanMode.SCAN_AT_FIXED_INTERVAL_WITH_PAUSE_AFTER_WRITE);
096
097            // Get HID services using custom specification
098            hidServices = HidManager.getHidServices(hidServicesSpecification);
099            hidServices.addHidServicesListener(RailDriverMenuItem.this);
100
101            // do the services have to be started here?
102            // They currently wait for the action to be triggered
103            // so that they're not starting at ctor time, e.g. in tests
104            // Provide a list of attached devices
105            //log.info("Enumerating attached devices...");
106            //for (HidDevice hidDevice : hidServices.getAttachedHidDevices()) {
107            //    log.info(hidDevice.toString());
108            //}
109            //
110  /*          if (!invokeOnMenuOnly) {
111                // start the HID services
112                InstanceManager.getDefault(ShutDownManager.class).register(hidServices::stop);
113                log.debug("Starting HID services.");
114                hidServices.start();
115
116                // Open the device device by Vendor ID, Product ID and serial number
117                hidDevice = hidServices.getHidDevice(VENDOR_ID, PRODUCT_ID, SERIAL_NUMBER);
118                if (hidDevice != null) {
119                    log.info("Got RailDriver hidDevice: {}", hidDevice);
120                    // Consider overriding dropReportIdZero on Windows
121                    // if you see "The parameter is incorrect"
122                    // HidApi.dropReportIdZero = true;
123                    setupRailDriver();
124                }
125            }*/
126        } catch (HidException ex) {
127            log.error("HidException", ex);
128        }
129    }
130
131    private void setupRailDriver() {
132        if (hidDevice != null) {
133            setLEDs("Pro");
134            speakerOn();
135
136            testRailDriver(false);  // set true to test RailDriver functions
137
138            ThrottleFrameManager tfManager = InstanceManager.getDefault(ThrottleFrameManager.class);
139
140            // if there's no active throttle frame
141            if (activeThrottleFrame == null) {
142                // we're going to try to open the default throttles layout
143                try {
144                    LoadXmlThrottlesLayoutAction lxta = new LoadXmlThrottlesLayoutAction();
145                    if (!lxta.loadThrottlesLayout(new File(ThrottleFrame.getDefaultThrottleFilename()))) {
146                        // if there's no default throttle layout...
147                        // throw this exception so we'll create a new throttle window
148                        throw new IOException();
149                    }
150                } catch (IOException ex) {
151                    //log.debug("No default throttle layout, creating an empty throttle window");
152                    // open a new throttle window and get its components
153                    throttleWindow = tfManager.createThrottleWindow();
154                    activeThrottleFrame = throttleWindow.addThrottleFrame();
155                }
156                // move throttle on screen so multiple throttles don't overlay each other
157                //throttleWindow.setLocation(400 * numThrottles, 50 * numThrottles);
158            }
159
160            // since LoadXmlThrottlesLayoutAction uses an invokeLater to
161            // open the default throttles layout then we have to delay our
162            // actions here until after that one is done.
163            SwingUtilities.invokeLater(() -> {
164                if (activeThrottleFrame == null) {
165                    throttleWindow = tfManager.getCurrentThrottleFrame();
166                    if (throttleWindow != null) {
167                        activeThrottleFrame = throttleWindow.getCurrentThrottleFrame();
168                    }
169                }
170                if (activeThrottleFrame != null) {
171                    activeThrottleFrame.toFront();
172
173                    throttleWindow.addPropertyChangeListener(this);
174                    activeThrottleFrame.addPropertyChangeListener(this);
175                }
176            });
177
178            // if I already have a thread running
179            if (thread != null) {
180                // interrupt it
181                thread.interrupt();
182                try {
183                    // wait (500 mSec) for it to die
184                    thread.join(500);
185                } catch (InterruptedException ex) {
186                    log.debug("InterruptedException", ex);
187                }
188            }
189            // start a new thread
190            thread = new Thread(() -> {
191                byte[] buff_old = new byte[14]; // read buffer
192                Arrays.fill(buff_old, (byte) 0);
193                while (!thread.isInterrupted()) {
194                    if (!hidDevice.isOpen()) {
195                        hidDevice.open();
196                    }
197                    byte[] buff_new = new byte[14]; // read buffer
198                    int ret = hidDevice.read(buff_new);
199                    if (ret >= 0) {
200                        //log.debug("hidDevice.read: {}", buff_new);
201                        for (int i = 0; i < buff_new.length; i++) {
202                            if (buff_old[i] != buff_new[i]) {
203                                if (i < 7) {
204                                    // analog values
205                                    // convert to unsigned int
206                                    int vInt = 0xFF & buff_new[i];
207                                    // convert to double (0.0 thru 1.0)
208                                    double vDouble = (256 - vInt) / 256.D;
209                                    if (i == 1) {   // throttle
210                                        // convert to float (-1.0 thru +1.0)
211                                        vDouble = (2.D * vDouble) - 1.D;
212                                    }
213                                    String name1 = String.format("Axis %d", i);
214                                    log.info("firePropertyChange(\"Value\", {}, {})", name1, vDouble);
215                                    firePropertyChange("Value", name1, Double.toString(vDouble));
216                                } else {
217                                    // digital values
218                                    byte xor = (byte) (buff_old[i] ^ buff_new[i]);
219                                    for (int bit = 0; bit < 8; bit++) {
220                                        byte mask = (byte) (1 << bit);
221                                        if (mask == (mask & xor)) {
222                                            int n = (8 * (i - 7)) + bit;
223                                            String name2 = String.format("%d", n);
224                                            boolean down = (mask == (buff_new[i] & mask));
225                                            log.info("firePropertyChange(\"Value\", {}, {})", name2, down ? "1" : "0");
226                                            firePropertyChange("Value", name2, down ? "1" : "0");
227                                        }
228                                    }
229                                }
230                                buff_old[i] = buff_new[i];
231                            }
232                        }
233                    } else {
234                        String error = hidDevice.getLastErrorMessage();
235                        if (error != null) {
236                            log.error("hidDevice.read error: {}", error);
237                        }
238                    }
239                }
240            });
241            thread.setName("RailDriver");
242            thread.start();
243        }
244    }
245
246    private void testRailDriver(boolean testFlag) {
247        if (testFlag) {
248            new Thread(() -> {
249                //
250                // this is here for testing the SevenSegmentAlpha (LED display)
251                //
252                for (int pass = 0; pass < 3; pass++) {
253                    for (char c = 'A'; c < 'Z'; c++) {
254                        StringBuilder s = new StringBuilder();
255                        for (int i = 0; i < 3; i++) {
256                            char ci = (char) (c + i);
257                            ci = (char) (((ci - 'A') % 26) + 'A');
258                            s.append(ci);
259                            if (0 == ci % 3) {
260                                s.append('.');
261                            }
262                        }
263                        setLEDs(s.toString());
264                        sleep(0.25);
265                    }
266                }
267
268                sendString("The quick brown fox jumps over the lazy dog.", 0.250);
269                sleep(2.0);
270
271                setLEDs("8.8.8.");
272                sleep(2.0);
273
274                setLEDs("???");
275                sleep(3.0);
276
277                setLEDs("Pro");
278            }).start();
279        }
280    }
281
282    /**
283     * send a string to the LED display (asynchronously)
284     *
285     * @param string what to send
286     * @param delay  how much to delay before shifting in next character
287     */
288    public void sendStringAsync(@Nonnull String string, double delay) {
289        new Thread(() -> {
290            sendString(string, delay);
291        }).start();
292    }
293
294    /**
295     * send a string to the LED display
296     *
297     * @param string what to send
298     * @param delay  how much to delay before shifting in next character
299     */
300    public void sendString(@Nonnull String string, double delay) {
301        for (int i = 0; i < string.length(); i++) {
302            StringBuilder ledstring = new StringBuilder();
303            int maxJ = 3;
304            for (int j = 0; j < maxJ; j++) {
305                if (i + j < string.length()) {
306                    char c = string.charAt(i + j);
307                    ledstring.append(c);
308                    if (c == '.') {
309                        maxJ++;
310                    }
311                } else {
312                    break;
313                }
314            }
315            setLEDs(ledstring.toString());
316            sleep(delay);
317        }
318    }
319
320    private void sleep(double delay) {
321        try {
322            TimeUnit.MILLISECONDS.sleep((long) (delay * 1000.0));
323        } catch (InterruptedException ex) {
324            log.debug("TimeUnit.sleep InterruptedException", ex);
325        }
326    }
327
328    //
329    // constants used to talk to RailDriver
330    //
331    // these are the report ID's
332    private final byte LEDCommand = (byte) 134; // Command code to set the LEDs.
333    private final byte SpeakerCommand = (byte) 133; // Command code to set the speaker state.
334
335    // Seven segment lookup table for digits ('0' thru '9')
336    private final byte SevenSegment[] = {
337        //'0'   '1'   '2'   '3'   '4'   '5'   '6'   '7'   '8'   '9'
338        0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f};
339
340    // Seven segment lookup table for alphas ('A' thru 'Z')
341    private final byte SevenSegmentAlpha[] = {
342        //'A'   'b'   'C'   'd'   'E'   'F'   'g'   'H'   'i'   'J'
343        0x77, 0x7C, 0x39, 0x5E, 0x79, 0x71, 0x6F, 0x76, 0x04, 0x1E,
344        //'K'   'L'   'm'   'n'   'o'   'P'   'q'   'r'   's'   't'
345        0x70, 0x38, 0x54, 0x23, 0x5C, 0x73, 0x67, 0x50, 0x6D, 0x44,
346        //'u'   'v'   'W'   'X'   'y'   'z'
347        0x1C, 0x62, 0x14, 0x36, 0x72, 0x49
348    };
349
350    // other seven segment display patterns
351    private final byte BLANKSEGMENT = 0x00;
352    private final byte QUESTIONMARK = 0x53;
353    private final byte DASHSEGMENT = 0x40;
354    private final byte DPSEGMENT = (byte) 0x80;
355
356    // Set the LEDS.
357    public void setLEDs(@Nonnull String ledstring) {
358        byte[] buff = new byte[7]; // Segment buffer.
359        Arrays.fill(buff, (byte) 0);
360
361        int outIdx = 2;
362        for (int i = 0; i < ledstring.length(); i++) {
363            char c = ledstring.charAt(i);
364            if (Character.isDigit(c)) {
365                //log.debug("buff[{}] = {}", outIdx, "" + c);
366                // Get seven segment code for digit.
367                buff[outIdx] = SevenSegment[c - '0'];
368            } else if (Character.isWhitespace(c)) {
369                buff[outIdx] = BLANKSEGMENT;
370            } else if (c == '_') {
371                buff[outIdx] = BLANKSEGMENT;
372            } else if (c == '?') {
373                buff[outIdx] = QUESTIONMARK;
374            } else if ((c >= 'A') && (c <= 'Z')) {
375                // Get seven segment code for alpha.
376                buff[outIdx] = SevenSegmentAlpha[c - 'A'];
377            } else if ((c >= 'a') && (c <= 'z')) {
378                // Get seven segment code for alpha.
379                buff[outIdx] = SevenSegmentAlpha[c - 'a'];
380            } else if (c == '-') {
381                buff[outIdx] = DASHSEGMENT;
382            } else // Is it a decimal point?
383            if (c == '.') {
384                // If so, OR in the decimal point segment.
385                buff[outIdx + 1] |= DPSEGMENT;
386                outIdx++;
387            } else {    // everything else is ignored
388                outIdx++;
389            }
390            outIdx--;
391            if (outIdx < 0) {
392                if (++i < ledstring.length()) {
393                    if (ledstring.charAt(i) == '.') {
394                        buff[0] |= DPSEGMENT;
395                    }
396                }
397                break;
398            }
399        }
400        sendMessage(buff, LEDCommand);
401    }   // setLEDs
402
403    public void setSpeakerOn(boolean onFlag) {
404        byte[] buff = new byte[7]; // data buffer
405        Arrays.fill(buff, (byte) 0);
406
407        buff[5] = (byte) (onFlag ? 1 : 0);      // On / off
408
409        sendMessage(buff, SpeakerCommand);
410    }   // setSpeakerOn
411
412    // Turn speaker on.
413    public void speakerOn() {
414        setSpeakerOn(true);
415    }
416
417    // Turn speaker off.
418    public void speakerOff() {
419        setSpeakerOn(false);
420    }
421
422    /**
423     * send message to hid device {p}
424     * <p>
425     * @param message   the message to send
426     * @param reportID  the report ID
427     */
428    private void sendMessage(byte[] message, byte reportID) {
429        // Ensure device is open after an attach/detach event
430        if (!hidDevice.isOpen()) {
431            hidDevice.open();
432        }
433
434        try {
435            int ret = hidDevice.write(message, message.length, reportID);
436            if (ret >= 0) {
437                log.debug("hidDevice.write returned: {}", ret);
438            } else {
439                log.error("hidDevice.write error: {}", hidDevice.getLastErrorMessage());
440            }
441        } catch (IllegalStateException ex) {
442            log.error("hidDevice.write Exception", ex);
443        }
444    }
445
446    /*
447     * {@inheritDoc}
448     */
449    @Override
450    public void hidDeviceAttached(HidServicesEvent event) {
451        log.info("hidDeviceAttached({})", event);
452/*        HidDevice tHidDevice = event.getHidDevice();
453        if ((tHidDevice.getVendorId() == VENDOR_ID) && (tHidDevice.getProductId() == PRODUCT_ID) && (!invokeOnMenuOnly) ) {
454//                && ((SERIAL_NUMBER == null) || (tHidDevice.getSerialNumber().equals(SERIAL_NUMBER))) {
455            setupRailDriver();
456        }*/
457    }
458
459    /*
460     * {@inheritDoc}
461     */
462    @Override
463    public void hidDeviceDetached(HidServicesEvent event) {
464        log.info("hidDeviceDetached({})", event);
465        if (hidDevice == event.getHidDevice()) {
466            hidDevice = null;
467        }
468    }
469
470    /*
471     * {@inheritDoc}
472     */
473    @Override
474    public void hidFailure(HidServicesEvent event) {
475        log.warn("hidFailure({})", event);
476    }
477
478    /*
479     * {@inheritDoc}
480     */
481    @Override
482    public void propertyChange(PropertyChangeEvent event) {
483        // log.debug("{}", event);
484        switch (event.getPropertyName()) {
485            case "ancestor":
486                //ancestor property change - closing throttle window
487                // Remove all property change listeners and
488                // dereference all throttle components
489                if (throttleWindow != null) {
490                    throttleWindow.removePropertyChangeListener(this);
491                    throttleWindow = null;
492                }   if (activeThrottleFrame != null) {
493                    activeThrottleFrame.removePropertyChangeListener(this);
494                    activeThrottleFrame = null;
495                }
496                // Now remove this propertyChangeListener from the model
497                //global model
498                //model.removePropertyChangeListener(self)
499                break;
500            case "ThrottleFrame":
501                //Current throttle frame changed
502                Object object = event.getNewValue();
503                //log.debug("event.newValue(): " + object);
504                if (object == null) {
505                    if (activeThrottleFrame != null) {
506                        activeThrottleFrame.removePropertyChangeListener(this);
507                        activeThrottleFrame = null;
508                    }
509                } else if (object instanceof ThrottleFrame) {
510
511                    if (throttleWindow != null) {
512                        throttleWindow.removePropertyChangeListener(this);
513                        throttleWindow = null;
514                    }
515                    if (activeThrottleFrame != null) {
516                        activeThrottleFrame.removePropertyChangeListener(this);
517                        activeThrottleFrame = null;
518                    }
519
520                    activeThrottleFrame = (ThrottleFrame) object;
521                    throttleWindow = activeThrottleFrame.getThrottleWindow();
522
523                    throttleWindow.addPropertyChangeListener(this);
524                    activeThrottleFrame.addPropertyChangeListener(this);
525
526                }
527                break;
528            case "Value":
529                String oldValue = event.getOldValue().toString();
530                String newValue = event.getNewValue().toString();
531                DccThrottle throttle = activeThrottleFrame.getAddressPanel().getThrottle();
532                AddressPanel addressPanel = activeThrottleFrame.getAddressPanel();
533                //log.info("propertyChange \"Value\" old: {}, new: {}", oldValue, newValue);
534
535                double value;
536                try {
537                    value = Double.parseDouble(newValue);
538                } catch (NumberFormatException ex) {
539                    log.error("RailDriver parse property new value ('{}')", newValue, ex);
540                    return;
541                }
542                switch (oldValue) {
543                    case "Axis 0":
544                        // REVERSER is the state of the reverser lever, values greater
545                        // than 0.5 are forward, values near to 0.5 are neutral and
546                        // values (much) less than 0.5 are reverse.
547                        log.info("REVERSER value: {}", value);
548                        if (throttle != null) {
549                            if (value < 0.45) {
550                                throttle.setIsForward(false);
551                            } else if (value > 0.55) {
552                                throttle.setIsForward(true);
553                            }
554                        }
555                        break;
556                    case "Axis 1":
557                        // THROTTLE is the state of the Throttle (and dynamic brake).  Values
558                        // (much) greater than 0.0 are for throttle (maximum throttle is
559                        // values close to 1.0), values near 0.0 are at the center position
560                        // (idle/coasting), and values (much) less than 0.0 are for dynamic
561                        // braking, with values aproaching -1.0 for full dynamic braking.
562                        log.info("THROTTLE value: {}", value);
563                        if (throttle != null) {
564                            // lever front is negative, back is positive
565                            // limit range to only positive side of lever
566                            double throttle_min = 0.125D;
567                            double throttle_max = 0.7D;
568                            double v = MathUtil.pin(value, throttle_min, throttle_max);
569                            // compute fraction (0.0 to 1.0)
570                            double fraction = (v - throttle_min) / (throttle_max - throttle_min);
571                            throttle.setSpeedSetting((float)fraction);
572                            if (value < 0) {
573                                //TODO: dynamic braking
574                                setLEDs("DBr");
575                            } else {
576                                String speed = String.format("%03d", (int) fraction*100);
577                                //log.info("speed: " + speed);
578                                setLEDs(speed);
579                            }
580                        }
581                        break;
582                    case "Axis 2":
583                        // AUTOBRAKE is the state of the Automatic (trainline) brake.  Large
584                        // values for no braking, small values for more braking.
585                        log.info("AUTOBRAKE value: {}", value);
586                        break;
587                    case "Axis 3":
588                        // INDEPENDBRK is the state of the Independent (engine only) brake.
589                        // Like the Automatic brake: large values for no braking, small
590                        // values for more braking.
591                        log.info("INDEPENDBRK value: {}", value);
592                        break;
593                    case "Axis 4":
594                        // BAILOFF is the Independent brake 'bailoff', this is the spring
595                        // loaded right movement of the Independent brake lever.  Larger
596                        // values mean the lever has been shifted right.
597                        log.info("BAILOFF value: {}", value);
598                        break;
599                    case "Axis 5":
600                        // HEADLIGHT is the state of the headlight switch.  A value below 0.5
601                        // is off, a value near 0.5 is dim, and a number much larger than 0.5
602                        // is full. This is an analog input w/detents, not a switch!
603                        log.info("HEADLIGHT value: {}", value);
604                        break;
605                    case "Axis 6":
606                        // WIPER is the state of the wiper switch.  Much like the headlight
607                        // switch, this is also an analog input w/detents, not a switch!
608                        // Small values (much less than 0.5) are off, values near 0.5 are
609                        // slow, and larger values are full.
610                        log.info("WIPER value: {}", value);
611                        break;
612                    default:
613                        log.info("FUNCTION {} value: {}", oldValue, value);
614                        boolean isDown = (value > 0.5D);
615                        int fNum ;
616                        try {
617                            fNum = Integer.parseInt(oldValue);
618                        } catch (NumberFormatException ex) {
619                            //log.error("RailDriver parse property new value ('{}') exception: {}", newValue, ex);
620                            return;
621                        }
622                        String ledString = String.format("F%d", fNum + 1);
623                        switch (fNum) {
624                            case 28: {  // zoom/rocker button up
625                                if ((addressPanel != null) && isDown) {
626                                    addressPanel.selectRosterEntry();
627                                    DccLocoAddress a = addressPanel.getCurrentAddress();
628                                    ledString = "sel " + ((a != null) ? a.toString() : "null");
629                                }
630                                break;
631                            }
632                            case 29: {  // zoom/rocker button down
633                                if ((addressPanel != null) && isDown) {
634                                    addressPanel.dispatchAddress();
635                                    DccLocoAddress a = addressPanel.getCurrentAddress();
636                                    ledString = "dis " + ((a != null) ? a.toString() : "null");
637                                }
638                                break;
639                            }
640                            case 30: {  // four way panning up
641                                if ((addressPanel != null) && isDown) {
642                                    int selectedIndex = addressPanel.getRosterSelectedIndex();
643                                    if (selectedIndex > 1) {
644                                        addressPanel.setRosterSelectedIndex(selectedIndex - 1);
645                                        ledString = String.format("Prev %d", selectedIndex - 1);
646                                    }
647                                }
648                                break;
649                            }
650                            case 31: {  // four way panning right
651                                if (isDown) {
652                                    if (throttleWindow != null) {
653                                        throttleWindow.nextThrottleFrame();
654                                    }
655                                    ledString = "NXT";
656                                }
657                                break;
658                            }
659                            case 32: {  // four way panning down
660                                if ((addressPanel != null) && isDown) {
661                                    RosterEntrySelectorPanel resp = addressPanel.getRosterEntrySelector();
662                                    if (resp != null) {
663                                        RosterEntryComboBox recb = resp.getRosterEntryComboBox();
664                                        if (recb != null) {
665                                            int cnt = recb.getItemCount();
666                                            int selectedIndex = addressPanel.getRosterSelectedIndex();
667                                            if (selectedIndex + 1 < cnt) {
668                                                try {
669                                                    addressPanel.setRosterSelectedIndex(selectedIndex + 1);
670                                                    ledString = String.format("Next %d", selectedIndex + 1);
671                                                } catch (ArrayIndexOutOfBoundsException ex) {
672                                                    // ignore this
673                                                }
674                                            }
675                                        }
676                                    }
677                                }
678                                break;
679                            }
680                            case 33: {  // four way panning left
681                                if (isDown) {
682                                    if (throttleWindow != null) {
683                                        throttleWindow.previousThrottleFrame();
684                                    }
685                                    ledString = "PRE";
686                                }
687                                break;
688                            }
689                            case 34: {  // Gear Shift Up
690                                if ((throttle != null) && isDown) {
691                                    // shuntFn
692                                    throttle.setFunction(3, false);
693                                }
694                                break;
695                            }
696                            case 35: {  // Gear Shift Down
697                                if ((throttle != null) && isDown) {
698                                    // shuntFn
699                                    throttle.setFunction(3, true);
700                                }
701                                break;
702                            }
703                            case 36:
704                            case 37: {  // Emergency Brake up/down
705                                if ((throttle != null) && isDown) {
706                                    throttle.setSpeedSetting(-1);
707                                }
708                                break;
709                            }
710
711                            case 38: {  // Alerter
712                                if (isDown) {
713                                    fNum = 6;   // alertFn
714                                }
715                                break;
716                            }
717                            case 39: {  // Sander
718                                if (isDown) {
719                                    fNum = 7;   // sandFn
720                                }
721                                break;
722                            }
723                            case 40: {  // Pantograph
724                                if (isDown) {
725                                    fNum = 8;   // pantoFn
726                                }
727                                break;
728                            }
729                            case 41: {  // Bell
730                                if (isDown) {
731                                    fNum = 1;   // bellFn
732                                }
733                                break;
734                            }
735                            case 42:
736                            case 43: {  // Horn/Whistle
737                                fNum = 2;   // hornFn
738                                break;
739                            }
740                            default: {
741                                break;
742                            }
743                        }
744                        if (throttle != null && fNum > 0 && fNum < throttle.getFunctions().length)  {
745                            if (! throttle.getFunctionMomentary(fNum)) {
746                                if (isDown) {
747                                    throttle.setFunction(fNum, !throttle.getFunction(fNum) );
748                                }
749                            } else {
750                                throttle.setFunction(fNum, isDown);
751                            }
752                        }
753                        if (isDown) {
754                            if (ledString.length() <= 3) {
755                                setLEDs(ledString);
756                            } else {
757                                sendStringAsync(ledString, 0.333);
758                            }
759                        }
760                        break; // if (oldValue.equals(...) {} else...
761                }
762                break;
763            default:
764                break;
765        }
766    }   // propertyChange
767
768    //initialize logging
769    private transient final static Logger log = LoggerFactory.getLogger(RailDriverMenuItem.class);
770
771}