Skip to main content

Not RRRduino, unless you have an MQTT connection via Ethernet or WiFi

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, :)

Popular posts from this blog

Pi Pico, we smell competition in the land of the RRRduino! 

Pi Pico on an N-scale gondola Pi Pico, we smell competition in the land of the ‘duino!  Our very favorite low cost microcontroller system is seeing some fresh competition.  Everyone by now has heard about the Raspberry Pi, some fruity company in the United Kingdom, making single board computers!  They run Raspbian (or other flavors of Linux and are capable of some Windows versions) for as little money as $10 for the Pi Zero W.  The more popular Model 4, with 2 GB of RAM, retails for about $29.  Add a $5  micro SD card and you have a real computer with which you can surf the internet, write code and even program Arduinos.  It also runs our other favorite, JMRI.  Of course, plug it into a small or big screen television with an HDMI cable and you can even stream Netflix.  If you want a really cool computer built into a keyboard, also check out the brand new, Raspberry Pi 400 , you might just think you own a ZX Spectrum again. These are all “computers” with processors and now the Raspberry

Making things flash...yes LEDs!

So we have all seen the BLINK program, where we first configure the onboard LED to be an OUTPUT and then we turn the LED on, waste some time, turn the LED off and waste some time in the loop() function and then everything repeat again. So, that is really cool and 25 times faster using an Arduino, compared to 25 years ago where you had to erase the EPROM with a UV light first before you could upload the code that you tediously wrote in Assembler! And the 8051 did not have all the awesome built-in modules to simply do: Serial.begin( 300 ); // yes it was slow back then Serial.println( "Hello world" ); // and remember the extra work to do a String? // while \n and \r was needed too!!! Part I: So, back to the LED, the next question you ask is: "I have at least 17 more free pins , can they blink too?", and the answer is "Sure!". But in the current digitalWrite( 13, HIGH ); delay( 500 ); digitalWrite( 1