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}