
#
# A Digitrax BXPA1 AutoReverser-to-Sensor "follower"
#
# This script provides a JMRI sensor where the sensor's state "follows" the
# reported "autoreverser state" of a Digitrax BXPA1 device, as reported by
# LocoNet messaging.
#
# This script can be configured to monitor all BXPA1 devices, reporting each
# individual device's autoreversing state in its own device-specific sensor.
#
# Sensor naming is of the form "ISPM1aReversedX".  This is interpreted as:
#    "I" for JMRI's "Internal" object grouping
#    "S" for "Sensor" (in the "Internal" object grouping)
#    "PM" for "Power Management" effects
#    "1a" for a source from a "BXPA1" device
#    "Reversed" to denote the sensor as reporting "autoreversing" state (Active
#         means "Reversed", Inactive means "Normal")
#    "X" is the BoardID number reported in the LocoNet message from the BXPA1
#         device
#
# As written, this script supports either
#   - monitoring a single BXPA1 device, specified by BoardId number
#   - monitoring of _all_ BXPA1 devices and reporting each via a separate sensor.
#
# Configuring the script
# ----------------------
# - See comments below for information on configuring the behavior of this
#     script:
#     -- the "interestingBoardIdNumber" determines what BXPA1 device BoardID
#        value to watch for, or, alternately, configures the script to monitor
#        for LocoNet messages from _all_ BXPA1 BoardID values, and causes this
#        script to follow each unique BXPA1 BoardID number using a unique Sensor.
#     -- DebuggingMessages determines whether debugging messages will be sent to
#        the JMRI "Script Output" window.
#
# Script details
# --------------
# This script has three main functional parts (in order of appearance).
#
# The first part provides some "import" statements and declares some variables
# that influence script behavior.  Users may wish to modify some variable values
# in this section.
#
# The second part is where the bulk of the work is done, because it declares and
# defines a "Bxpa1ReverserStateListener" class which implements a JMRI
# "LocoNetListener" which has BXPA1-specific features.  The class's "message()"
# method is triggered upon JMRI receipt of a LocoNet message.  It "parses" the
# received LocoNet message to determine if it is a valid "Autoreversing" status
# message from a BXPA1 device.  If it is, the "BoardID" value is extracted and
# checked to determine if it is a BXPA1 for which the message should be reported
# via a JMRI Sensor object.  This determination is made in the isInterestingBoardId()
# method in Section 2, described below.
#
# For a message that is interesting to the script, the BoardId number from the
# message is used to create a variable representing the JMRI Sensor name, and
# that sensor name is used to create a JMRI sensor (if it does not already
# exist) and update the sensor's value.
#
# The isInterestingBoardId() method uses the user-customization variables of
# Section 1, along with the LocoNet BXPA1 Autoreverse status message's extracted
# boardID value, to determine whether or not to update a sensor.  It returns True
# if the message's BoardID value refers to a BXPA1 which should be tracked via a
# JMRI sensor, or it returns False to indicate that the message should be ignored.
#
# The third part of the script creates an instance of the LocoNet Listner class
# and "connects" that instance to the LocoNet connection.
#
# Notes and Limitations
# ---------------------
#
# Note the following limitations:
# - So far as the script author knows, there is _no_ way to query the autoreversing
#     state of a BXPA1 device.  As such, until a BXPA1 device changes its
#     auto-reversing state, there is _no_ way to tell what the device's current
#     state is.
#
# - This script creates, if necessary, the Internal Sensor used to "follow" the
#     BXPA1 device's autoreversing state.  Before a BXPA1 reports its autoreversing
#     state, JMRI will _not_ have a corresponding Sensor object, unless you have
#     opened a JMRI "panel" XML file which was saved when the corresponding sensor
#     was known to JMRI.
#
# - If you open a saved JMRI Panel XML file and that file had one or more JMRI
#     Sensor objects created by this script, such If you "save" a panel XML
#     file, JMRI will create the associate Sensors but will leave those sensors
#     in the "unknown" state.  Once a BXPA1 sends an "Autoreversing" event
#     LocoNet message, the associated sensor will be updated to "Inactive" or
#     "Active", as appropriate.
#
# - This script provides little if any "error-checking" of the configuration
#     variables.  Specifying an "out-of-range" connection index will result in
#     an exception reported in the JMRI Console log as well as failure of the
#     script to perform.  Specifying a BoardID value of 0 will not result in
#     capture of _any_ BXPA1 autoreversing data.
#     Various other configuration boo-boos may result in exceptions in the log
#     and/or failure of this script to perform.
#
# - Because of an apparent BXPA1 firmware issue seen in at least some BXPA1 devices,
#     BXPA1 devices with some BoardID values report their status as if they had
#     different BoardID values.  To avoid this problem, avoid using BoardID
#     values where
#           (your BoardID value) / 8
#     has a fractional part of .0, .625, .75, or .825.
#     As examples:
#                             BoardID / 8
#                  BoardId    fractional
#        BoardId     / 8         part        OK to use?
#           1        0.125       .125          Yes
#           2        0.25        .25           Yes
#           3        0.375       .375          Yes
#           4        0.5         .5            Yes
#           5        0.625       .625          No
#           6        0.75        .75           No
#           7        0.825        .825          No
#           8        1.0         .0            No
#           9        1.125       .125          Yes
#           10       1.25        .25           Yes
#           11       1.375       .375          Yes
#           12       1.5         .5            Yes
#           13       1.625       .625          No
#           14       1.75        .75           No
#           15       1.825        .825          No
#           16       2.0         .0            No
#           17       2.125       .125          Yes
#      ...
#
# - It is possible that a BXPA1 firmware revision that resolves the above BoardId
#     issue _could_ require re-work of the message parsing found in the LocoNet
#     Listener implementation.
#
# Script version 1.0 created 30Mar2021 by Bob M.
#
# Script version 1.1 created 26Nov2024 by Bob M.
#             - do a better job of watching for BXPA1 "Autoreverse" event
#               LocoNet messages.

# Part 1:

import jmri
import java

# Initialize the variable "connectionIndex" to specify which of potentially
# several LocoNet connections this script watches.  This is specified by the
# LocoNet connection's "index":
#    - When you have just a single connection, the only usable index is 1.
#    - When you have more than one LocoNet connection defined, the index is the
#      count, from the left, of the "tabs" for LocoNet connections as seen
#      in the "Connections" page of the JMRI "Preferences".
connectionIndex = 1 # this is appropriate for a JMRI install with only a single
                    # LocoNet connection, or when monitoring the _first_ of
                    # available (and active) LocoNet connections.

reportAllBxpa1s = False # assume that only a specific BXPA1 Board ID number
                        # will be reported.  This value may be overridden below.

# Initialize the variable "interestingBoardIdNumber" to reflect the
# BoardId of the BXPA1 you care about.  If you want to create a sensor
# for _each_ BXPA1 BoardId that reports autoreversing state, set
# interestingBoardIdNumber to a negative value.  Uncomment (and modify, # as
# needed) one of the examples shown below.
#
# Example: follow only Autoreversing messages from BXPA1 BoardID 1
#interestingBoardIdNumber = 1
#
# Example: follow only Autoreversing messages from BXPA1 BoardID 12
interestingBoardIdNumber = 12

# Example: follow every BXPA1 Autoreversing message, regardless of BoardID, by
# overriding the previous value of reportAllBxpa1s.  Note that the value of the
# interestingBoardIdNumber variable has no effect which reportAllBxpa1s is True.:
reportAllBxpa1s = True

# Print debugging messages to the "script output" window?
#  - True enables the debugging messages.
#  - False disables the debugging messages.
DebuggingMessages = True

# Part 2:

# Define a LocoNet "BXPA1 Autoreversing Event" listener class
class Bxpa1ReverserStateListener(jmri.jmrix.loconet.LocoNetListener) :
    def isInterestingBoardId(self, boardId) :
        # This method is used to determine whether a particular BXPA1's
        # autoreversing messages will be used to update an appropriately-named
        # JMRI sensor.
        #
        #   Returns True to update the sensor based on the LocoNet message contents.
        #   Returns False to ignore the BXPA1's LocoNet Message.
        #
        # The user may modify this method to suit his/her personal "selection"
        # criteria.

        if (reportAllBxpa1s == True) :
            return True
        if (boardId == interestingBoardIdNumber) :
            return True
        return False

    def message(self, msg) :
        # This method (if registered as a LocoNet listener!) parses all incoming
        # LocoNet messages and deals with BXPA1 autoreversing messages.

        # Is this message an autoreversing message from a BPXA1 device?
        if ((msg.getNumDataElements() == 6) and (msg.getElement(0) == 0xD0) \
            and ((msg.getElement(1) & 0x7E) == 0x62) \
            and ((msg.getElement(3) & 0xF0) == 0x50) ) :

            # Yes, the message is for a BXPA1 autoreversing message!
            boardId = 1 + (msg.getElement(2) * 8) + (msg.getElement(3) & 0x7)

            # Determine whether to update a sensor for the reported
            # BoardId number seen in this LocoNet message
            if (self.isInterestingBoardId(boardId) == False) :
                # Not interested in this particular board number
                if (DebuggingMessages == True) :
                    print ("Ignoring message apparantly from BXPA1 with boardID #"),
                    print (boardId),
                    print (".")
                return

            # Specify the sensor to be updated (create it if it isn't
            # present!)
            s = sensors.provideSensor("ISPM1aReversed"+str(boardId))

            # Update the sensor based on data from the LocoNet message
            if ((msg.getElement(3) & 0x08) == 0) :

                # update the sensor for "Normal" polarity
                s.state = INACTIVE
                if (DebuggingMessages == True) :
                    print ("Sensor"),
                    print ( s.getSystemName()),
                    print ("is set to Inactive (i.e. normal polarity) for"),
                    print ("autoreversing state of BXPA1 with boardID #"),
                    print (boardId),
                    print (".")

            else :
                # Update the sensor for "Reversed" polarity
                s.state = ACTIVE
                if (DebuggingMessages == True) :
                    print ("Sensor"),
                    print ( s.getSystemName()),
                    print ("is set to Active (i.e. reversed polarity) for"),
                    print ("autoreversing state of BXPA1 with boardID #"),
                    print (boardId),
                    print (".")

        # Nothing more to do, so ...
        return

# Part 3:

# Create an instance of the listener class
ln = Bxpa1ReverserStateListener()

# Register the Listener
jmri.InstanceManager.getList(jmri.jmrix.loconet.LocoNetSystemConnectionMemo).get(connectionIndex - 1).getLnTrafficController().addLocoNetListener(0xFF,ln)

if (DebuggingMessages == True) :
    print "Registered the BXPA1 Autoreversing Message listener"

# end of script
