389 lines
19 KiB
Python
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.")
|
|
|