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}