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.throttle.ThrottleFrameManager;
018import jmri.jmrit.roster.swing.RosterEntryComboBox;
019import jmri.jmrit.roster.swing.RosterEntrySelectorPanel;
020import jmri.jmrit.throttle.LoadXmlThrottlesLayoutAction;
021import jmri.jmrit.throttle.ThrottleWindow;
022import jmri.jmrit.throttle.implementation.ThrottleFrame;
023import jmri.jmrit.throttle.implementation.ThrottleUICore;
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(ThrottleUICore.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 = (ThrottleFrame) throttleWindow.newThrottleController();
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 = (ThrottleWindow)tfManager.getCurentThrottleController();
166                    if (throttleWindow != null) {
167                        activeThrottleFrame = throttleWindow.getCurentThrottleController();
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                    if (throttleWindow != null) {
511                        throttleWindow.removePropertyChangeListener(this);
512                        throttleWindow = null;
513                    }
514                    if (activeThrottleFrame != null) {
515                        activeThrottleFrame.removePropertyChangeListener(this);
516                        activeThrottleFrame = null;
517                    }
518                    activeThrottleFrame = (ThrottleFrame) object;
519                    throttleWindow = activeThrottleFrame.getThrottleControllersContainer();
520                    throttleWindow.addPropertyChangeListener(this);
521                    activeThrottleFrame.addPropertyChangeListener(this);                   
522                }
523                break;
524            case "Value":
525                String oldValue = event.getOldValue().toString();
526                String newValue = event.getNewValue().toString();
527                DccThrottle throttle = activeThrottleFrame.getThrottle();
528                log.debug("propertyChange \"Value\" old: {}, new: {}", oldValue, newValue);
529
530                double value;
531                try {
532                    value = Double.parseDouble(newValue);
533                } catch (NumberFormatException ex) {
534                    log.error("RailDriver parse property new value ('{}')", newValue, ex);
535                    return;
536                }
537                switch (oldValue) {
538                    case "Axis 0":
539                        // REVERSER is the state of the reverser lever, values greater
540                        // than 0.5 are forward, values near to 0.5 are neutral and
541                        // values (much) less than 0.5 are reverse.
542                        log.info("REVERSER value: {}", value);
543                        if (throttle != null) {
544                            if (value < 0.45) {
545                                throttle.setIsForward(false);
546                            } else if (value > 0.55) {
547                                throttle.setIsForward(true);
548                            }
549                        }
550                        break;
551                    case "Axis 1":
552                        // THROTTLE is the state of the Throttle (and dynamic brake).  Values
553                        // (much) greater than 0.0 are for throttle (maximum throttle is
554                        // values close to 1.0), values near 0.0 are at the center position
555                        // (idle/coasting), and values (much) less than 0.0 are for dynamic
556                        // braking, with values aproaching -1.0 for full dynamic braking.
557                        log.info("THROTTLE value: {}", value);
558                        if (throttle != null) {
559                            // lever front is negative, back is positive
560                            // limit range to only positive side of lever
561                            double throttle_min = 0.125D;
562                            double throttle_max = 0.7D;
563                            double v = MathUtil.pin(value, throttle_min, throttle_max);
564                            // compute fraction (0.0 to 1.0)
565                            double fraction = (v - throttle_min) / (throttle_max - throttle_min);
566                            throttle.setSpeedSetting((float)fraction);
567                            if (value < 0) {
568                                //TODO: dynamic braking
569                                setLEDs("DBr");
570                            } else {
571                                String speed = String.format("%03d", (int) fraction*100);
572                                //log.info("speed: " + speed);
573                                setLEDs(speed);
574                            }
575                        }
576                        break;
577                    case "Axis 2":
578                        // AUTOBRAKE is the state of the Automatic (trainline) brake.  Large
579                        // values for no braking, small values for more braking.
580                        log.info("AUTOBRAKE value: {}", value);
581                        break;
582                    case "Axis 3":
583                        // INDEPENDBRK is the state of the Independent (engine only) brake.
584                        // Like the Automatic brake: large values for no braking, small
585                        // values for more braking.
586                        log.info("INDEPENDBRK value: {}", value);
587                        break;
588                    case "Axis 4":
589                        // BAILOFF is the Independent brake 'bailoff', this is the spring
590                        // loaded right movement of the Independent brake lever.  Larger
591                        // values mean the lever has been shifted right.
592                        log.info("BAILOFF value: {}", value);
593                        break;
594                    case "Axis 5":
595                        // HEADLIGHT is the state of the headlight switch.  A value below 0.5
596                        // is off, a value near 0.5 is dim, and a number much larger than 0.5
597                        // is full. This is an analog input w/detents, not a switch!
598                        log.info("HEADLIGHT value: {}", value);
599                        break;
600                    case "Axis 6":
601                        // WIPER is the state of the wiper switch.  Much like the headlight
602                        // switch, this is also an analog input w/detents, not a switch!
603                        // Small values (much less than 0.5) are off, values near 0.5 are
604                        // slow, and larger values are full.
605                        log.info("WIPER value: {}", value);
606                        break;
607                    default:
608                        log.info("FUNCTION {} value: {}", oldValue, value);
609                        boolean isDown = (value > 0.5D);
610                        int fNum ;
611                        try {
612                            fNum = Integer.parseInt(oldValue);
613                        } catch (NumberFormatException ex) {
614                            //log.error("RailDriver parse property new value ('{}') exception: {}", newValue, ex);
615                            return;
616                        }
617                        String ledString = String.format("F%d", fNum + 1);
618                        switch (fNum) {
619                            case 28: {  // zoom/rocker button up
620                                if (isDown)  {
621                                    activeThrottleFrame.getRosterEntrySelector().setSelectedRosterEntry();                                    
622                                    DccLocoAddress a = activeThrottleFrame.getAddress();
623                                    ledString = "sel " + ((a != null) ? a.toString() : "null");
624                                }
625                                break;
626                            }
627                            case 29: {  // zoom/rocker button down
628                                if (isDown) {
629                                    activeThrottleFrame.dispatchAddress();
630                                    DccLocoAddress a = activeThrottleFrame.getAddress();
631                                    ledString = "dis " + ((a != null) ? a.toString() : "null");
632                                }
633                                break;
634                            }
635                            case 30: {  // four way panning up
636                                if (isDown)  {
637                                    int selectedIndex = activeThrottleFrame.getRosterEntrySelector().getRosterListSelectedIndex();
638                                    if (selectedIndex > 1) {
639                                        activeThrottleFrame.getRosterEntrySelector().setRosterListSelectedIndex(selectedIndex - 1);
640                                        ledString = String.format("Prev %d", selectedIndex - 1);
641                                    }
642                                }
643                                break;
644                            }
645                            case 31: {  // four way panning right
646                                if (isDown) {
647                                    if (throttleWindow != null) {
648                                        throttleWindow.nextThrottleFrame();
649                                    }
650                                    ledString = "NXT";
651                                }
652                                break;
653                            }
654                            case 32: {  // four way panning down
655                                if (isDown) {
656                                    RosterEntrySelectorPanel resp = activeThrottleFrame.getRosterEntrySelector();
657                                    if (resp != null) {
658                                        RosterEntryComboBox recb = resp.getRosterEntryComboBox();
659                                        if (recb != null) {
660                                            int cnt = recb.getItemCount();
661                                            int selectedIndex = resp.getRosterListSelectedIndex();
662                                            if (selectedIndex + 1 < cnt) {
663                                                try {
664                                                    activeThrottleFrame.getRosterEntrySelector().setRosterListSelectedIndex(selectedIndex + 1);
665                                                    ledString = String.format("Next %d", selectedIndex + 1);
666                                                } catch (ArrayIndexOutOfBoundsException ex) {
667                                                    // ignore this
668                                                }
669                                            }
670                                        }
671                                    }
672                                }
673                                break;
674                            }
675                            case 33: {  // four way panning left
676                                if (isDown) {
677                                    if (throttleWindow != null) {
678                                        throttleWindow.previousThrottleFrame();
679                                    }
680                                    ledString = "PRE";
681                                }
682                                break;
683                            }
684                            case 34: {  // Gear Shift Up
685                                if ((throttle != null) && isDown) {
686                                    // shuntFn
687                                    throttle.setFunction(3, false);
688                                }
689                                break;
690                            }
691                            case 35: {  // Gear Shift Down
692                                if ((throttle != null) && isDown) {
693                                    // shuntFn
694                                    throttle.setFunction(3, true);
695                                }
696                                break;
697                            }
698                            case 36:
699                            case 37: {  // Emergency Brake up/down
700                                if ((throttle != null) && isDown) {
701                                    throttle.setSpeedSetting(-1);
702                                }
703                                break;
704                            }
705
706                            case 38: {  // Alerter
707                                if (isDown) {
708                                    fNum = 6;   // alertFn
709                                }
710                                break;
711                            }
712                            case 39: {  // Sander
713                                if (isDown) {
714                                    fNum = 7;   // sandFn
715                                }
716                                break;
717                            }
718                            case 40: {  // Pantograph
719                                if (isDown) {
720                                    fNum = 8;   // pantoFn
721                                }
722                                break;
723                            }
724                            case 41: {  // Bell
725                                if (isDown) {
726                                    fNum = 1;   // bellFn
727                                }
728                                break;
729                            }
730                            case 42:
731                            case 43: {  // Horn/Whistle
732                                fNum = 2;   // hornFn
733                                break;
734                            }
735                            default: {
736                                break;
737                            }
738                        }
739                        if (throttle != null && fNum > 0 && fNum < throttle.getFunctions().length)  {
740                            if (! throttle.getFunctionMomentary(fNum)) {
741                                if (isDown) {
742                                    throttle.setFunction(fNum, !throttle.getFunction(fNum) );
743                                }
744                            } else {
745                                throttle.setFunction(fNum, isDown);
746                            }
747                        }
748                        if (isDown) {
749                            if (ledString.length() <= 3) {
750                                setLEDs(ledString);
751                            } else {
752                                sendStringAsync(ledString, 0.333);
753                            }
754                        }
755                        break; // if (oldValue.equals(...) {} else...
756                }
757                break;
758            default:
759                break;
760        }
761    }   // propertyChange
762
763    //initialize logging
764    private transient static final Logger log = LoggerFactory.getLogger(RailDriverMenuItem.class);
765
766}