The following is an eclectic collection of functions, classes, and settings that make it easier to do specific tasks in Ignition. Each was built to assist a larger goal and are handy to have around, a means to an end. Many focus on introspection - allowing code to look at itself (and by extension you).
If you find an issues, bugs, failings, or disappointments - file an issue! Feature requests are appreciated, and pull requests triply so! Any modifications submitted will be called out and the script file will be updated with a __credits__
listing with your name (plus, it'll be tracked by Github).
Of all the scripts, the pretty printing functions are probably the most handy. Here's an example of how to use it, as seen from the Ignition script console:
>>> some_dataset = shared.tools.examples.simpleDataset
>>> from shared.tools.pretty import *
>>> p(some_dataset)
"some_dataset" <DataSet> of 3 elements and 3 columns
====================================================
a | b | c
<java.lang.Integer> | <java.lang.Integer> | <java.lang.Integer>
---------------------------------------------------------------------------------
0 | 1 | 2 | 3
1 | 4 | 5 | 6
2 | 7 | 8 | 9
>>> pdir(p)
Properties of "p" <'function'>
================================
(o, indent=' ', listLimit=42, ellipsisLimit=80, directPrint=True)
--------------------------------------------------------------------
Pretty print objects. This helps make lists, dicts, and other things easier to understand.
Handy for quickly looking at datasets and lists of lists, too, since it aligns columns.
Attribute Repr <Type>
------------- ------------------------------------------------------------------------------------------------------------------------ -----------
func_closure None NoneType
func_code <code object p at 0x2, file "<module:shared.tools.pretty>", line 181> tablecode
func_defaults (' ', 42, 80, True) tuple
func_dict {} dict
func_doc 'Pretty print objects. This helps make lists, dicts, and other things easier to understand.\n\tHandy for quickly loo... str
func_globals {'shared': <app package shared at 3>, 'JavaException': <type 'java.lang.Exception'>, '__copyright__': 'Copyright (C)... dict
func_name 'p' str
>>> pdir(some_dataset)
Properties of "some_dataset" <'com.inductiveautomation.ignition.common.BasicDataset'>
=======================================================================================
The most base type
Attribute Repr <Type>
-------------------- ------------------------------------------------------------------------------------------------------------------------ -----------------------
asXML u'H4sIAAAAAAAAALOxr8jNUShLLSrOzM+zVTLUM1BSSM1Lzk/JzEu3VQoNcdO1ULK34+WySSm2s0nO\nz7GzSbEztNFPAdEmUNocTOuDZaFKjKBSplDa... unicode
binarySearch (<int>, <java.lang.Object>) instancemethod
bulkQualityCodes None NoneType
class
columnContainsNulls (<int>) instancemethod
columnCount 3 int
columnNames [a, b, c] java.util.Collections
columnTypes [class java.lang.Integer, class java.lang.Integer, class java.lang.Integer] java.util.Collections
data array([Ljava.lang.Object;, [array(java.lang.Object, [1, 4, 7]), array(java.lang.Object, [2, 5, 8]), array(java.lang.... array.array
dataDirectly n/a write-only attr
datasetContainsNulls () instancemethod
equals (<java.lang.Object>) instancemethod
getAsXML () instancemethod
getBulkQualityCodes () instancemethod
getClass () instancemethod
getColumnAsList (<int>) instancemethod
getColumnCount () instancemethod
getColumnIndex (<java.lang.String>) instancemethod
getColumnName (<int>) instancemethod
getColumnNames () instancemethod
getColumnType (<int>) instancemethod
getColumnTypes () instancemethod
getData () instancemethod
getPrimitiveValueAt (<int>, <int>) instancemethod
getQualityAt (<int>, <int>) instancemethod
getRowCount () instancemethod
getValueAt (<int>, <int>) -OR- (<int>, <java.lang.String>) instancemethod
hashCode () instancemethod
notify () instancemethod
notifyAll () instancemethod
rowCount 3 int
setAllDirectly (<java.util.List>, <java.util.List>, <[[Ljava.lang.Object;>) instancemethod
setColumnNames (<java.util.List>) instancemethod
setColumnTypes (<java.util.List>) instancemethod
setData (<[[Ljava.lang.Object;>) instancemethod
setDataDirectly (<[[Ljava.lang.Object;>) instancemethod
setFromXML (<java.util.List>, <java.util.List>, <java.lang.String>, <int>) instancemethod
setValueAt (<int>, <int>, <java.lang.Object>) instancemethod
toString () instancemethod
wait () -OR- (<long>) -OR- (<long>, <int>) instancemethod
One of the neat tricks some of the functions abuse is how aware of their surroundings they are. Note that the p(some_dataset)
calls out the name of the variable itself, and that pdir(p)
lists out the arguments of the function. Note that pdir(some_dataset)
not only lists all the attributes, but the signatures of functions and the values of attributes, even though it is a Java object.
Tests are currently written for Python's built in unittest
and doctest
libraries. See the test/shared/tools
folder for coverage.
While I use the code here semi-regularly, do take a moment to understand any code you plan to use in production! This is always a good idea, but some of the scripts operate on Python magic. Some are extremely handy, like scope injection used in the Logger
to make Python3-style string formatting. Others like @async
gently abuse notation for convenience. And others like Trap
violently abuse the Python's internal runtime machinery.
All the code is safe. All applications of it are not.
That out of the way, here's the handy stuff!
Here's an overview of each script with a little about what each function has to offer.
Jython 2.5 (Ignition 7) does not have all the builtin features normally expected. Python 2.6 brought in a large number of important changes, some of which are not obviously missing. These are some of the more commonly backported/monkey patched details that seem to come up.
Worst offender? next
. Boy howdy to a lot of programs expect that to just exist.
>>> OrderedDict((key,value) for key,value in zip(list('asdf'),range(4)))
OrderedDict([('a', 0), ('s', 1), ('d', 2), ('f', 3)])
These functions make it easier to convert to and from the DataSet
Java object. Immutable and fast, DataSet virtues are also a chore to work with when so much of Python is geared around transparent iteration and mutability.
To better analyze Ignition projects, the dump
scripts can be used to trawl resources to disk. Typically, the files on disk are base-64 encoded gzip dumps of the XML generated. Scripts here help work around that to get the fine-grained XML detail for resources.
My typical use for this is performing a diff between projects. By consuming the XML and sorting & flattening objects, a typical diff algorithm can be used to compare, well, almost anything. The detail is extremely high, though, so beware the siren song of excessive depth and filter judiciously; otherwise you may find even trivial projects suffer from thousands of minor, inconsequential differences. (Careful and extensive filtering is the secret sauce to making sense of it all, and frankly it would be a problem regardless of the output, be it XML, JSON, or binary.)
It will not work on all resources. Please raise an issue if you find a resource that is not properly extracted; it may not be obviously fixable, but it's usually worth inspecting.
dumpProject
runs over the full currently-loaded project in the Ignition Designer and places the trawled output in the given directory on disk.getResources
is a more targeted function that will return only a certain class of object.serializeToXML
will (if easily possible) return the string the Java serializer for an object type generates.
Handy port of the easing functions from Javascript. If you need to make transitions, this is a nice way to smooth them out and look more natural.
The enum type doesn't get added to Python until version 3.4, but there's a use for this very specific type of object. Subclassing Enum
will effectively create a singleton class whose elements may be used transparently as their values, but can be checked against as an enumeration.
For example, take the following arbitrary enumeration:
from shared.tools.enum import Enum
class ArbFlags(Enum):
NUMBER_1 = 1
NUMBER_2 = 2
Fourth = 4
SomeString = 'This is a string.'
Note that the numbers may be referenced directly by name...
>>> ArbFlags.NUMBER_1
<ArbFlags.NUMBER_1 1>
>>> ArbFlags.NUMBER_1 + 3
4
But behave exactly as their type...
>>> ArbFlags.NUMBER_1 == 1
True
>>> ArbFlags.NUMBER_1 is 1
False
>>> ArbFlags.NUMBER_1 + 3 == ArbFlags.Fourth
True
>>> (1+3) is 4
True
>>> ArbFlags.NUMBER_1 + 3 is ArbFlags.Fourth
False
The having something act as its value while being distinguishable from that value is what makes enumerations special.
For testing purposes, it's helpful to not need to constantly redefine things. In code examples (as already seen) these can be called upon to provide a repeatable/demonstratable input. Plus, they're used by the tests.
Logging in Ignition comes in many flavors and scenarios. And yet, there are a few caveats. Can't use system.util.getLogger
effectively in the script console, defining loggers and their contexts can be verbose, and getting clients to log to the gateway is occasionally super helpful. This logging class lets you put as little or as much effort into this as you'd like.
In general, simply import Logger
and instantiate it when you want to use it.
Logger().debug('These words will be logged.)
You may use one of the following canonical logging levels (what the Java wrapper uses):
trace
debug
info
warn
error
log
(for whatever's set as default)
When created, the logger will inspect its calling scope and attempt to make sense of where it is. Currently, it can detect the following automatically:
- Script console (previously the Script Playground) - it will
print
instead of use the Java logger. - Module scripts - it will name itself after the script's module, like
shared.tools.meta
- Tags - it will name itself as a
[TAG PROVIDER] Tag Change Event
and prefix all logs with the tag's path - Perspective - It will name itself after the Perspective client's ID and prefix the log with the calling component's path (if possible)
- Clients - it will name itself after the Vision client's ID and prefix the log with the calling component's path.
- WebDev - it will name itself
[PROJECT NAME] WebDev
and prefix logs with the event name, like[GET endpoints/submit]
- Unknown - it will just call itself
Logger
. Nothing otherwise interesting.
Note: logging from the Client context requires that a message handler is set up. The handler is provided, though - just create a message handler named whatever is named in:
VISION_CLIENT_MESSAGE_HANDLER
PERSPECTIVE_SESSION_MESSAGE_HANDLER
with the following code
from shared.tools.logging import Logger Logger.messageHandler(payload)
The Logger
class will look back into its calling scope to figure out context. That allows it to do some helpful string work for you. The following are all equivalent (assuming there's a variable x
that is an integer
):
Logger().debug('The variable x is currently %d' % x)
Logger().debug('The variable x is currently %(ecks)d', ecks=x)
Logger().debug('The variable x is currently %(x)d')
These are functions that dig into the Python and Ignition context. Most have trivial defaults and are easy to use. Just be careful when using them - they are meant for ad hoc introspection and not constant use under production. They are not efficient but rather handy.
The Overwatch
class is a wrapper to simplify building your own debugger. Because it will execute on whatever provided events, it is possible to react to deeply detailed contexts, but it also means that it will really slow down execution. (Generally in a troubleshooting scenario this is fine, but understand that this causes additional code to be executed every. single. call/line/return/exception.)
Configured correctly, it could even act as a Trap
and behave as a just-in-time try
/except
clause for code you can't touch.
By default this will NOT run outside of the Vision Designer context.
Print things in an easy to read way! Ever print someList
and had it dump so many things you couldn't even scroll to the end? Hate having lists of lists be so hard to read because the columns don't line up? Want to dir
all the things but can't stand mere names of things? Wish you could print nested objects without getting confused?
Then p
and pdir
are for you. See the example at the start of this for how it works. Generally just take whatever you've got and jam it in the function.
The @async
decorator is a simple, easy way to make something asynchronous (run in its own thread) while keeping the boilerplate to a minimum. Call it with a number to set it to run itself after a pause, like this will run foo
in half a second:
@async(0.5)
def foo(x):
print x
Note also that the function returns the thread handle itself. See the docstring for the decorator for more details on how that works.
If you need to run something in a loop, but don't want to use a timer component, these classes should help.
AtLeastThisDelay
will pause execution after it's run until at least the given delay has passed. Handy for rate limiting things like REST calls.EveryFixedBeat
will execute every given time period, but if execution takes too long it will skip and wait for the next beat. Use this for when you need evenly spaced events.EveryFixedDelay
will execute each iteration after the given delay. Use this when you need at least a certain delay between iterations.
Note: Both
EveryFixedBeat
andEveryFixedDelay
behave as though they were called withenumerate
(returning both the iteration number and the last iteration's time each loop), butEveryFixedBeat
may skip iterations if the loop takes too long.
WARNING: This is under development and should NOT be used in production. This is still being ported into Ignition's Jython execution environment.
Trap
watches execution and does something when it sees a scenario come up (typically dropping into debug mode, if possible).
A fairly simple way to create an ad hoc virtual environment. By surrounding a block of code with Venv
, a virtual environment can be created. This will hoist the code into the given namespace, allowing for at-runtime creation of module contexts. Using this, you could load an entire library just-in-time without clobbering the namespace of the client. Anything created in the virtual namespace will be transient, allowing you to test code without risk to the shared global Python namespace.
You probably don't need to use this. But if you have a really badly designed singleton class that clobbers its own global variables during execution, this can be a very fast way to quarantine and isolate it without introducing any side effects.
See the docstring for details on use.
Subclassing and expanding on classes is usually pleasant in Python, but if that class is extremely clever (for example, using metaclasses) it may not be easy. Subclass Wrapped
and then set the type to what you want to expand on, and Wrapped
will use your code and for everything else interact as though it's that chosen type.
For example, the Sparkplug B specification uses Google Protobuf objects, and these can not be inherited sanely. Instead, you can do something like this:
from .sparkplug_b_protobuf import Payload
from .wrapped import Wrapped
class SparkplugBPayload(Wrapped):
_wrap_type = Payload
_allow_identity_init = True
def __init__(self, data=None):
"""Init for convenience.
If the data is already a protobuf payload, just use it.
If the data is a string to be parsed, init normally and parse.
"""
if isinstance(data, (str,unicode)):
super(SparkplugBPayload, self).__init__()
self._self.ParseFromString(data)
else:
super(SparkplugBPayload, self).__init__(data)
def addMetric(self, name, alias, metric_type, value=None):
"""Helper method for adding metrics to a container which can be a
payload or a template
"""
metric = self.metrics.add()
self.initMetric(metric, name, alias, metric_type, value)
# Return the metric
return metric
# etc.
The SparkplugBPayload
class here adds a helper function to the payload definition without interfering with the class itself.
Please open an issue if there's any problems with the scripts. If you have corrections or additional features submit them or provide a pull request. Anything pulled in will be listed here. Thanks!
Written with StackEdit.