Files
JIMRI/jython/xboxThrottle.py
2026-06-17 14:00:51 +02:00

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)