# 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()