JMRI code to connect to an MQTT broker, to publish and subscribe to the messages for signal masts.
Of course, we agree to a standard for the MQTT topics, and since you were not here, we settled on "mast.xxxx" where xxxx is a number starting at zero. This MQTT topic is used as the User Name in a JMRI signal mast. Created as a Virtual Mast with the Aspect used as the payload in the message. And, yes, the device under the real mast needs to be programmed with the same name and code for each of the "aspect" payloads to be implemented. A yellow and red LED'd dwarf signal can not do a "Clear" aspect, so make sure "Clear" is maybe set to yellow for "Approach" as well.
Virtual Masts with agreed upon User Names. Comments are for the One Wire Signals on the NeoPixel string (see prior post)
Virtual Mast
The Jython (or Python) code shown here below, is to attach a Listener to each Signal Mast, and to publish a message to the MQTT Broker when the Aspect changes...read that again, "changes". When the Mast comes up in JMRI as "Stop" when you load your Panel file, and the mast is already shown to "Stop", simply running the script will not set the Aspect again, even if you throw a switch to make the Mast show "Stop" again...since there was no change from "Stop" or red. But good news, I added subscriptions to the Broker as well, so now the script will subscribe to the Broker and see the Aspect was set to "Approach" last time, and your mast will update to "Approach", so that when you run the "default" routes, and the mast goes "Stop", the Broker will get the message, and then the real mast will update too. Keep in mind that it might not be a good idea to control signals from JMRI and also listen to other devices controlling them...we will think this one through in the coming months!
# MQTTSignalMastChanger.py to connect to an MQTT broker and publish signal
# Mast events, while also listening to Mast events sent from another MQTT
# publisher like your smart phone test application.
#
# Howto: Load your panel file containing Masts, usernames in
# the form mast.xxxx, eg. mast.0001, which is also the MQTT topic
# Then run this script, which will attach the listeners to the masts in JMRI
# A button labeled “-mast” will show up, top-left, that is to remove
# the listeners and stop the MQTT connection, so you can add or modify masts
# and run the script again.
#
#
# Authors: Gert 'Speed' Muller
# 2017.11.20,
# paho_mqtt-1.3.1, see note at "on_connect"
#
# 2017.10.16,
# Jython 2.7, JMRI 4.8, paho_mqtt-1.2.2-py2.7
#
import jmri
import java
from javax.swing import JButton, JFrame
import paho.mqtt as mq # used for version check
import paho.mqtt.client as mqtt
import thread
import time
VERSION = "version 0.3"
BROKER_ADDRESS = "192.168.1.12" # put your Broker IP address in here.
BROKER_PORT = 1883
# the 60 needs to be longer, else we time out and miss future messages
BROKER_TIMEOUT = 120
# only going to listen for x seconds now:
RUN_THREAD_FOR_MINUTES = 120
print( "Starting",VERSION,"up with:", BROKER_ADDRESS, BROKER_PORT,
BROKER_TIMEOUT, RUN_THREAD_FOR_MINUTES )
# this line is needed to avoid the default "MQTT" protocol ( default does not work )
client = mqtt.Client( "JMRI", True, None, protocol=mqtt.MQTTv31 )
# we only publish if connected
clientConnected = False
# we would like to stop the thread too when we click the -mast button
running = True
# we keep a copy of the last message received, since we don't want to set the
# mast to that aspect and then trigger another message leaving again. If the
# last incoming message matched the next message to leave, we don't send it.
lastIn = [ "", "" ]
def printPahoVersion( ):
print( "We are running Paho.MQTT version:", mq.__version__ )
# Message when connection is made
# def on_connect( client, userdata, rc ): # 1.2.2 vesrion
def on_connect( client, userdata, flags, rc ): # 1.3.1 version
global uList, running
print( "Flags", flags )
if not running:
return
print( "Connected with result code " + str( rc ) )
for uName in uList:
client.subscribe( uName )
# The callback for when a PUBLISH message is received from the Broker.
def on_message( client, userdata, msg ):
global uList, running, lastIn
if not running:
return
payload = str( msg.payload )
print( msg.topic + " " + payload )
if msg.topic in uList:
if 1:
#( ( 'Stop' in payload ) or
#( 'Approach' in payload ) or
#( 'Clear' in payload ) or
#( 'Unlit' in payload ) ): # test if we don't trust the topic!
mast = masts.getByUserName( msg.topic )
try:
lastIn = [ msg.topic, payload ]
if mast.getAspect() != payload:
print( lastIn )
mast.setAspect( payload )
else:
print( "all set(2)" ) # already on this Aspect
except:
print msg.topic, payload
#. sensors.provideSensor( msg.topic ).setState( ACTIVE )
#.else:
#. sensors.provideSensor( "IS1" ).setState( INACTIVE )
# This listener class gets attached to every mast and the propertyChange
# method is called when the mast changes state. Keep in mind that the first
# first state on startup is not a change, a red signal is not changing when you
# set it to red again. the magic word is "change". So, for booting up in the
# morning, we need to invent something more specific to force a 'Stop'.
class SignalMastChanger( java.beans.PropertyChangeListener ):
'SignalMastChanger 0.2'
mastTopic = "" # this stores the topic to publish to
# instead of using a constructor, we just feed the topic right after we
# create the object, listener = SignalMastChanger( topic ) # should look
# better
def setMastTopic( self, topic ):
self.mastTopic = topic
print( "mast topic set to", self.mastTopic )
# when a property changes, this is called
def propertyChange( self, event ):
global client, clientConnected, lastIn
# only publish if connected, but the future might need a "TrafficManager"
# if things start to publish at the same time...
if clientConnected:
print( "___", lastIn, self.mastTopic, event.newValue)
if ( lastIn[0] == self.mastTopic ) and ( lastIn[1] == event.newValue ):
print( "all set(1)" ) # recently received this Aspect, don't publish
else:
client.publish( self.mastTopic, event.newValue, retain=True )
else:
print "not connected..."
return
# Just some debug printing in the JMRI Output box
print( self.mastTopic, event.propertyName, event.newValue )
if ( event.newValue == "Unlit" ):
print "hello darkness my old friend..."
elif ( event.newValue == "Clear" ):
print "throttle up"
elif ( event.newValue == "Approach" ):
print "slow down"
elif ( event.newValue == "Stop" ):
print "breaks on"
else:
print "too advanced!"
### end of Class SignalMastChanger ###
# Remove all the SignalMastChanger listeners from the mast
# This is needed if a mast is added in the table, or a Username has
# to be changed. Remove the SignalMastChangers first and then add
# them all again by running the script again.
def signal_remove( event ):
global running, uList
running = False # stop thread(s)
print 'SignalMastChangers leaving...'
list = masts.getSystemNameList()
uList = []
for sName in list:
mast = masts.getBySystemName( sName )
#..print sName,":",mast.getNumPropertyChangeListeners()
llist = mast.getPropertyChangeListeners()
for changer in llist:
try:
if ( isinstance( changer, SignalMastChanger) ):
#..print "removing...", changer
mast.removePropertyChangeListener( changer )
else:
if ( changer.__doc__[0:17] == "SignalMastChanger" ):
#..print "removing older...", changer
mast.removePropertyChangeListener( changer )
else:
pass #..print "not one", changer
except:
pass
#..print "not a SignalChanger, since", sys.exc_info( )[ 0 ]
frameSC.visible = False
print "SignalMastChangers removed!"
printPahoVersion( )
# There is a button on the left side of the screen, click it to remove
# the listeners before you run the script again, else you will send
# more events every time
#
# GUI
frameSC = JFrame( 'SignalMastChanger',
defaultCloseOperation = JFrame.DO_NOTHING_ON_CLOSE,
size = ( 100, 45 )
) # width, height
frameSC.setResizable( False )
frameSC.setLocation( 50, 50 )
frameSC.setUndecorated( True )
# Go through he list of Masts and add listeners to each,
# as each listener is created, set the topic, so we don't have to look the
# mast up from the propertyChangeEvent
list = masts.getSystemNameList( )
uList = []
for sName in list:
listener = SignalMastChanger( )
mast = masts.getBySystemName( sName )
uName = mast.getUserName( )
uList.append( uName )
listener.setMastTopic( uName )
mast.addPropertyChangeListener( listener,
"SignalMastChanger", "SignalMastChangerRef" )
print uName, sName, ":", mast.getNumPropertyChangeListeners()
# Create and show the button to remove listeners
button = JButton( '-masts', actionPerformed=signal_remove )
frameSC.add( button )
frameSC.visible = True
# Everything down from here is to create a separate thread for the MQTT
# subscription to run in...else, you freeze JMRI up and need to kill it
# to gain control again.
#
# Define a function for the thread, it also connects to the broker and will
# time out here after RUN_THREAD_FOR_SECONDS, but since the client is
# global, we can still publish with the listener class. Might not need this
# later.
def mqtt_time( threadName ):
global client, clientConnected, running
client.on_connect = on_connect
client.on_message = on_message
client.connect( BROKER_ADDRESS, BROKER_PORT, BROKER_TIMEOUT )
clientConnected = True
client.loop_start()
count = 0
# show thread is alive every minute:
while ( count < RUN_THREAD_FOR_MINUTES ) and ( running ):
time.sleep( 60 )
count += 1
print "%s %6d: %s" % ( threadName, count, time.ctime( time.time() ) )
client.loop_stop()
print( "MQTT Thread stopped" )
# Create threads
try:
thread.start_new_thread( mqtt_time, ("MQTT Thread", ) )
except:
print "Error: unable to start thread"
time.sleep( 1 )
print( "Just an afterthought..." )
# notes:
#
# need to add subscriptions for sensors and turnouts
# determine is subscribing to masts is a good thing
# need to add checking if Broker is available
# need to add something when timeout is reached
# need to fix the mess when the script is re-run, the last thread does
# not dissappear.
# test subscription with: 20170424_MQTT.xml
# mosquitto_sub -h 192.168.16.152 -t "mast.0002"
# and
# mosquitto_pub -h 192.168.16.152 -t "mast.0002" -r -m "Approach"
# or watch more than one topic and show verbose:
# mosquitto_sub -h 192.168.16.152 -v -t "mast.0001" -t "mast.0002"
-t "mast.0003" -t "mast.0004" -t "mast.0005"
So to make this work, you will need to install the Jython paho library and convince JMRI to use it.
On my Ubuntu computer, it lives under /usr/local/lib/python2.7/dist-packages/paho My MQTT Broker runs on a Raspberry Pi Zero W ($10+4GB SD Card) with the default raspbian (Linux 4.9.24+) and "sudo apt-get install mosquitto". Of course it also has the WiFi AP for devices to connect too and the DHCP server running.
After you loaded your Panel file, run the script with Panels->Run Script from the main menu. Right now, to add masts or change User Names, you need to click on the button that appears at the top left hand side of the screen and then restart JMRI before running the script again...we are chasing the trouble down with the thread not ending properly.
2017.11.20:
Added a note at def on_connect() to use the extra "flags" parameter when using Paho.MQTT version 1.3.1. Version 1.2.2 did not have flags, as far as we can tell, :)
On my Ubuntu computer, it lives under /usr/local/lib/python2.7/dist-packages/paho My MQTT Broker runs on a Raspberry Pi Zero W ($10+4GB SD Card) with the default raspbian (Linux 4.9.24+) and "sudo apt-get install mosquitto". Of course it also has the WiFi AP for devices to connect too and the DHCP server running.
After you loaded your Panel file, run the script with Panels->Run Script from the main menu. Right now, to add masts or change User Names, you need to click on the button that appears at the top left hand side of the screen and then restart JMRI before running the script again...we are chasing the trouble down with the thread not ending properly.
2017.11.20:
Added a note at def on_connect() to use the extra "flags" parameter when using Paho.MQTT version 1.3.1. Version 1.2.2 did not have flags, as far as we can tell, :)