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

Comments

Popular posts from this blog

Signal Masts in JMRI 4.14, for beginners...

Here are all the notes and files from the Plano Train Show Clinic. You need to have an internal sensor ( IMCOMPORT ) with the port in the value like COM10 or /dev/ttyUSB0 , whatever Device Manager (or /dev/ttyUSBx) shows for the connected Arduino. If you have Panels -> Script Output open, you will see the message:  !!! Please add IMCOMPORT with the port in the value!!! The basic idea here is to use the Comment in the Signal Head field to define the position of the Pixel in the string commented to the Arduino, as well as the RGB colors to show for Clear, Approach and Stop (00:00 FF 00: BBAA 00: FF 0000). With all the Heads in the Table, run the python script and now you can update the colors in the Comment "live", keeping in mind that a message is only sent when a Head changes color. We then put the Heads in Masts and use the Signal Mast Logic to do the magic. I like the original Panel Editor more and it works fine as long as you keep using Mast pairs, which require

The One Pin Signal System, using an Arduino

The One (RRRduino) Pin Signal System! Part I NeoPixels: Yes, you read that right, you can control a whole signal system with up to 200 lights by using a single Arduino pin. And this was not my idea, all the credit and the patent goes to my friend Tom, but since I already had something using the same technology up and running using only a Nano, I do not feel too bad telling you about it. Short video first! There is a tiny 8 pin chip out there called a WS2811 which is a three channel LED driver, and you might find them on the interweb as NeoPixel LED Driver Chips. They need power and ground, typically 5 Vdc, and data to be put in the three registers inside the chip, one for each LEDs it can control. So, in most cases out there today, the LEDs connected are Red, Green and Blue (RGB for short) and by setting the three registers each with an 8 bit value (which in English means a value from 0 to 255, since 2 8 = 256, but since 0 is an acceptable value too, the maximum is 2 8 - 1