Files
JIMRI/jython/AssignIdTagToRollingStock.py
T
2026-06-17 14:00:51 +02:00

389 lines
19 KiB
Python

#Script to create a property change listener on the ID Tag Table
# A more advanced example of scripting, this script "listens" for new ID tags and pops up a window to allow
# the user to enter a user name (typically loco or car/wagon number) and comment. Additionally, it listens for
# new or changed user names on existing RFID tags. It does this by creating
# a property change listener on the ID Tag Table. It will also update OperationsPro Locomotive Table or Car
# Table if requested (check box on the popup window). It will flag loco/car numbers that do not exist, as well
# as move an ID Tag from one loco to another or from one car to another and eliminate dupicate assignment of
# ID Tag or cars or locomotives.
#If you are actively using RFID, recommended that this script be started as a JMRI Startup action, although it can be started at any time
#See https://www.jmri.org/help/en/html/hardware/rfid/GettingStartedWithRFID.shtml for more information.
# Script will display a window when:
# (1) new ID Tag seen to allow entry of user name (typically loco or car/wagon number) and comment
# (2) Id Tag User Name is changed
# Multiple windows may be open at the same time for user convenience
# Will update Operations Locomotive Table or Car Table if requested (check box on the popup window)
# Will flag loco/car numbers that do not exist, as well as move an ID Tag from one loco to another or from one car to another
# Will eliminate dupicate assignment of RFID Tag or cars or locomotives
#Companion scripts: ScanRFIDTagToLoco or ScanRFIDTagToWagon
# Illustrates user of JMRI property change listener, writing to script output window, adding messages to System Console, updating ID Tag Table, updating operations Locomotive and Car Tables, popping up a new window via Java swing with entry fields and radio buttons
# Author: Jerry Grochow (c) 2023 (with acknowledgement to Tom Seitz)
# Uses basics from SensorLog.py and JButtonComplexExample (Original Author: Bob Jacobsen, copyright 2004, 2006)
# Software made available under GNU General Public License version 2 (https://www.jmri.org/COPYING)
# Please report any bugs or suggestions via https://groups.io/g/jmriusers/
# Part of the JMRI distribution
import jmri
import java
import javax.swing
import java.beans
from org.slf4j import LoggerFactory
DEBUG = False
DEBUGX= False #Additional level of debug info put in Script Output Window, to avoid unnecessary log calls
AssignIdTagToRS_log = LoggerFactory.getLogger("jmri.jmrit.jython.exec.AssignIdTagToRS")
#Update version number and date in following statement:
AssignIdTagToRS_log.info("'AssignIdTagToRS' v42 10/13/2023 2305 loaded")
print "AssignIdTagToRS: will pop up a window when new RFID Tag detected \n or when new user name entered for existing RFID Tag.\n Optionally, will update Operations Locomotive and/or Car Tables.\n May be necessary to refresh ops tables to see effect."
#To move subsequent frames around screen
myFrameLocIncr = 0
myFrameLocOffset = 15 #Number of pixels to offset next window (FUTURE: allow multiple open windows
################################################################################################################
# Create a frame to hold the button, set up for nice layout
class AssignIdTagToRSFrame:
def __init__(self, newIdTag, existingIDTagUserName):
self.newIdTag = newIdTag
self.myFrame = None
# To keep track of text field entry to enable the button
self.myIdTagUserNameChanged = False
self.myIdTagCommentChanged = False
self.createFrame(existingIDTagUserName)
# To keep track of whether to update either of the ops tables
self.myIdTagUpdateLoco = False
self.myIdTagUpdateCar = False
#############################################################
def createFrame (self, existingIDTagUserName) :
global myFrameLocIncr, myFrameLocOffset
# Create input fields
self.createInputFields(existingIDTagUserName)
# Create process buttons
self.createProcessButtons()
# Create the buttons that control Ops Tables Updates
self.createOpsTablesSelectButtons()
# Create text display lines
self.createTextElements()
# Now put all the panels in a frame and display
self.myFrame = javax.swing.JFrame("RFID Tag: " + str(self.newIdTag)) # argument is the frames title
self.myFrame.contentPane.setLayout(javax.swing.BoxLayout(self.myFrame.contentPane, javax.swing.BoxLayout.Y_AXIS))
self.myFrame.contentPane.add(self.instrLine1)
self.myFrame.contentPane.add(self.instrLine2)
self.myFrame.contentPane.add(self.createEntryLine())
self.myFrame.contentPane.add(self.createCheckboxLine())
self.myFrame.contentPane.add(self.createButtonLine())
self.myFrame.contentPane.add(self.bottomLine)
self.myFrame.pack()
self.myFrame.setSize(700, 240)
myFrameLocIncr += myFrameLocOffset # Move any subsequent window
x = self.myFrame.getX()
y = self.myFrame.getY()
#self.myFrame.setLocation(java.awt.Point.translate(myFrameLocIncr,self.myFrameLocIncr))
self.myFrame.setLocation(x + myFrameLocIncr, y + myFrameLocIncr)
self.myFrame.setVisible(True)
return
def createInputFields(self,existingIDTagUserName):
# Input fields fields
self.myIdTagUserName = javax.swing.JTextField(12)
self.myIdTagUserName.setText(existingIDTagUserName)
self.myIdTagComment = javax.swing.JTextField(20)
# Set the text field actions
self.myIdTagUserName.actionPerformed = self.whenMyIdTagUserNameChanged # if user hit return or enter
self.myIdTagUserName.focusLost = self.whenMyIdTagUserNameChanged # if user tabs away
self.myIdTagComment.actionPerformed = self.whenMyIdTagCommentChanged # if user hit return or enter
self.myIdTagComment.focusLost = self.whenMyIdTagCommentChanged # if user tabs away
return
def createProcessButtons(self):
self.processButton = javax.swing.JButton("Process")
self.cancelButton = javax.swing.JButton("Cancel")
# create the button features
self.cancelButton.setEnabled(True) # button enabled
self.cancelButton.actionPerformed = self.whenMyCancelButtonClicked
self.processButton.setEnabled(False) # button disabled until user name entered
self.processButton.actionPerformed = self.whenMyProcessButtonClicked
return
def createOpsTablesSelectButtons(self):
# Create the check box group features
self.noOpsUpdateBox = javax.swing.JRadioButton("No Ops Table Update")
self.noOpsUpdateBox.setSelected(True) #pre-select the no Operations Table update box
self.noOpsUpdateBox.actionPerformed = self.radioBtnCheck
self.locoUpdateBox = javax.swing.JRadioButton("Locomotive Table")
self.locoUpdateBox.actionPerformed = self.radioBtnCheck
self.carUpdateBox = javax.swing.JRadioButton("Car Table")
self.carUpdateBox.actionPerformed = self.radioBtnCheck
#...and the checkbox group
opsUpdateBoxGroup = javax.swing.ButtonGroup()
opsUpdateBoxGroup.add(self.noOpsUpdateBox)
opsUpdateBoxGroup.add(self.locoUpdateBox)
opsUpdateBoxGroup.add(self.carUpdateBox)
return
def createTextElements(self):
#Instructions at top
self.instrLine1 = javax.swing.JPanel()
self.instrLine1.add(javax.swing.JLabel("Enter User Name (Loco or Car/Wagon #) for RFID Tag " + str(self.newIdTag)))
self.instrLine2 = javax.swing.JPanel()
self.instrLine2.add(javax.swing.JLabel("Check box to also update Operations Locomotive or Car Table"))
#Create bottom line
self.bottomLine = javax.swing.JPanel()
self.bottomLine.add(javax.swing.JLabel("Check Scripting Output window for messages"))
return
def createEntryLine(self):
#Tom: how does this know which to lay out horizontally and which vertically??
entryField1 = javax.swing.JPanel()
entryField1.add(javax.swing.JLabel("RFID Tag User Name (Loco or Car/Wagon #):"))
entryField1.add(self.myIdTagUserName)
entryField2 = javax.swing.JPanel()
entryField2.add(javax.swing.JLabel("Comment:"))
entryField2.add(self.myIdTagComment)
#Laid out vertically
entryLine = javax.swing.JPanel()
entryLine.add(entryField1)
entryLine.add(entryField2)
return entryLine
def createCheckboxLine(self):
#Laid out horizontally
checkboxLine = javax.swing.JPanel()
checkboxLine.add(self.noOpsUpdateBox)
checkboxLine.add(self.locoUpdateBox)
checkboxLine.add(self.carUpdateBox)
return checkboxLine
def createButtonLine(self):
#Laid out horizontally
buttonLine = javax.swing.JPanel()
buttonLine.add(self.cancelButton)
buttonLine.add(self.processButton)
return buttonLine
#############################################################
# Change handlers for frame
# When one of the radio buttons is selected
def radioBtnCheck(self,event):
# Process the events and update the label
#Tom's version:
self.myIdTagUpdateLoco = self.locoUpdateBox.isSelected()
self.myIdTagUpdateCar = self.carUpdateBox.isSelected()
# if self.noOpsUpdateBox.isSelected():
# self.myIdTagUpdateCar = False
# self.myIdTagUpdataLoco = False
# elif self.locoUpdateBox.isSelected():
# self.myIdTagUpdateLoco = True
# self.myIdTagUpdateCar = False
# elif self.carUpdateBox.isSelected():
# self.myIdTagUpdateCar = True
# self.myIdTagUpdataLoco = False
# else: # Shouldn't ever get here
# print "ART180: ERR in radio button selection"
if DEBUGX:
print "ART207:", self.myIdTagUpdateLoco, self.myIdTagUpdateCar
return
def whenMyIdTagUserNameChanged(self, event) :
if (DEBUGX):
print "ART186:", event
if (self.myIdTagUserName.text is not "") : # myIdTagUserName only changed if a value was entered
self.myIdTagUserNameChanged = True
self.processButton.setEnabled(True) # enable button if user name entered
return
# have the comment text field enable the process button only if user name already entered
def whenMyIdTagCommentChanged(self, event) :
if (DEBUGX):
print "ART195:", event
if (self.myIdTagComment.text is not "") :
self.myIdTagCommentChanged = True
if (self.myIdTagCommentChanged and self.myIdTagUserNameChanged) : # make sure user name entered
self.processButton.setEnabled(True)
return
def whenMyCancelButtonClicked(self, event) :
global myFrameLocIncr, myFrameLocOffset
if (DEBUGX):
print "ART205:", event
if (self.myFrame is not None): #Shut down window
self.myFrame.dispose()
myFrameLocIncr -= myFrameLocOffset
return
# define what Process button does when clicked - THIS DOES MOST OF THE WORK
def whenMyProcessButtonClicked(self, event) :
global myIdTagMgrListenerGlobal, myFrameLocIncr, myFrameLocOffset
if DEBUG:
print "ART212:", self.newIdTag, "User entered IdTagUserName: ", self.myIdTagUserName.text, " IdTagComment: ", self.myIdTagComment.text, "Frame: ", self.myFrame
#Temporarily remove listener
#idtags = jmri.InstanceManager.getDefault(jmri.IdTagManager) #Uncomment this line if using JMRI 5.5.3 or earlier
idtags.removePropertyChangeListener(myIdTagMgrListenerGlobal)
#Check if system name still available
if (self.newIdTag is not None):
#If user name was updated:
if self.myIdTagUserName.text is not "":
# Update IdTag Table entry
self.newIdTag.setUserName(self.myIdTagUserName.text)
# See if either Ops Locommotive or Car Table to be updated
self.assigned = False
# See if Ops Table update requested
if not (self.myIdTagUpdateCar or self.myIdTagUpdateLoco):
print "AssignId: Ops tables not updated for ", self.newIdTag.getUserName()
else:
self.updateOpsTables()
# Update comment into IdTag Table
if self.myIdTagComment.text is not "":
self.newIdTag.setComment(self.myIdTagComment.text)
#Get rid of open window
if (self.myFrame is not None):
self.myFrame.dispose()
myFrameLocIncr -= myFrameLocOffset
else: # Shouldn't ever get here
print "ART260: ERR System name not available"
# Re-attach the ID Tab Table listener
idtags.addPropertyChangeListener(myIdTagMgrListenerGlobal)
return
#############################################################
def updateOpsTables(self):
#Create abbreviations
cars = jmri.InstanceManager.getDefault(jmri.jmrit.operations.rollingstock.cars.CarManager)
engines = jmri.InstanceManager.getDefault(jmri.jmrit.operations.rollingstock.engines.EngineManager)
#Find in operations-car table and update
if self.myIdTagUpdateCar:
#Tom's: self.updateLocoCarTable("car", jmri.jmrit.operations.rollingstock.cars.CarManager)
self.updateLocoCarTable("car", cars)
#Find in operations-engine table and update
elif self.myIdTagUpdateLoco:
#Tom's: self.updateLocoCarTable("engine", jmri.jmrit.operations.rollingstock.engines.EngineManager)
self.updateLocoCarTable("engine", engines)
else: # Shouldn't ever get here
print ("ART249: ERR in Ops Table selection")
#See if RFID Tag previously assigned to another car or loco and remove from any (should not happen, but just in case)
if self.assigned: #Check in both types of tables
for rs in cars.getByRfidList():
self.findDupUN(rs, "car")
for rs in engines.getByRfidList():
self.findDupUN(rs, "engine")
return
def updateLocoCarTable(self, type_name, ops_table):
for rs in ops_table.getByIdList():
if type_name == "car":
self.processNewUN(rs, "car")
elif type_name == "engine":
self.processNewUN(rs, "engine")
if self.assigned:
break
else:
print("AssignId: No such " + type_name + " number " + str(self.newIdTag.getUserName()))
return
def processNewUN(self, rs, type_name):
if (DEBUGX):
print "ART281:" , rs.getId(), rs.getNumber(), "[", self.newIdTag.getUserName(), "]", rs.getIdTag(), rs.getRfid()
if rs.getNumber() == self.newIdTag.getUserName():
if rs.getIdTag() is not None:
print "AssignId: REPLACED " , rs.getRfid(), "on ", type_name, rs.getId()
#print("AssignId: REPLACED {} on {} {}".format(rs.getRfid(), type_name, rs.getId()))
rs.setIdTag(self.newIdTag)
rs.setRfid(self.newIdTag.getSystemName())
print "AssignId: Assigned" , rs.getRfid(), "to ", type_name, rs.getId()
#Tom's: print("AssignId: Assigned {} to {} {}".format(rs.getRfid(), type_name, rs.getId()))
self.assigned = True
return
def findDupUN(self, rs, type_name):
if (DEBUGX):
print "ART296:" , rs.getId(), "[", rs.getNumber(),"]", "[",self.newIdTag.getSystemName(), "]", rs.getRfid()
if rs.getRfid() == self.newIdTag.getSystemName():
if rs.getNumber() != self.newIdTag.getUserName():
rs.setIdTag(None)
rs.setRfid(None)
print "AssignId: REMOVED " , self.newIdTag.getSystemName(), "from ", type_name, rs.getId()
#Tom's: print("AssignId: REMOVED {} from {} {}".format(self.newIdTag.getSystemName(), type_name, rs.getId()))
return
############################################################################################################
# Define a Manager listener for the IdTagManager.
class AssignIdTagManagerListener(java.beans.PropertyChangeListener):
prevEventSource = None
prevEventProperty = None
prevEventNewValue = None
def propertyChange(self, event):
if DEBUG:
print "ART316: ", event.source, "/property=",event.propertyName, "/oldValue=", str(event.oldValue), "/newValue=", str(event.newValue)
if (event.source == self.prevEventSource and event.propertyName == self.prevEventProperty and event.newValue == self.prevEventNewValue): #Only process first event from this source (JMRI bug to be fixed 2023-08-14)
return
self.prevEventSource = event.source
self.prevEventProperty = event.propertyName
self.prevEventNewValue = event.newValue
#idtags = jmri.InstanceManager.getDefault(jmri.IdTagManager) #Uncomment this line if using JMRI 5.5.3 or earlier
if (event.propertyName == "beans" and event.newValue is not None and event.oldValue == None): #New RFID tag
newIdTag = idtags.getBySystemName(str(event.newValue))
if newIdTag is not None:
if DEBUG:
print "ART326: AssignIdTagManagerListener: ", newIdTag, "/", event.propertyName, "/", event.newValue
AssignIdTagToRSFrame(newIdTag, "")
elif (event.propertyName == "DisplayListName" and event.newValue is not None): #Added/changed user name to existing RFID Tag
existingIdTag = idtags.getByUserName(str(event.newValue))
if existingIdTag is not None:
if DEBUG:
print "ART332: AssignIdTagManagerListener: ", existingIdTag, "/", event.propertyName, "/", event.newValue
AssignIdTagToRSFrame(existingIdTag, str(event.newValue))
return
############################################################################################################
#----- MAINLINE --------------------------------------------------------------------------------
if globals().get("AssignIdTagToRS_running") is not None: # Script already loaded so exit
AssignIdTagToRS_log.warn("'AssignIdTagToRS' already loaded and running. Restart JMRI before loading this script.")
else: # Continue running script
AssignIdTagToRS_log.info("'AssignIdTagToRS' started.")
#idtags = jmri.InstanceManager.getDefault(jmri.IdTagManager) #Uncomment this line if using JMRI 5.5.3 or earlier
# Remove a prior listener, if any
if globals().get("myIdTagMgrListenerGlobal") is not None:
if myIdTagMgrListenerGlobal is not None:
idtags.removePropertyChangeListener(myIdTagMgrListenerGlobal)
AssignIdTagToRS_log.info("'AssignIdTagToRS' prior IdTag Table listener removed.")
myIdTagMgrListenerGlobal = None
# Attach the new sensor manager listener
myIdTagMgrListenerGlobal = AssignIdTagManagerListener()
idtags.addPropertyChangeListener(myIdTagMgrListenerGlobal)
AssignIdTagToRS_running = True #So script won't be loaded twice
AssignIdTagToRS_log.info("'AssignIdTagToRS' IdTag Table listener started.")