Quantcast
Channel: Nick Mudge's Weblog

Datasets in Ignition

$
0
0

A dataset in Ignition is a set of rows and columns holding values, like a spreadsheet.

Many components in Ignition use datasets, including table components, dropdown list, list,template repeater and canvas and chart components. In addition database queries return results as a dataset.

It is very true that if somebody is going to do any Python scripting in Ignition then he/she is going to be dealing with datasets.

Being able to access data in datasets, create new datasets and create modified versions of datasets are importand skills, and being able to do so smoothly and easily makes programming in Ignition that much smoother and easier.

This article is going to describe three different kinds of datasets.

Dataset

The Dataset dataset is a Java data type that is used by component properties in Ignition. For example the "data" property of an Ignition table is a Dataset.

Dataset are treated as immutable. That means Datasets are not changed. When you want to "change" a Dataset you actually create a new, different Dataset that is different in someway than the one before. Some built-in system.dataset.* functions in Ignition make it easier to do. Here is an example that makes all the values in a column upper case:

table = event.source.parent.getComponent('Table')
for rowIndex in range(table.data.rowCount):
    value = table.data.getValueAt(rowIndex,"Col 2")	
    table.data = system.dataset.setValue(table.data, rowIndex, "Col 2",value.upper())

Notice that the system.dataset.setValue function assigns its return value to the "data" property of the table in each iteration of the loop. This is because the system.dataset.setValue function actually creates a new Dataset each time it is called, copying the Dataset that was passed in as an argument except making the specified value different.

Also notice that we cannot directly loop through the rows of the Dataset. Instead we need to create a Python list of integers by using the range function and then loop through the integer list, using each integer as a rowIndex. The Dataset data type does not provide any of Pythons nice and easy syntax for looping through data, accessing data or modifying data.

PyDataSet

A PyDataSet is similar to a Dataset but it provides some of Python's nice syntax for looping, row access and slicing. PyDataSet isn't useful for anything else. Like Dataset, PyDataSet is immutable. But there are no functions for creating new different PyDataSets. Database queries in Ignition return PyDataSets. Here's some examples:

table = event.source.parent.getComponent('Table')
#Convert a Dataset into a PyDataSet
data = system.dataset.toPyDataSet(table.data)
#loop directly through a PyDataSet
for row in data:
    #Nice python syntax for accessing values in rows
    print row["Col 2"]

#Nice python syntax for row access
print data[2]

#Nice python syntax for row and column access to get values
print data[2]["Col 2"]

#Nice python slice syntax for getting the first three rows
firstThreeRows = data[:3]
print firstThreeRows

#Python syntax to get the last row? No cigar, doesn't work.
print data[-1]

#No, this does not work because PyDataSet is immutable, cannot be changed
data[2]["Col 2"] = "something"

A PyDataSet can help you manually create a different dataset. Here's a way to make all the values in a column uppercase:

table = event.source.parent.getComponent('Table')
newData = []
columnNames = list(table.data.getColumnNames())
pyDataSet = system.dataset.toPyDataSet(table.data)
for row in pyDataSet:
    rowList = []
    for column in columnNames:
        if column == "Col 2":
            rowList.append(row[column].upper())
        else:
            rowList.append(row[column])
    newData.append(rowList)
table.data = system.dataset.toDataSet(columnNames,newData)

This code copies the PyDataSet into a Python list of lists, except it uppercases the "Col 2" column. Once this is done the list of lists is converted into a Dataset and assigned to the table.

MutablePyDataSet

The MutablePyDataSet takes things further. It is like a PyDataSet except it is mutable and it has many many methods for accessing data in it, sorting, searching and modifying it. Here's the code for uppercasing a column:

table = event.source.parent.getComponent('Table')
#convert the Dataset into a MutablePyDataSet
data = pa.dataset.toData(table.data)
for row in data:
    row["Col 2"] = row["Col 2"].upper()
table.data = data.toDataSet()

Very simple, we convert a Dataset into a MutablePyDataSet, loop though it and directly change the "Col 2" column in each row. Then it is converted back into a Dataset.

Here's another example that achieves the same thing but in a completely different way. It shows off some interesting and useful ways to access and change data:

table = event.source.parent.getComponent('Table')
data = pa.dataset.toData(table.data)
data.setColumn("Col 2",map(unicode.upper,data.columns["Col 2"]))
table.data = data.toDataSet()

To summerize briefly, the third line of code accesses the "Col 2" column of the MutablePyDataSet, creates a new "Col 2" list with uppercase values and sets the values in the "Col 2" column of the MutablePyDataSet.

Here's a more detailed description: The 'data.columns["Col 2"]' part returns all the values in the "Col 2" column as a Python list. 'unicode.upper' is a function that uppercases unicode strings. The 'map' function applies the 'unicode.upper' function to each item in the 'data.columns["Col 2"]' list and returns the results as a new list.

The 'data.setColumn' method sets the 'Col 2' column position in all rows in the MutablePyDataSet to the values from the list created by 'map(unicode.upper,data.columns["Col 2"])'.

One of the nice things MutablePyDataSet does is provide a very nice print out of its contents. This is very useful for development and debugging. Here is an example:

table = event.source.parent.getComponent('Table')
print pa.dataset.toData(table.data)
Output Console:
row | Col 1 Col 2       Col 3        
-------------------------------------
 0  | 84    TEST ROW 11 43.4355031232
 1  | 12    TEST ROW 16 87.0748299491
 2  | 21    TEST ROW 1  46.9309520282
 3  | 76    TEST ROW 0  79.041838165 
 4  | 94    TEST ROW 1  17.8630985677
 5  | 97    TEST ROW 2  94.3354725437
 6  | 20    TEST ROW 17 27.9129276711

The output shows the columns and rows of the data.

There are many examples of various capabilities and methods of MutablePyDataSet I could show you but an example already exists for each capability and method in the MutablePyDataSet documentation.

You can find real-world examples of using MutablePyDataSet on the Inductive Automation forum by searching for MutablePyDataSet.

The MutablePyDataSet is part of the PA Power Scripting Module.


PA Power Scripting Module

$
0
0

I created the Perfect Abstractions (PA) Power Scripting Module to make some things easier to do and to make new things possible.

I will give a free license for this module to the first five people that send me a good example of using it in some way.

Contents:

pa.db.insertRows

Have you ever had data in an Ignition table or dataset and wanted to insert the data into a database table?

Did you write a Python script that looped though the dataset row by row, inserting each row into a database table? Man, that is slow, especially if the script is running in a client.

The right way to do this is to create a single SQL query that contains all the rows to insert, for example:

INSERT INTO mytable (col1,col2) VALUES (?,?,?), (?,?,?), (?,?,?), (?,?,?)

That query inserts four rows of data into a database table (but such a query could insert many more rows). It is much, much faster than running four separate queries.

So you should aggregate multiple inserts into a single query when you can. But have you written the logic in Python to dynamically create such a multi-insert database query and arranged the values so they can be inserted? It is ugly and painful.

That is why I created the pa.dataset.insertRows function. Let it do the tiresome work of dynamically creating the single efficient database query needed to insert all the rows of your dataset into a database table.

pa.db.insertRows documentation

serverFunction

This is my favorite feature. It might be hard to learn or understand at first because it adds something new to Ignition. A new idea, that's not part of regular Ignition. But it is really worth understanding because it is awesome and because it allows you to do very valuable things that you could not do before.

It might also be hard to understand because it is a general capability. It allows you to do many different things so it can be easy to get lost in the generalness of it and not see the value. Kind of like a programming language. What good is a programming language? A programming language is so general that you can say things like, "A programming language allows you to do stuff, to make stuff". And to a lot of people that means nothing because it isn't specific enough. So don't let the generalness of this feature hide its value from you. And don't let specific examples of its use limit your understanding of what it could be used for.

serverFunction enables you to write a Python script that runs in the runtime Ignition client that calls a user-defined function that gets executed in the Ignition Gateway and returns its results back to the client. This is a remote function call from the client to the Gateway, with the return value returned back to the client. What can you do with this? That is up to your imagination.

You should really understand this so contact me personally if you don't understand after finishing reading this blog post.

Here is an example:

#First script
#This is in the project.util Python module.
@serverFunction
def getServerTextFile(filePath):
    textFileContent = system.file.readFileAsString(filePath)
    return textFileContent
#Second script
#This is in a visionWindowOpened component event script
filePath = "C:\\Users\\nick\\Documents\\MyTextFile.txt"
text = project.util.getServerTextFile(filePath)
root = event.source.rootContainer
root.getComponent("DocumentViewer").text = text

The "getServerTextFile(filePath)" function has been declared a server function. When it is called in the client it will be executed on the Ignition server and the results will be returned back to the client.

The second script executes when an Ignition window opens. The script retrieves a file from the Ignition server computer (not the client computer) and updates a Document Viewer component in the window with the text from the file. This is a nice way for a window to display the contents of a file from the Ignition server. Similarly a writeServerTextFile(filePath) server function could be written to save changes made in the client to files on the Ignition server computer.

Notice the "@" symbol before "serverFunction". You might wonder what that is. That is standard Python syntax for something that has probably never been used in Ignition before. That syntax is shorthand for creating a Python decorator, which is a function that takes as an argument a function and returns a function. In the case above the syntax is shorthand for getServerTextFile = serverFunction(getServerTextFile).

You can learn more about Python decorators by searching google for Python decorators. It isn't anything special.

@serverFunction replaces the function defined after it with a different function that makes a remote function call to the Ignition Gateway to the function with the same name that exists in the Gateway.

Three Great Uses of serverFunction

1. As you saw in the example above serverFunction provides a great way for runtime clients to retrieve resources that exist on the Ignition server computer. serverFunction can also be used to send data and resources from clients to an Ignition server computer.

2. There is another fantastic use of serverFunction that is completely different: improve performance of multiple dependent database queries. It sometimes happens that multiple database queries that depend on each other need to be run in a Python script in a project. This creates a performance problem because each query can only execute after the one before it has completed executing. Each query needs to be sent across a computer network, which could include the Internet, to an Ignition Gateway, executed and the results need to be sent back to the client. The back and forth network traffic nature of queries makes Python scripts run slow. If it takes 1 second to execute a database query in an Ignition client and a script needs to execute 5 of them then the Python script will take at least 5 seconds to execute which in many cases is a noticeable and annoying lag or frozen screen for users.

In the past the solution has been to write an ugly database stored procedure, which gets hidden away from Ignition. This isn't a good solution because stored procedures don't get saved with an Ignition project and it is much nicer to implement logic in Python than in a SQL langauge.

A server function can solve this problem. Often an Ignition Gateway is installed on the same computer server as the database server or close to it. This enables very fast communication between the Ignition Gateway and the database server. Therefore a client can call a server function that executes multiple queries. Now the queries run fast because they are executed in the Ignition Gateway very near the database server. There have been a number of times I wished I could send multiple database queries to the server to be executed together, now this can be done.

3. It is also possible for a client to call a server function that is defined in a different project than the one running in the client. This allows functions to be grouped into appropriate projects but they can still be called from other projects if needed.

serverFunction documentation

@run

@run provides a nicer syntax for calling system.util.invokeLater(function) and system.util.invokeAsynchronous(function) functions.

system.util.invokeLater(function) is used for two different things. It is used to execute a function after all events and bindings have completed in a client. This is sometimes useful in the "visionWindowOpened" window event script because this script executes before bindings. This script might set properties in the window which will get overwritten when bindings are executed. Defining a function in this script and executing it with invokeLater prevents component properties that have been assigned values in the script from getting overwritten by bindings.

The underlying programming framework used by Ignition clients is Java Swing. Java Swing is single threaded meaning all GUI access and interaction is done on a single thread called the Event Dispatch Thread or EDT for short. No other thread is supposed to touch GUI components.

But sometimes other threads are needed in Ignition clients. If an operation (like getting data from somewhere or making a lot of calculations) takes a long time to execute and the code is running in the EDT (GUI Thread) then the screen will freeze for the user and this is not good. No one likes a frozen screen for any length of time. And when something is taking a long time it is nice to be notified graphically of the progress. This is where the system.util.invokeAsynchronous(function) function comes in. system.util.invokeAsynchronous(function) executes a function in a background thread.

A way is needed for a background thread to update the GUI with its progress and update the GUI when it is done. This is the second use of system.util.invokeLater(function). When system.util.invokeLater(function) is called in a background thread the function that is passed to it is executed in the EDT (GUI thread). system.util.invokeAsynchronous(function) and system.util.invokeLater(function) are used together to run background threads and update the GUI.

Here is an example:

def async():
    result = functionThatTakesALongTimeToFinish()
    def later():
        event.source.parent.getComponent("Label").text = "Finished running"
    system.util.invokeLater(later)
system.util.invokeAsynchronous(async)

The system.util.invokeAsynchronous function returns immediately so the EDT is uninterrupted. When the functionThatTakesALongTimeToFinish() function completes the GUI is updated.

Here is the same code using the new @run syntax:

@run
def async():
    result = functionThatTakesALongTimeToFinish()
    @run
    def later():
        event.source.parent.getComponent("Label").text = "Finished running"

This second version is not much different than the first version. In the second version the explicit calls to system.util.invokeAsynchronous and system.util.invokeLater are gone and instead @run is at the top of the functions. @run will execute a function defined after it based on its name. Functions named "async" are executed with system.util.invokeAsynchronous and functions named "later" are executed with system.util.invokeLater. The specific function names are useful because they document what the function is for: asynchonous execution or EDT execution.

I found that using @run makes code cleaner, easier to read and write, especially when code is dense with invokeLater and invokeAsynchronous.

@run documentation

pa.thread.multiTask(functions, endFunction)

The pa.thread.multiTask(functions, endFunction) function executes multiple functions concurrently in different threads.

The pa.thread.multiTask(functions, endFunction) function is useful in cases where multiple long-running independent operations or code need to run. Instead of executing long running operations in sequence all the long running operations can run at the same time, speeding up the application.

Python lists, sets and dictionaries are thread-safe, which means they can be shared between concurrently running functions. When the concurrent functions finish running the endFunction is executed. The endFunction can do something with the results like update the GUI.

Here is a simple example:

myTextList = []
myFunctionsList = []

def func1():
   text = system.db.runScalarQuery("SELECT name FROM names1 LIMIT 1","datasource1")
   myTextList.append([text])
myFunctionsList.append(func1)

def func2():
   text = system.db.runScalarQuery("SELECT name FROM names2 LIMIT 1","datasource2")
   myTextList.append([text])
myFunctionsList.append(func2)

def func3():
   text = system.db.runScalarQuery("SELECT name FROM names3 LIMIT 1","datasource3")
   myTextList.append([text])
myFunctionsList.append(func3)

def updateGUI():
   @run
   def later():
       table = event.source.parent.getComponent("Table")
       table.data = system.dataset.toDataSet(["Names"],myTextList)

pa.thread.multiTask(myFunctionsList,updateGUI)

In this example the pa.thread.multiTask function executes three functions concurrently. Each function executes a query in a different database. The results are collected in the MyTextList Python list. After the three functions have completed running the results are put in an Ignition table in a window.

I have used the above pattern in production to concurrently execute 10 different queries at the same time. It is about 10 times faster than executing the database queries sequentially.

pa.thread.multiTask documentation

MutablePyDataSet

Datasets are very very common in Python scripts in Ignition. Ignition components use them to hold tabular data and SQL queries return results in datasets. So they are everywhere.

I found myself writing the same code over and over again in different projects for handling datasets. The solution was MutablePyDataSet. I put in MutablePyDataSet many capabilities and methods that are useful. If you think of more let me know.

MutablePyDataSet is a swiss army knife for datasets. MutablePyDataSet helps write clear, short, easy code for datasets.

MutablePyDataSet is like PyDataSet except that it has many methods for accessing and manipulating its data and it is mutable.

MutablePyDataSet can do so many things to datasets that a huge page of documentation is needed to show what it can do.

Some of MutablePyDataSet's power comes from the generality of its functions. For example the sortRows method can receive a comparison function to determining how rows should be sorted. A comparison function can sort rows in any way the programmer wishes. Similarly, the findRows, filterRows, and removeRows methods receive a function argument to determine the condition or algorithm to use for the task. Such methods give flexibility/power while removing looping boilerplate code.

MutablePyDataSets print out to the console very nicely. This is very useful for development and debugging. Here's an example:

columnNames = ["name","age","rank"]
rows = [["Bob",32,"Private"],
        ["Bill",28,"Major"],
        ["Sarah",34,"Colonel"], 
        ["Kane",56,"General"], 
        ["Kirk",46,"Captain"],       
        ["Steve",22,"Lieutenant"], 
        ["Spock",156,"First Officer"], 
        ["Sierra",18,"Private"]]                      
mutablePyDataSet = pa.dataset.toData(columnNames,rows)
mutablePyDataSet.filterRows(lambda row: row["age"] > 30)
mutablePyDataSet.reverseRows()
print mutablePyDataSet
Output Console:
row | name  age rank         
-----------------------------
 0  | Spock 156 First Officer
 1  | Kirk  46  Captain      
 2  | Kane  56  General      
 3  | Sarah 34  Colonel      
 4  | Bob   32  Private     

The code creates a MutablePyDataSet with columns and rows. The filterRow method removes all rows that do not meet the condition given in the lambda function. lambda is Python syntax for creating a function without a name on the fly. The MutablePyDataSet is reversed and printed.

Method calls can be chained. For example the code could be written like this:

print pa.dataset.toData(columnNames,rows).filterRows(lambda row: row["age"] > 30).reverseRows()

For a primer on Datasets and MutablePyDataSets in Ignition read this blog post: Datasets in Ignition

MutablePyDataSet documentation

pa.ignition.db.runPrepInternalDBQuery

This function enables you to query Ignition's internal database, (SELECT queries only). The results are returned as a MutablePyDataSet.

Here is an example that gives all the datable tables that can be queried in Ignition:

query = """SELECT TABLE_NAME
            FROM INFORMATION_SCHEMA.SYSTEM_TABLES
            WHERE TABLE_SCHEM='PUBLIC'"""
result = pa.ignition.db.runPrepInternalDBQuery(query)
print result
Output Console:
row | TABLE_NAME                                             
-------------------------------------------------------------
 0  | ALARMJOURNALSETTINGS                                   
 1  | ALARMNOTIFICATIONPROFILES                              
 2  | ALERTLOG                                               
 3  | ALERTNOTIFICATIONPROFILEPROPERTIES_BASIC               
 4  | ALERTNOTIFICATIONPROFILEPROPERTIES_BASIC_EMAILADDRESSES
 5  | ALERTNOTIFICATIONPROFILEPROPERTIES_DISTRIBUTION        
 6  | ALERTNOTIFICATIONPROFILES                              
 7  | ALERTSTATE                                             
 8  | ALERTSTORAGEPROFILEPROPERTIES_DATASOURCE               
 9  | ALERTSTORAGEPROFILES                                   
 10 | AUDITEVENTS                                            
 11 | AUDITPROFILEPROPERTIES_DATASOURCE                      
 12 | AUDITPROFILES                                          
 13 | AUTHPROFILEPROPERTIES_AD                               
 14 | AUTHPROFILEPROPERTIES_ADHYBRID                         
 15 | AUTHPROFILEPROPERTIES_ADTODB                           
 16 | AUTHPROFILEPROPERTIES_DB                               
 17 | AUTHPROFILES                                           
 18 | BASIC_SCHEDULES                                        
 19 | COMPACTLOGIXDRIVERSETTINGS                             
 20 | COMPOSITE_SCHEDULES                                    
 21 | CONTROLLOGIXDRIVERSETTINGS                             
 22 | DATASOURCEDRIVINGSQLTSETTINGS                          
 23 | DATASOURCES                                            
 24 | DBTRANSLATORS                                          
 25 | DEVICES                                                
 26 | DEVICESETTINGS                                         
 27 | DRIVERPROPERTIES                                       
 28 | DUAL                                                   
 29 | EMAILNOTIFICATIONSETTINGS                              
 30 | GENERALALARMSETTINGS                                   
 31 | HOLIDAYS                                               
 32 | HOMEPAGE_SETTINGS                                      
 33 | IMAGES                                                 
 34 | INTERNALAUTHMAPPINGTABLE                               
 35 | INTERNALCONTACTINFOTABLE                               
 36 | INTERNALROLETABLE                                      
 37 | INTERNALSCHEDULEADJUSTMENTTABLE                        
 38 | INTERNALSQLTPROVIDERSETTINGS                           
 39 | INTERNALUSEREXTRAPROPS                                 
 40 | INTERNALUSERTABLE                                      
 41 | JDBCDRIVERS                                            
 42 | LEGACYSQLTPROVIDERSETTINGS                             
 43 | LOGIXDRIVERSETTINGS                                    
 44 | MICROLOGIXDRIVERSETTINGS                               
 45 | MOBILEMODULESETTINGS                                   
 46 | MODBUSTCPDRIVERSETTINGS                                
 47 | OPCSERVERS                                             
 48 | PLC5DRIVERSETTINGS                                     
 49 | PROJECTS                                               
 50 | PROJECT_CHANGES                                        
 51 | PROJECT_RESOURCES                                      
 52 | REGIONSETTINGS                                         
 53 | ROSTER                                                 
 54 | ROSTER_ENTRY                                           
 55 | S71200DRIVERSETTINGS                                   
 56 | S7300DRIVERSETTINGS                                    
 57 | S7400DRIVERSETTINGS                                    
 58 | SCHEDULES                                              
 59 | SCHEDULE_PROFILES                                      
 60 | SCRIPTSETTINGSRECORD                                   
 61 | SIMPLETAGPROVIDERPROFILE                               
 62 | SIPNOTIFICATIONSETTINGS                                
 63 | SLCDRIVERSETTINGS                                      
 64 | SMSNOTIFICATIONPROFILESETTINGS                         
 65 | SQLTAG                                                 
 66 | SQLTAGALARMPROP                                        
 67 | SQLTAGALARMS                                           
 68 | SQLTAGEVENTSCRIPTS                                     
 69 | SQLTAGHISTORYPROVIDER                                  
 70 | SQLTAGPROP                                             
 71 | SQLTAGPROVIDER                                         
 72 | SQLTSCANCLASS                                          
 73 | SRFEATURES                                             
 74 | STOREANDFORWARDSYSSETTINGS                             
 75 | SYSPROPS                                               
 76 | TAGHISTORYPROVIDEREP                                   
 77 | TAGPERMISSIONRECORD                                    
 78 | TCPDRIVERSETTINGS                                      
 79 | TRANSLATIONSETTINGS                                    
 80 | TRANSLATIONTERMS                                       
 81 | UACONNECTIONSETTINGS                                   
 82 | UDPDRIVERSETTINGS                                      
 83 | XOPCSETTINGS  

pa.ignition.db.runPrepInternalDBQuery documentation

pa.ignition.getContext()

The pa.ignition.getContext() function returns the context object for the scope it is called in. For example if called in a client the ClientContext is returned. If called in a designer then the DesignerContext is returned. If called in the gateway then the GatewayContext is returned.

The context object provides access to the Ignition Module SDK API. This means that internal Ignition data can be accessed and changed with the context object. For example all of a projects resources, such as windows, client tags, client properties and scripts can be accessed through a context object.

pa.ignition.getContext() documentation

The PA Power Scripting Module is available on the Module Marketplace.

The module works fully without a license in an Ignition designer. Without a license it works fully during the 2 hour trial period in Ignition clients.

I am interested in being notified about new features and capabilities that could be added to this Ignition module. What features and capabilities do you want?

Getting Rid of GStore

$
0
0

GStore was some functionality I built into the Power Scripting module. It developed over a short time and now I think it is best to get rid of it.

GStore is short for global storage. It was a way to store global data in Python variables in Ignition projects. The main problem with it is that there is already a great way to do it -- just store global data in Python project modules. I thought I could add various functionality to gstore to make it more unique and useful. But I found that I could add the same functionality in more general ways.

GStore still exists in the Power Scripting module for backwards compatibility but I removed it from Power Scripting documentation and its use is no longer recommended.

GStore answered this question very clearly: "How can I store and use data globally in an Ignition project in a very flexible and dynamic way during runtime in an Ignition client?" But the best answer is use a Python module library; create a module level variable and functions for setting and accessing it, then use those functions in your application to store and manipulate global data.

Here is a simple example:

#project.machines Python module
#create module level variable
_machineConfig = {}
#create functions for setting and getting machines that can be used anywhere in the project
def addMachineConfig(machineID, config):
    _machineConfig[machineID] = config

def getMachine(machineID)
    return _machineConfig[machineID]

I really want to build new and useful things into the Power Scripting module, so let me know what you want.

Python Module Level Variables vs. Client Tags

$
0
0

I don't use client tags in Ignition much. Not because they are bad or something like that, but because there is something better: module level Python variables.

Python module level variables and client tags are both ways to store and use global data in an Ignition client.

Python module level variables don't have the following limitation: Client tags can only use a few Java data types.

Client tags have one data type for holding aggregate values, the dataset. Datasets are terrible for accessing, setting and manipulating data compared to Python dictionaries and lists. In addition datasets enforce a structure -- rows of the same kind of data. Python dictionaries allow you to structure and organize data in a way that is best suited to the data and how you use it. That makes short, easy to read and write maintainable code.

Python module level variables can hold data of any data type. In my experience Python dictionaries and lists are the most useful data types for storing and using global data in Ignition clients.

It is also nice that Python module variables can be defined in the same place as the functions and code that manipulate them.

Client tags have useful scalar data types like Integer, Float, String and so is most useful for holding single value data.

What About Tag Change Scripts and Other Client Tag Features?

Some client tag features are easily implemented in simple Python code. Here is a simple example that implements "value change script" functionality:

#project.machines Python module
#create module level variable
_machineConfig = {}
#create functions for setting and getting machines that can be used anywhere in the project
def addMachineConfig(machineID, config):
    _machineConfig[machineID] = config
    print "new machine config added:", config

def setMachineConfig(machineID,config):
    if machineID in _machineConfig:
        oldConfig = _machineConfig[machineID]
        _machineConfig[machineID] = config
        #value change script
        print "oldValue:",oldConfig, "newValue:",config
    else:
        addMachineConfig(machineID,config)

def getMachine(machineID)
    return _machineConfig[machineID]

Noticed that "value change script" code is executed any time a machine's configuration is changed by using the setMachineConfig function.

Here's a great comment from Alasdair Barclay (AlThePal) about this blog post:

As we got to know Ignition better we moved from the pointy-clicky interface to doing more things in code. It is more flexible but it's harder to get started - most beginners will need the pointy-clicky interface until they understand the concepts, then they can move to coding directly and enjoying more power.

Perfect Abstractions Ignition Project Development Style Guide

$
0
0

Having a consistent style when developing Ignition projects helps keep developers in agreement on how projects should be developed, facilitates communication and makes projects easier to understand and maintain.

I've been working on a style guide here: Ignition Project Development Style Guide.

I want the style guide to use conventions that most people are using already, so if the guide says something different that what you have seen or like, let me know.

Ignition Training in Sweden

New Ignition Modules From Automation Professionals

$
0
0

Phil Turmel, from Automation Prefessionals, has been working on some interesting new Ignition modules.

The Time Series Database Cache module was recently released on Module Marketplace.

The Time Series Database Cache module supplies dataset manipulation and database query functions that are optimized for use with large and/or high-resolution database tables containing timestamped data.

Check out the documentation for more details.

Simulation Aids Module

Phil recently released a public beta of a new Ignition module called the Simulation Aids Module. This module helped Phil test projects in-house when physical components were off-limits or didn't exist yet. See Phil's post on the forum about it.

PerfectChat: Instant Messaging and Chat Functionality for Ignition Projects

$
0
0

I am happy to announce the release of PerfectChat. It is a new product from the Perfect Abstractions Ignition development team.

PerfectChat is instant messenger and chat functionality that is very easy to use and add to any existing Ignition project that uses Ignition version 7.7.5 and up.

PerfectChat is freely available to anyone who wants to use it.

It takes less than a minute to install. It does not use a database connection so no database setup or configuration is needed.

It has no configuration. It just works. Here's what the chat window looks like:

Chat Window

Chat messages are stored in memory. They are collected and available as long as the client is running.

Messages can be sent to individual users or to a group chat room.

People can chat with each other even though they are using different projects. The projects need to be running on the same Ignition Gateway.

PerfectChat includes a south-docked notification window that gets displayed when the chat window is closed and a chat message is received. It notifies a user that he/she has received a message. It looks like this:

Chat Window

Clicking on the notification window will close it and at the same time open the chat window and select the user that the message came from.

Installation

An Ignition template with a button is used to install PerfectChat. Pressing the button executes special functions from the PA Power Scripting Module that ensure PerfectChat resources are installed without overwriting any existing resources in the project. This enables PerfectChat to be installed in any existing project safely. Here's what the "PerfectChat Installation" template looks like:

PerfectChat Template

The installation of PerfectChat requires that the PA Power Scripting Module is installed. The PA Power Scripting Module does not need a license to install PerfectChat. There is absolutely no license or cost for installing and using PerfectChat. It takes 2 minutes to download and install the PA Power Scripting Module.

Here's how to get PerfectChat running:

  1. Download this project file: PerfectChatInstallation.proj.
  2. Using an Ignition designer import the project file into an Ignition project.
  3. Open the "PerfectChat Installation" template that was imported.
  4. Put your Ignition designer into preview mode and click on the "Install" button.
  5. Installation is complete. You are done and there is nothing to configure.

You can now launch a client and open the chat window from the client menu. Here is what the menu looks like:

PerfectChat Template

The "PerfectChat Installation" template can also be imported from Inductive Automation's public cloud templates repository. It exists in the "Chat" category.

I created a video that shows the installation and PerfectChat in action. Video link: PerfectChat for Ignition Projects

The PA Power Scripting Module does not need a license for installing PerfectChat because all scripting functions provided by the Power Scripting Module work without a license when run in the Ignition designer. Since installation of PerfectChat is done in the Ignition designer all of the functionality of the Power Scripting Module can be used without any cost or license. PerfectChat itself does not use the Power Scripting Module.

I do suggest purchasing a license for the Power Scripting Module if its licensed functionality will be useful to you and/or save you time. Here is an article about some of its features: PA Power Scripting Module

PerfectChat was developed by the Perfect Abstractions Ignition development team. Please, feel free to contact us on any question about the chat usage or any development needs or consultation.

Some discussion and improvements are in this forum thread: Add Instant Messaging and Chat Functionality to Any Project


New Features in Ignition 7.8

$
0
0

Here's a list of new features and changes in Ignition 7.8:

New Reporting Module

The new reporting module is just as flexible and powerful as the old reporting module but has a significantly improved user interface for designing reports.

The new reporting module is using the same underlying reporting engine as before, so leaning the new reporting module will be easy for anyone already familiar with the old reporting module. But designing reports will be much easier because of the interface improvements.

The old reporting module only worked with the Vision module. It was not possible to have reports without the Vision module installed. The new reporting module is different - it can exist and work standalone, without the Vision module. But the reports created with the new reporting module can still be used in Vision clients.

The new reporting module is also backward compatible with the old reporting module. So if you upgrade the reporting module to the new one existing reports will still work. Existing reports will be the same after upgrading to Ignition 7.8.

It is possible to convert old reports to the new reporting style by following the instructions given by Kathy in this forum post: https://inductiveautomation.com/forum/viewtopic.php?p=53383#p53383

Scheduling reports to be generated and sent to people was a real problem for the old reporting module. This is solved in the new reporting module with built-in functionality to schedule the generation and sending of reports.

It appears that Inductive Automation tried to keep the flexibility and power of the old reporting system and at the same time fix its failings -- and it looks like they succeeded brilliantly.

I am really excited about the new reporting module in Ignition. It's a whole new ball game for creating reports in Ignition.

Check out the new reporting videos on Inductive University to learn more.

New Ignition User Manual

Ignition 7.8 has a new Ignition user manual. The online version allows users to add to the documentation by submitting comments. Check out the new Ignition user manual.

Gateway Network

The Gateway Network, new in Ignition 7.8, provides a way for Ignition Gateways to communicate between each other. The Gateway Network is provided by the Ignition platform, not by any Ignition module. The Gateway Network is exposed to Ignition module developers so it is possible to develop new features that use it. Inductive Automation plans to develop new features that use it. The new Enterprise Administration Module module uses the Gateway Network. See Gateway Network documentation to learn more.

Enterprise Administration Module (EAM)

The EAM module allows a user to monitor and control any number of Ignition Gateways from a central Ignition Gateway. See the documentation for more information.

Selectively Publish Resources

Ignition allows developers to makes changes to projects and save those changes without publishing those changes to Ignition clients that users are actually using. This allows developers to make changes and test those changes in staging clients before publishing those changes to their users. Ignition 7.8 adds the ability to select which resources to publish. Before Ignition 7.8 there was no choice -- everything would be published or nothing.

Now developers can make changes to a lot of resources like windows, Python modules, templates and reports, save those changes and selectively publish any of the resources when they are ready.

Publishing specific resources is done by clicking on the File -> Publish Selected... menu item in the Ignition designer. A popup window allows you to select which resources to publish.

New Interactive Script Console

The Script Playground in the Ignition designer has been replaced with a new interactive Python console. Here's what it looks like:

SQL Bridge Changes

The Tag Historian functionality in the SQL Bridge module has been taken out and put into its own module, the Tag Historian module. In addition the Tag History Splitter module has been integrated into the Tag Historian module.

The SQL Bridge module is now just transaction group functionality.

Copy & Paste Bindings

This is done by binding something to a property. Then right click on the property in the Property Editor and select "Copy Binding". Then right click on a different property (on the same or different component) and select "Paste Binding".

Copy Custom Properties

You can now copy custom properties on one component and paste them into another component. This is done by opening the Custom Properties editor on one component, selecting the custom properties that you want to copy, pressing Control-C, exit the Custom Properties editor, then open the Custom Properties editor for a different component and press Control-V. This copy and paste operation will also copy and paste the values of the custom properties.

New Ignition Module SDK

Inductive Automation released a new Ignition Module SDK that uses Maven, a software project management tool. The Ignition Module SDK is no longer downloaded from the Inductive Automation website. Instead Maven with the new Ignition Maven Plugin is used to get module development started. See the new Ignition Module SDK documentation for more information.

New OPC-UA Drivers (DNP3, Omron)

Inductive Automation released a new OPC-UA driver for the DNP3 protocol. See more.

A new driver to communicate with the Omron NJ series of controllers will be released soon.

Improved runScript Expression Function

The runScript function has a couple new features. When runScript executes a Python function it can now take additional arguments that are passed into the Python function. In addition, runScript can now reference custom methods on components by using "self" to refer to the component runScript is used with. Here is an example expression:

runScript("self.returnText",0,"neat")

In the example above "self.returnText" refers to the returnText custom method on the component that the expression is used on. The 0 is the polling rate. The specific value 0 turns polling off. The "neat" argument is passed as an argument to the "returnText" method.

The ability to pass arguments to Python functions in this way is much easier than alternative ways and executes a lot quicker.

The ability to use "self" to reference a component makes it possible to directly call custom methods on components with runScript. This was not possible before.

Improved Custom Functions

Python functions have built-in functionality for handling default argument values, variable arguments and keyword arguments. Custom methods on components now support these. Here's examples of different argument functionality:

#default argument value
def returnText(text="No Text"):
   return text
#Passing in an argument is optional since a default value exists
print returnText()

#variable arguments
#the args variable will contain a list of all arguments passed into the function
def returnText(*args):
   print args
returnText("first arg","second arg","third arg")

#keyword arguments
def returnText(**kwargs):
   print kwargs
#kwargs becomes a dictionary with the items: {'f':'first','s':'second'}
returnText(f="first",s="second")

Power Table Improvements

The Power Table component has a new property called "View Dataset" which contains the data in the table as it appears on the screen, after any column hiding/sorting/re-ordering and filtering.

It is now possible to select rows on a power table, press Control-C (copy) and then paste the rows as text into a text editor or other places.

New Scripting Functions for User Schedules

New scripting functions have been added for getting/adding/editing/removing schedules and holidays. See the new scripting functions in the documentation.

New Scripting Functions for Rosters

New scripting functions have been added for getting and creating rosters. Rosters are used to send alarm notifications to users. The two new scripting functions are system.alarm.createRoster and system.alarm.getRosters.

Roles for Creating Projects

Ignition 7.8 added functionality to specify what roles users must have in order to create new projects. The roles are specified in the "Gateway Settings" webpage in the Ignition configuration web application.

Popup Windows Can Overlap Docked Windows

A new option allows popup windows to overlap docked windows. See the image on the right to see this in action.

The new option exists in the project properties of a project in the designer, specifically: Project Properties -> Client -> User Interface. It is a checkbox called, "Prevent Popup/Docking Overlap".

New DataSet and Array Tags

Tags in Ignition can now hold arrays and DataSets. Query tags can now hold multipe rows of results by storing them in a DataSet.

BestFit Layout Mode for Template Repeater

The new "Best Fit" layout mode for the template repeater fits the template instances within the repeater without scrolling and maintains aspect ratios.

Ignition Titles

Web browser tab titles and Ignition designer titles show the system/server name of the Ignition installation being used. This helps differentiate different Ignition installations when using more than one at the same time.

New Ignition Pricing

$
0
0

Inductive Automation released a new pricing web application. You can choose various packages, Ignition modules and functionality and get prices instantly. Check it out. Zack Scriven writes details about it on his blog.

In a recent forum post Carl Gould describes Ignition price changes:

Yes, the pricing changed. I wouldn't expect this every year, but this year we did make quite a few adjustments, as you all noticed. We felt that the improvements to the Reporting module warranted an increase in the price. However, I would draw your attention to the fact that we dropped the price of the Vision module by more than the increase to the Reporting module. We also split the Tag Historian and SQL Bridge products in order to drop the entry point for a number of types of packages, allowing us to offer the new Foundation package at a sub $10k price point, a package with no limitations whatsoever. We also dropped the price of alarm notification module. We've decided to start charging for drivers in order to help fund development of more protocols, so make sure your new quotes don't include drivers you aren't going to be using. I hope this demonstrates that prices were adjusted, up *and* down, all around, this wasn't a general price increase.

Also, I think you may have missed one of the major new changes: the price of redundancy dropped significantly. Redundant gateways are now priced at 50% of the primary gateway! This means that a Redundant Pro is *less* than Mission Critical Works was.

Updated Perfect Abstraction Modules to Ignition 7.8

Understanding Threading, system.util.invokeAsynchronous in Ignition

$
0
0

The Vision Module, which is installed in Ignition by default, enables people to create Java-based graphical user interfaces. The underlying Java library that Vision Module GUIs are created upon is Java Swing. Java Swing is a standard graphics library that ships with Java, so you can learn all about it on the Internet and in books.

Because Vision Module-based GUIs are built on top of Java Swing, they follow the rules of Java Swing.

A primary rule of Java Swing is that the user interface is single threaded.

This single thread is called the Event Dispatch Thread or EDT. Code that touches the user interface is supposed to execute in the EDT. This paradigm eliminates numerous multithreading problems and hassles by not having multiple threads touch the GUI.

All the component event handling code in Ignition and the extension functions on components run in the EDT by default so you don't have to do anything special to make your code execute in the EDT. It is the default.

But if this was the end of the story there would be a big problem in Ignition GUI development. The GUI should be and needs to be very very fast - always. A slow, unresponsive GUI (even if it is just a little slow) means that a programmer didn't program it to be fast.

Some code is a little slower than very very fast, and some code is a lot slower, and there is no way around it. Database queries, requests over the network, computations over large sets of data, all these things can take awhile.

If long running code executes in the EDT then the GUI will freeze and appear slow. This is obvious since the EDT can't do anything else until it is done doing what it is currently doing. So if the EDT needs to make a database query (or other such thing) over a potentially slow network the GUI will freeze while this happens and the user will feel like the application is slow or broken.

Executing code that directly affects the GUI such as setting values on component properties, moving components, changing the display of components in some way, and configuring components is usually very fast. It is actually the non-GUI code that is most often slow, like getting data over a network and processing lots of data.

So the question is, How do you make a very fast GUI always when you have code that takes awhile to execute? The answer is to separate the slow (data processing or data retrieval) code from the fast GUI manipulation code. Put the slow code in a background thread. When the slow code completes execution have the background thread pass the data results to code in the EDT that configures and updates the GUI with that data.

Ignition provides the system.util.invokeAsynchronous(func) function to execute a function in a background thread. And Ignition provides the system.util.invokeLater(func) function to pass data produced in a background thread back into the EDT so the data can be used to update GUI components.

Here is a simple example:

#This code could be on an actionPerformed event of a button to retrieve, process and display data.
#This code will prevent the GUI from freezing if the code to get and process the data takes awhile.
def async():
    #Fetch and process a lot of data. Return a useful dataset
    dataset = functionThatTakesALongTimeToFinish()	
    def later():
        label = event.source.parent.getComponent("Label")
	label.text = "Processed and retrieved %s results."% dataset.getRowCount()
	event.source.parent.getComponent("Table").data = dataset
    system.util.invokeLater(later)
system.util.invokeAsynchronous(async)

In the code example above the async function is executed in a background thread. The async function takes its time fetching and processing data. During this time the GUI is responsive and not frozen. Then the async function calls the system.util.invokeLater function, passing the function named 'later' as an argument. The system.util.invokeLater executes its argument (the function called later) in the EDT. Notice that function later can access and use the dataset variable that was created in the async function.

It is possible to sprinkle a background thread with multiple calls to system.util.invokeLater in order to update the user interface as the background thread makes progress. This keeps the user informed of the progress of something happening.

Note that the runScript expression function in Ignition executes in the EDT. So runScript should only execute Python code that is fast and not do things that is potentially slow like execute database queries or otherwise access the network.

The PA Power Scripting Module makes writing code that uses system.util.invokeLater and system.util.invokeAsynchronous easier with @run.

Graphical Object Inspector In Ignition

$
0
0

A Perfect Abstractions Ignition Project Developer recently made a graphical interface for displaying all the available super classes and methods that exists in a component.

Sometimes it is very useful for a developer to see all that can be done with a particular component. This new object inspector helps a developer do that.

The functionality exists in a single template called TreeObjectInspector. It works directly in the Ignition designer and in clients. The template is freely available and can be imported into your own projects. The template works in Ignition 7.8.0 and up. Download here: TreeObjectInspector.proj.

Here is a screenshot of the template displaying the super classes and methods of the button component in Ignition:

Ignition 7.9 Released!

Discover Gallery Features Project Developed By Perfect Abstractions

$
0
0

LafargeHolcim's Technical Information System Ignition project was recently featured in the Discover Gallery at the 2016 Ignition Community Conference.

The user interface in this project was developed by Perfect Abstractions. You can see a video about it at this link: Ignition Helps Huge Company Empower Thousands of Users

Here's a brief description of some of the interesting things we did:

Dynamic Creation of Template Instances

We displayed a list of names of Ignition templates in the client. We implemented the ability for users to drag a template from the list and drop it on a window. An instance of the template would be dynamically created and placed where the user dropped it. After that the user could resize, move and configure the template. Whatever the user did it could be saved and used later. We essentially created a simplified Ignition designer in the Ignition client that regular users could use to create and save functionality. You can see this functionality in the video.

Graphics Library Integrated Into Ignition

We integrated a graphics library into Ignition that shows how things are connected to each other in a graphical, animated and interactive way. You can see this functionality in the video.

Excel in Ignition

We heavily customized an Ignition Power Table to behave and function like an Excel spreadsheet. Users can add columns and rows. Users can input values and create formulas. Users can do various text formatting like change font, color, height, change text alignment and more. Users can also drop Ignition tags into cells and see real-time values. And more can be done. You can see this functionality in the video.


Power Scripting Module Released for Ignition 7.9

Drag and Drop Ignition Templates on the Template Canvas

$
0
0

I was curious if it was possible to implement the ability to drag and drop Ignition templates onto a template canvas. So I implemented it. I found that it is possible and it works. I am releasing the Template Canvas Drag and Drop project so you can have it, use it, and modify it. Download here: TemplateCanvasDragAndDrop.proj

This project shows a way for users to dynamically create Ignition screens in the runtime client by choosing templates that were created in the Ignition designer and dragging and dropping them onto a template canvas component. Additional functionality such as saving the states of screens to a database and opening them could be developed.

The Template Canvas Drag and Drop project has two windows. One window shows a list of Ignition templates that users can choose from to make a screen. Ignition automatically stores a thumbnail image of every Ignition template that is created. So the first window simply grabs the thumbnail images from the project and displays them to the user.

Users can click on a thumbnail template image and drag it onto the other window. When they drop the template the template appears in the other window where it was dropped. Then the user can select the template and move it around on the screen. I also implemented a right-click popup menu so users can right click on a template and delete it.

At the top is an "Edit" button. When this button is selected the project is in edit mode and existing templates can be clicked on and dragged around. When the button is unselected templates can be interacted with in the normal way.

The project is a good start but there is much more that can be implemented from this point. It shows that these things are possible. The ability to resize templates with the mouse can be implemented. The ability to save user-created windows, close them and open them can be implemented, and much more.

Here is a video that shows the project:

While dragging and dropping templates on a template canvas does work I admit that it is not ideal. There is one main annoyance: a change to the template canvas causes all templates to be reloaded. So if you just want to change the position of one template or add/delete one template, then all the templates get reloaded and there is a flash of a "Loading" message. Implementing configuration functionality on templates might be difficult because templates loose their state when the template canvas changes. If you can live with these problems then drag and drop with a template canvas can work.

A better solution would be to have a scripting function that can dynamically create template instances and add them to containers. This is something I've been thinking of working on. I am curious how many people would want this. I would add it to the Power Scripting Module.

Ignition Clients Displayed on High Resolution Monitors

$
0
0

Awhile ago Travis Cox gave a solution on his blog to make Java applications (and therefore Ignition clients) display nicely on high resolution monitors.

Check out his solution here: Java and High DPI Displays

Storing Images in Ignition Projects

$
0
0

Normally images in Ignition are stored in the Ignition gateway, not in Ignition projects. This makes it easy to reuse the same images in multiple projects, but it makes projects less portable because images used in projects are not exported with projects.

So you might add your own special icons and images to Ignition and use them in your project. Later you export your project and send the project export file to a colleague. Your colleague imports the project into a different Ignition server and the images are missing. Ah man, too bad. This blog post shows how to solve this problem so that your images stay with your exported projects.

One way to solve the problem is to use SVG graphics within your projects. SVG graphics are components in Ignition and are saved in Ignition projects.

However if you want to use jpeg and/or png images in your projects and want them to be exported as part of project files, follow the steps below.

Or you can just watch this video: How to Store Images In Ignition Projects

How to store jpeg and png images in an Ignition project

Step 1.

Create a new client tag with the tag path and name, "project_images". Make the value of the tag an empty dataset. This client tag is where the project images will be stored.

Step 2.

Download this project file: ProjectImagesTemplates.proj. Click on the link to download the file.

Import that project file into your existing Ignition project using the Designer.

The project file contains two Ignition templates: The ProjectImageStorage template and the ProjectImage template.

ProjectImagesStorage template:

The ProjectImagesStorage template is used to upload and manage images in your project.


ProjectImage template:

The ProjectImage template is used to display images that are stored in your project.

Step 3.

Open the ProjectImageStorage template in the Ignition designer. Put the Designer into preview mode and click on the "Upload Image" button to find images on your local computer and upload them into the project. Give names to images at the same time you upload them.

Uploaded images will be added as rows to the "[client]project_images" tag and will appear in the table in the template. The table is bound to the "[client]project_images" tag.

It is possible to change the name of an uploaded image by double clicking in a name cell. It is possible to remove images by selecting a row in the table and pressing the "Remove Image" button.

Be sure to use the ProjectImageStorage template in the Ignition Designer and not in a client.

Step 4.

Drag and drop the ProjectImage template on windows and templates where you want to display images.

The ProjectImage template has a template property called, "imageName". For each ProjectImage template instance on a window or template input the name of an image as given in ProjectImageStorage template. Setting the name of an image in the imageName property will make that image display.

Step 5.

Save the project in the Designer. The images that are uploaded in the Designer will not be available to clients until the project is saved.

Step 6.

The next step is to join my email list. Just kidding, you are done. But you should join my email list if you haven't already. So you are updated about new blog posts.

The solution given in this blog post is an efficient way to store images in projects and display them multiple times across multiple windows and templates. Your .proj project export files will include your images. And when you import your projects into a different Ignition server, your images will be there, displaying and working.

Check out this video that shows the steps: How to Store Images In Ignition Projects

View the Ignition wrapper.log File in the Designer or Client

$
0
0

One of the Perfect Abstractions Ignition project developers developed a very useful tool for viewing the Ignition wrapper.log file in an Ignition client or designer.

Find out what the Ignition wrapper.log file is and why it is useful.

We are releasing the Wrapper.log Viewer project here for other people to use. Download this project file: WrapperLogViewer.proj. Ignition 7.7.5 or higher is required to import the project.

Here is a screenshot of the project:

The Wrapper.log Viewer project was designed to be very easy to install and use. To install it import the Wrapper.log Viewer project into an existing Ignition project or import it as a new project. Note that importing into an existing project will delete existing Gateway Event Scripts in the project. That is all that is needed to install it. No database connections or anything else.

To use the Wrapper.log Viewer project in an Ignition designer open up the ViewerWindow window and put the designer into preview mode. The current contents of the wrapper.log file will display within 5 seconds.

To use the project in a client simply start a client and open the ViewerWindow window. Very easy, nothing to it.

The Wrapper.log Viewer project has been tested on both Windows and Linux.

How it Works

The Wrapper.log Viewer project has a Gateway Timer Script that runs every 5 seconds. It checks to see if the wrapper.log file has been modified. If it has been modified then the last 64,000 characters of the wrapper.log file are copied into a memory tag. The memory tag is automatically created when the project is first used.

A binding and Python scripting in the ViewerWindow window updates the display of the wrapper.log file when the content changes.

The AutoRefresh button is used to toggle on and off the functionality to automatically update the display of the wrapper.log file content. The auto update functionality is on by default.

The ViewerWindow window automatically scrolls the display of the content to the newest content as it comes in.

The Highlight Text field is used to highlight text in the wrapper.log contents that is displayed. This is useful for searching and finding specific text in the content.

Find out what problem the Wrapper.log Viewer project solves.

More Useful Projects

The Perfect Abstractions development team is interested in developing more useful tools and projects. If you think of something that you want feel free to tell us. We might implement it.





Latest Images