294 lines
12 KiB
Python
294 lines
12 KiB
Python
# Use an Xbox (original) controller as throttle(s)
|
|
#
|
|
# Author: Andrew Berridge, 2010 - Based on USBThrottle.py, Bob Jacobsen, copyright 2008
|
|
# Part of the JMRI distribution
|
|
#
|
|
# This is still experimental code and is under development. Currently, it supports:
|
|
# 1. Left-hand joystick as throttle - up moves forwards, down reverses
|
|
# 2. Some buttons assigned to functions (not complete)
|
|
# 3. Throttle lock: standard operation is immediate control of throttle. Releasing the joystick
|
|
# sets the throttle to zero (note: a dead zone is currently not implemented, but should be).
|
|
# To lock the throttle so it stays in its current position, pull the left trigger. This is useful
|
|
# for running a train round a loop (for example)
|
|
# 4. Some support for toggling functions though this may not be required (use standard throttle function
|
|
# locking?
|
|
# 5. Each controller will open its own throttle window
|
|
# 6. Selection of locos using the "hat" four-way switch. Start selects a loco, Back dispatches
|
|
# current loco. Using up and down on the hat switch will move between locos in the roster
|
|
# (as long as the last loco has been "dispatched"
|
|
# 7. Using left and right on the hat switch will select between throttle panels... Click the
|
|
# plus (+) button on the throttle window to add another panel
|
|
# 8. Emergency stop! Push the left stick down to trigger its button. This will
|
|
# e-stop the current throttle
|
|
#
|
|
# Future development ideas:
|
|
# 1. Support right-hand joystick to control a second throttle
|
|
# 2. Finish implementation of function buttons
|
|
#
|
|
# Notes:
|
|
# Hat switch is 0.0 for "released"
|
|
# 0.25 -> up
|
|
# 0.5 -> right
|
|
# 0.75 -> down
|
|
# 1.0 -> left
|
|
#
|
|
# Requires:
|
|
# XBCD Xbox Controller Driver (from http://www.redcl0ud.com/xbcd.html)
|
|
# Original Xbox (not 360) controller
|
|
#
|
|
# Currently tested on Windows only
|
|
#
|
|
# IMPORTANT Warnings:
|
|
# 1. Make sure you set a "Dead zone" for each of the analogue sticks (in the
|
|
# Windows Control planel)! If you don't there will be too many events triggered
|
|
# and everything will slow right down....
|
|
|
|
import jmri
|
|
import java
|
|
import java.beans
|
|
|
|
try:
|
|
#
|
|
# Set the name of the controller you're using
|
|
#
|
|
desiredControllerName = "XBCD XBox Gamepad"
|
|
|
|
#
|
|
# Use the following if you have an absolute, analog control
|
|
# (as "slider") you'd like to use to control speed
|
|
#
|
|
componentASlider = "Y" # absolute proportional device for throttle
|
|
sliderAMin = 0.0 # value of componentASlider for zero speed
|
|
sliderAMax = 0.99 # value of componentASlider for max speed
|
|
componentALock = "11" # the left trigger
|
|
|
|
hatSwitch = "Hat Switch" # The name of the Hat Switch
|
|
|
|
#
|
|
# Set the following to the buttons you want
|
|
# to use for function control
|
|
#
|
|
componentF0 = "6" # F0 follows this
|
|
componentF1 = "2" # 2 is Button B - F1 follows this
|
|
componentF2 = "1" # 1 is Button A - F2 follows this
|
|
componentF3 = "0" # F3 follows this
|
|
componentF4 = "5" # F4 follows this
|
|
componentF5 = "" # F5 follows this
|
|
componentF6 = "" # F6 follows this
|
|
componentF7 = "" # F7 follows this
|
|
componentF8 = "" # F8 follows this
|
|
|
|
componentStart = "7"
|
|
componentBack = "8"
|
|
|
|
componentLeftStick = "9" # The button on the left stick. Used as E-stop in this configuration
|
|
|
|
# from here down is the code for the throttle
|
|
# Generally, you shouldn't touch it unless you're debugging a problem
|
|
|
|
# uncomment the following two lines to run from a keyboard for debug
|
|
#desiredControllerName = "Apple Internal Keyboard / Trackpad"
|
|
#componentWheel = "a"
|
|
|
|
# connect to USB device
|
|
model = jmri.jmrix.jinput.TreeModel.instance()
|
|
|
|
#Keep track of the number of throttles created
|
|
numThrottles = 0
|
|
# and the number of controllers used
|
|
numControllers = 0
|
|
|
|
def isNaN(num):
|
|
return num != num
|
|
|
|
|
|
# add listener for USB events
|
|
class TreeListener(java.beans.PropertyChangeListener):
|
|
|
|
locked = False
|
|
F1isOn = False
|
|
F2isOn = False
|
|
F0isOn = False
|
|
#hash code is unique instance of controller
|
|
controllerHashCode = 0
|
|
throttleWindow = None
|
|
controlPanel = None
|
|
functionPanel = None
|
|
addressPanel = None
|
|
activeThrottleFrame = None
|
|
|
|
def __init__(self, controllerHashCode):
|
|
global numThrottles
|
|
global numControllers
|
|
self.controllerHashCode = controllerHashCode
|
|
# open a throttle window and get components
|
|
self.throttleWindow = jmri.jmrit.throttle.ThrottleFrameManager.instance().createThrottleWindow()
|
|
self.activeThrottleFrame = self.throttleWindow.addThrottleFrame()
|
|
# move throttle on screen so multiple throttles don't overlay each other
|
|
self.throttleWindow.setLocation(400 * numThrottles, 50 * numThrottles)
|
|
numThrottles += 1
|
|
numControllers += 1
|
|
self.activeThrottleFrame.toFront()
|
|
self.controlPanel = self.activeThrottleFrame.getControlPanel()
|
|
self.functionPanel = self.activeThrottleFrame.getFunctionPanel()
|
|
self.addressPanel = self.activeThrottleFrame.getAddressPanel()
|
|
self.throttleWindow.addPropertyChangeListener(self)
|
|
self.activeThrottleFrame.addPropertyChangeListener(self)
|
|
|
|
def propertyChange(self, event):
|
|
if (event.propertyName == "ancestor"):
|
|
#print "ancestor property change - closing throttle window"
|
|
# Remove all property change listeners and
|
|
# dereference all throttle components
|
|
self.activeThrottleFrame.removePropertyChangeListener(self)
|
|
self.throttleWindow.removePropertyChangeListener(self)
|
|
self.activeThrottleFrame = None
|
|
self.controlPanel = None
|
|
self.functionPanel = None
|
|
self.addressPanel = None
|
|
self.throttleWindow = None
|
|
# Now remove this propertyChangeListener from the model
|
|
global model
|
|
model.removePropertyChangeListener(self)
|
|
|
|
if (event.propertyName == "ThrottleFrame") : # Current throttle frame changed
|
|
#print "Throttle Frame changed"
|
|
self.addressPanel = event.newValue.getAddressPanel()
|
|
self.controlPanel = event.newValue.getControlPanel()
|
|
self.functionPanel = event.newValue.getFunctionPanel()
|
|
|
|
if (event.propertyName == "Value") :
|
|
# event.oldValue is the UsbNode
|
|
#
|
|
# uncomment the following line to see controller names
|
|
#print "|"+event.oldValue.getController().toString()+"|"
|
|
#print event.oldValue.getController().getName()
|
|
#print event.oldValue.getController().hashCode()
|
|
#
|
|
# Select just the device (controller) we want
|
|
if (event.oldValue.getController().toString() == desiredControllerName
|
|
and event.oldValue.getController().hashCode() == self.controllerHashCode) :
|
|
# event.newValue is the value, e.g. 1.0
|
|
# Check for desired component and act
|
|
component = event.oldValue.getComponent().toString()
|
|
value = event.newValue
|
|
#
|
|
# uncomment the following to see the entries
|
|
# print component, value
|
|
|
|
#print "addr: " + self.addressPanel.getCurrentAddress().toString()
|
|
|
|
#Hat switch switches between locos and throttles in a window
|
|
if (component == hatSwitch):
|
|
#self.addressPanel.showRosterSelectorPopup()
|
|
selectedIndex = self.addressPanel.getRosterSelectedIndex()
|
|
if (value == 0.75):
|
|
self.addressPanel.setRosterSelectedIndex(selectedIndex + 1)
|
|
if (value == 0.25):
|
|
self.addressPanel.setRosterSelectedIndex(selectedIndex - 1)
|
|
if (value == 0.5 or value == 1):
|
|
if (value == 0.5):
|
|
# advance to next throttle frame
|
|
self.throttleWindow.nextThrottleFrame()
|
|
else :
|
|
# go to previous throttle frame
|
|
self.throttleWindow.previousThrottleFrame()
|
|
return
|
|
|
|
if (component == componentStart and value > 0.5):
|
|
self.addressPanel.selectRosterEntry()
|
|
return
|
|
|
|
if (component == componentBack and value > 0.5):
|
|
self.addressPanel.dispatchAddress()
|
|
return
|
|
|
|
# "Lock" the throttle
|
|
if (component == componentALock and value > 0.0) :
|
|
if self.locked:
|
|
self.locked = False
|
|
slider = self.controlPanel.getSpeedSlider()
|
|
slider.setValue(0)
|
|
else:
|
|
self.locked = True
|
|
return
|
|
|
|
# absolute throttle component
|
|
if (component == componentASlider) :
|
|
if not self.locked:
|
|
isfwd = self.controlPanel.getIsForward()
|
|
if value > 0 :
|
|
# reverse
|
|
if isfwd :
|
|
self.controlPanel.setForwardDirection(False)
|
|
forward = False
|
|
|
|
else :
|
|
if not isfwd:
|
|
self.controlPanel.setForwardDirection(True)
|
|
forward = True
|
|
value = - value
|
|
|
|
# handle speed setting input
|
|
# limit range
|
|
if (value < sliderAMin) :
|
|
value = sliderAMin
|
|
if (value > sliderAMax) :
|
|
value = sliderAMax
|
|
# convert fraction of input to speed step
|
|
fraction = (value-sliderAMin)/(sliderAMax-sliderAMin)
|
|
slider = self.controlPanel.getSpeedSlider()
|
|
setting = int(round(fraction*(slider.getMaximum()-slider.getMinimum()), 0))
|
|
slider.setValue(setting)
|
|
return
|
|
if component == componentLeftStick: #emergency stop!
|
|
self.controlPanel.stop()
|
|
print "Emergency Stop!"
|
|
#print component, value
|
|
fNum = -1
|
|
if component == componentF0:
|
|
fNum = 0
|
|
elif component == componentF1:
|
|
fNum = 1
|
|
elif component == componentF2:
|
|
fNum = 2
|
|
elif component == componentF3:
|
|
fNum = 3
|
|
elif component == componentF4:
|
|
fNum = 4
|
|
elif component == componentF5:
|
|
fNum = 5
|
|
elif component == componentF6:
|
|
fNum = 6
|
|
elif component == componentF7:
|
|
fNum = 7
|
|
elif component == componentF8:
|
|
fNum = 8
|
|
|
|
button = self.functionPanel.getFunctionButtons()[fNum]
|
|
if button.getIsLockable() :
|
|
if value > 0.5 :
|
|
state = button.getState()
|
|
button.setSelected(not state)
|
|
else :
|
|
button.setSelected(value > 0.5)
|
|
|
|
return
|
|
|
|
#Iterate over the controllers, creating a new listener for each
|
|
#controller of the type we are interested in
|
|
for c in model.controllers():
|
|
name = c.getName()
|
|
hashCode = c.hashCode()
|
|
if (name == desiredControllerName):
|
|
print "Found " + name + " " + str(hashCode)
|
|
model.addPropertyChangeListener(TreeListener(hashCode))
|
|
except:
|
|
import javax.swing.JOptionPane as JOptionPane
|
|
import javax.swing.JFrame as JFrame
|
|
JOptionPane.showMessageDialog(JFrame(),
|
|
"""This code is no longer maintained.
|
|
|
|
Please use Jynstrument jython/Jynstrument/ThrottleWindowToolBar/USBThrottle.jyn instead
|
|
Or jython/USBThrottleAsJynstrument.py script""", "Outdated code", JOptionPane.WARNING_MESSAGE)
|