358 lines
16 KiB
Python
358 lines
16 KiB
Python
# Author: Lionel Jeanson copyright 2017
|
|
# Part of the JMRI distribution
|
|
#
|
|
# Use a Nintendo Wiimote device as a throttle
|
|
# You need to have Bluecove and WiiRemoteJ jars in your Java classpath, JMRI lib folder is a good place for that (copy both jars there)
|
|
# See: http://bluecove.org/ and https://github.com/micromu/WiiRemoteJ
|
|
#
|
|
# once Jynstrument started press 1+2 on the Wiimote you want to use, it should connect
|
|
# connection will be validated by Wiimote vibrating and one of the LED turning on
|
|
#
|
|
# See JMRI output or log in case of issue.
|
|
#
|
|
# Customize at will.
|
|
# Unfortunately, this is only a classic remote, nothing with movements
|
|
#
|
|
# Default control:
|
|
# left / right : browse through throttles in instrumented window
|
|
# home : lights (function 0 or advanced function 0)
|
|
# +/- : direction
|
|
# A : brake
|
|
# B : accelerate
|
|
# +&- : EStop
|
|
# 1 : function 1 (or advanced function 1)
|
|
# 2 : function 2 (or advanced function 2)
|
|
#
|
|
|
|
speedEStopSpeed = -1
|
|
valueSpeedTimerRepeat = 25 # repeat time in ms for speed set task
|
|
valueSpeedIncrement = 0.01
|
|
|
|
depErr="""Required dependency must be installed!
|
|
|
|
You need to install WiiRemoteJ library :
|
|
https://github.com/micromu/WiiRemoteJ/raw/master/WiiRemoteJ.jar
|
|
|
|
Copy the jar file your JMRI lib folder.
|
|
"""
|
|
|
|
import java
|
|
import java.awt
|
|
import java.awt.event
|
|
import java.beans
|
|
import java.util
|
|
import java.beans.PropertyChangeListener as PropertyChangeListener
|
|
import java.awt.event.ActionListener as ActionListener
|
|
import java.util.Calendar as Calendar
|
|
import java.lang.Runnable as Runnable
|
|
import javax.swing.Timer as Timer
|
|
import javax.swing.JButton as JButton
|
|
import javax.swing.ImageIcon as ImageIcon
|
|
import javax.swing.SwingUtilities as SwingUtilities
|
|
import thread
|
|
import jmri.jmrit.throttle.interfaces.AddressListener as AddressListener
|
|
import jmri.jmrit.jython.Jynstrument as Jynstrument
|
|
import javax.swing.JOptionPane as JOptionPane
|
|
import javax.swing.JTextArea as JTextArea
|
|
import javax.swing.JFrame as JFrame
|
|
try:
|
|
import wiiremotej.event.WiiRemoteListener as WiiRemoteListener
|
|
import wiiremotej.event.WiiDeviceDiscoveryListener as WiiDeviceDiscoveryListener
|
|
import wiiremotej.WiiRemoteJ as WiiRemoteJ
|
|
import wiiremotej.event.WRButtonEvent as WRButtonEvent
|
|
except:
|
|
JOptionPane.showMessageDialog(JFrame(), JTextArea(depErr), "Missing dependency", JOptionPane.ERROR_MESSAGE);
|
|
|
|
|
|
class WiimoteThrottle2(Jynstrument, PropertyChangeListener, AddressListener, WiiDeviceDiscoveryListener, WiiRemoteListener, Runnable):
|
|
#Jynstrument main and mandatory methods
|
|
def getExpectedContextClassName(self):
|
|
return "jmri.jmrit.throttle.ThrottleWindow"
|
|
|
|
def init(self):
|
|
self.getContext().addPropertyChangeListener(self) #ThrottleFrame change
|
|
if ( self.getContext().getCurentThrottleController() != None ):
|
|
self.addressPanel= self.getContext().getCurentThrottleController().getAddressPanel();
|
|
self.addressPanel.addAddressListener(self) # change of throttle in Current frame
|
|
self.throttle = self.getContext().getCurentThrottleController().getAddressPanel().getThrottle() # the throttle
|
|
else:
|
|
self.addressPanel = None
|
|
self.throttle = None
|
|
self.speedAction = SpeedAction() #Speed increase thread
|
|
self.speedAction.setThrottle( self.throttle )
|
|
self.speedTimer = Timer(valueSpeedTimerRepeat, self.speedAction ) # Very important to use swing Timer object (see Swing and multithreading doc)
|
|
self.speedTimer.setRepeats(True)
|
|
self.label = JButton(ImageIcon(self.getFolder() + "/WiimoteThrottle2.png","WiiMote")) #label
|
|
self.label.addMouseListener(self.getMouseListeners()[0]) # In order to get the popupmenu on the button too
|
|
self.add(self.label)
|
|
self.lastTimeButton1 = Calendar.getInstance().getTimeInMillis()
|
|
self.lastTimeButton2 = Calendar.getInstance().getTimeInMillis()
|
|
self.advFunctions = AdvFunctions()
|
|
self.lastTimeEStop = Calendar.getInstance().getTimeInMillis()
|
|
self.wiiDevice = None
|
|
self.sync = thread.allocate_lock() # A lock protecting bellow self.evt
|
|
self.evt = None
|
|
java.lang.System.setProperty("bluecove.jsr82.psm_minimum_off", "true"); # Required for Bluecove + WiiRemoteJ
|
|
WiiRemoteJ.findRemotes(self, 1) # Search for 1 Wiimote, and call back
|
|
|
|
def quit(self):
|
|
self.speedTimer.stop()
|
|
WiiRemoteJ.stopFind()
|
|
if ((self.wiiDevice != None) and (self.wiiDevice.isConnected())):
|
|
self.wiiDevice.removeWiiRemoteListener(self)
|
|
self.wiiDevice.disconnect()
|
|
self.wiiDevice = None
|
|
self.speedAction = None
|
|
self.speedTimer = None
|
|
self.throttle = None
|
|
self.advFunctions = None
|
|
if (self.getContext() != None):
|
|
self.getContext().removePropertyChangeListener(self)
|
|
if (self.addressPanel != None):
|
|
self.addressPanel.removeAddressListener(self)
|
|
self.addressPanel = None
|
|
#Wiimote discoverer events
|
|
|
|
def findFinished(self, nb):
|
|
print "Search finished, found ",nb ," wiimotes"
|
|
|
|
def wiiDeviceDiscovered(self, evt):
|
|
print "Found a Wiimote, number: ", evt.getNumber()
|
|
self.wiiDevice = evt.getWiiDevice()
|
|
ledLights = [False, False, False, False]
|
|
ledLights[evt.getNumber()%4] = True
|
|
self.wiiDevice.setLEDLights(ledLights)
|
|
self.wiiDevice.addWiiRemoteListener(self)
|
|
|
|
#Wiimote events
|
|
def buttonInputReceived(self, evt):
|
|
# print("Wiimote Button event: ", evt)
|
|
self.sync.acquire()
|
|
self.evt = evt
|
|
self.sync.release()
|
|
SwingUtilities.invokeLater(self) # Delegate processing to Swing thread (when we are here, we're in the WiiRemoteJ driver thread)
|
|
|
|
def run(self):
|
|
self.sync.acquire()
|
|
evt = self.evt
|
|
self.sync.release()
|
|
if (self.speedTimer != None):
|
|
self.speedTimer.stop() # In any case
|
|
# ThrottleFrames
|
|
if ( evt.wasReleased(WRButtonEvent.RIGHT) ): # NEXT
|
|
self.getContext().nextThrottleFrame()
|
|
if ( evt.wasReleased(WRButtonEvent.LEFT) ): # PREVIOUS
|
|
self.getContext().previousThrottleFrame()
|
|
if ( evt.wasReleased(WRButtonEvent.UP) ): # NEXT RUNNING
|
|
self.getContext().nextRunningThrottleFrame()
|
|
if ( evt.wasReleased(WRButtonEvent.DOWN) ): # PREVIOUS RUNNING
|
|
self.getContext().previousRunningThrottleFrame()
|
|
# No throttle assigned to current frame, browse through roster
|
|
if (self.throttle == None):
|
|
if (evt.wasReleased(WRButtonEvent.HOME) ): # Assign selected roster entry
|
|
self.addressPanel.selectRosterEntry()
|
|
return
|
|
if ( evt.wasReleased(WRButtonEvent.PLUS) ): # Next roster entry
|
|
selectedIndex = self.addressPanel.getRosterSelectedIndex()
|
|
self.addressPanel.setIcon(False)
|
|
self.addressPanel.setVisible(True)
|
|
self.addressPanel.setRosterSelectedIndex(selectedIndex + 1)
|
|
return
|
|
if ( evt.wasReleased(WRButtonEvent.MINUS) ): # Previous roster entry
|
|
selectedIndex = self.addressPanel.getRosterSelectedIndex()
|
|
self.addressPanel.setIcon(False)
|
|
self.addressPanel.setVisible(True)
|
|
self.addressPanel.setRosterSelectedIndex(selectedIndex - 1)
|
|
return
|
|
# Throttle assigned to current frame, control it
|
|
if (self.throttle != None):
|
|
# Speed control
|
|
if ( evt.isPressed(WRButtonEvent.B) ): # SPEED - increment
|
|
self.speedAction.setSpeedIncrement( valueSpeedIncrement )
|
|
self.speedTimer.start()
|
|
if ( evt.isPressed(WRButtonEvent.A) ): # SPEED - decrement
|
|
self.speedAction.setSpeedIncrement( -valueSpeedIncrement )
|
|
self.speedTimer.start()
|
|
# EStop
|
|
if ( evt.isPressed( WRButtonEvent.PLUS | WRButtonEvent.MINUS ) ): # estop = + & -
|
|
self.throttle.setSpeedSetting( speedEStopSpeed )
|
|
self.lastTimeEStop = Calendar.getInstance().getTimeInMillis() # To cancel next inputs
|
|
self.wiiDevice.vibrateFor(750)
|
|
# Directions
|
|
if ( evt.wasReleased(WRButtonEvent.PLUS) ): # FORWARD
|
|
self.throttle.setIsForward(True)
|
|
if ( evt.wasReleased(WRButtonEvent.MINUS) ): # BACKWARD
|
|
self.throttle.setIsForward(False)
|
|
# Home : F0
|
|
if ( evt.wasReleased(WRButtonEvent.HOME) ): # LIGHTS
|
|
if not ((self.addressPanel.getRosterEntry() != None) and (self.advFunctions.call(self.addressPanel.getRosterEntry(), "0", False, self.throttle) != None)):
|
|
self.throttle.setFunction(0, not self.throttle.getFunction(0) )
|
|
# Wiimote 1 & 2 buttons
|
|
if (evt.isPressed(WRButtonEvent.ONE)):
|
|
if not ((self.addressPanel.getRosterEntry() != None) and (self.advFunctions.call(self.addressPanel.getRosterEntry(), "1", True, self.throttle) != None)):
|
|
pass # default F1 not momentary (switch only on Release, do nothing here)
|
|
if (evt.wasReleased(WRButtonEvent.ONE)):
|
|
if not ((self.addressPanel.getRosterEntry() != None) and (self.advFunctions.call(self.addressPanel.getRosterEntry(), "1", False, self.throttle) != None)):
|
|
self.throttle.setFunction(1, not self.throttle.getFunction(1) ) # default F1 not momentary
|
|
if (evt.isPressed(WRButtonEvent.TWO)):
|
|
if not ((self.addressPanel.getRosterEntry() != None) and (self.advFunctions.call(self.addressPanel.getRosterEntry(), "2", True, self.throttle) != None)):
|
|
self.throttle.setFunction(2, True ) # default F2 momentary
|
|
if (evt.wasReleased(WRButtonEvent.TWO)):
|
|
if not ((self.addressPanel.getRosterEntry() != None) and (self.advFunctions.call(self.addressPanel.getRosterEntry(), "2", False, self.throttle) != None)):
|
|
self.throttle.setFunction(2, False )
|
|
|
|
def disconnected(self):
|
|
self.wiiDevice = None
|
|
print("Lost wiimote")
|
|
|
|
def accelerationInputReceived(self, evt):
|
|
pass
|
|
def combinedInputReceived(self, evt):
|
|
pass
|
|
def extensionConnected(self, extension):
|
|
pass
|
|
def extensionDisconnected(self, extension):
|
|
pass
|
|
def extensionInputReceived(self, evt):
|
|
pass
|
|
def extensionPartiallyInserted(self):
|
|
pass
|
|
def extensionUnknown(self):
|
|
pass
|
|
def IRInputReceived(self, evt):
|
|
pass
|
|
def statusReported(self, evt):
|
|
print("Wiimote status reported: ", evt)
|
|
|
|
#Property listener part
|
|
def propertyChange(self, event):
|
|
self.speedTimer.stop()
|
|
if (event.propertyName.startswith("ThrottleFrame")) : # Current throttle frame changed
|
|
if event.oldValue != None :
|
|
event.oldValue.getAddressPanel().removeAddressListener(self)
|
|
if event.newValue != None :
|
|
self.addressPanel = event.newValue.getAddressPanel()
|
|
self.throttle = self.addressPanel.getThrottle()
|
|
self.speedAction.setThrottle( self.throttle )
|
|
self.addressPanel.addAddressListener(self)
|
|
|
|
#AddressListener part: to listen for address changes in address panel (release, acquired)
|
|
def notifyAddressChosen(self, address):
|
|
pass
|
|
|
|
def notifyAddressThrottleFound(self, throttle):
|
|
self.speedTimer.stop()
|
|
self.throttle = throttle
|
|
self.speedAction.setThrottle( self.throttle )
|
|
|
|
def notifyAddressReleased(self, address):
|
|
self.speedTimer.stop()
|
|
self.throttle = None
|
|
self.speedAction.setThrottle( self.throttle )
|
|
|
|
def notifyConsistAddressChosen(self, address):
|
|
self.notifyAddressChosen(address)
|
|
|
|
def notifyConsistAddressThrottleFound(self, throttle):
|
|
self.notifyAddressThrottleFound(throttle)
|
|
|
|
def notifyConsistAddressReleased(self, address):
|
|
self.notifyAddressReleased(address)
|
|
|
|
# Speed timer class, to increase speed regularly once button pushed, thread stopped on button release
|
|
class SpeedAction(ActionListener):
|
|
def __init__(self):
|
|
self.sync = thread.allocate_lock() # Protects properties getter and setter
|
|
self.speedIncrement = 0
|
|
self.throttle = None
|
|
|
|
def setSpeedIncrement(self, si):
|
|
self.sync.acquire()
|
|
self.speedIncrement = si
|
|
self.sync.release()
|
|
|
|
def getSpeedIncrement(self):
|
|
self.sync.acquire()
|
|
si = self.speedIncrement
|
|
self.sync.release()
|
|
return si
|
|
|
|
def setThrottle(self, throt):
|
|
self.sync.acquire()
|
|
self.throttle = throt
|
|
self.sync.release()
|
|
|
|
def getThrottle(self):
|
|
self.sync.acquire()
|
|
throt = self.throttle
|
|
self.sync.release()
|
|
return throt
|
|
|
|
def actionPerformed(self, e):
|
|
throttle = self.getThrottle()
|
|
spi = self.getSpeedIncrement()
|
|
if (throttle != None) :
|
|
ns = throttle.getSpeedSetting() + spi
|
|
if (ns < 0 ) :
|
|
ns = 0
|
|
if (ns > 1 ) :
|
|
ns = 1
|
|
throttle.setSpeedSetting( ns )
|
|
|
|
class AdvFunctions():
|
|
def call(self, rosterEntry, advFn, status, throttle):
|
|
assert (rosterEntry!=None), "rosterEntry is null"
|
|
assert (advFn!=None), "advFn is null"
|
|
assert (status!=None), "status is null"
|
|
assert (throttle!=None), "throttle is null"
|
|
todoStr = rosterEntry.getAttribute("advF"+advFn)
|
|
if (todoStr == None):
|
|
return None
|
|
# poor man parser, should unserialize a json object instead
|
|
todo = todoStr.split(";")
|
|
for task in todo:
|
|
task = task.lstrip()
|
|
# Actual function call
|
|
if (task.startswith("F")):
|
|
task = task.rstrip()
|
|
setter = None
|
|
getter = None
|
|
ok = False
|
|
for fct in throttle.getClass().getMethods():
|
|
fctName = fct.getName()
|
|
if (fctName == "set"+task):
|
|
setter=fct
|
|
if (fctName == "get"+task):
|
|
getter=fct
|
|
if (setter != None and getter != None):
|
|
ok = True
|
|
break
|
|
if (ok):
|
|
if (not rosterEntry.getFunctionLockable(int(task[1:]))):
|
|
setter.invoke(throttle, status)
|
|
else:
|
|
state = getter.invoke(throttle)
|
|
setter.invoke(throttle, not state)
|
|
continue
|
|
# Play sound
|
|
if (task.startswith("P") and status):
|
|
path = task[1:]
|
|
self.play(path, throttle)
|
|
continue
|
|
return True
|
|
|
|
def play(self, sndPath, throttle):
|
|
assert (sndPath!=None), "sndPath is null"
|
|
sourceName="IAS"+sndPath+"-"+str(throttle.getLocoAddress())
|
|
bufferName="IAB"+sndPath
|
|
source = audio.getAudio(sourceName)
|
|
if (source == None):
|
|
buffer = audio.getAudio(bufferName)
|
|
if (buffer == None):
|
|
buffer = audio.provideAudio(bufferName)
|
|
buffer.setURL(sndPath)
|
|
source = audio.provideAudio(sourceName)
|
|
source.setAssignedBuffer(bufferName)
|
|
# would need to update location here
|
|
source.play()
|