Ignition SDK Programmer's Guide
How-to Articles
Strategic Partner Links
Sepasoft - MES Modules
Cirrus Link - MQTT Modules
Resources
Inductive University
Ignition Demo Project
Knowledge Base Articles
Forum
IA Support
SDK Examples
Sepasoft - MES Modules
Cirrus Link - MQTT Modules
Inductive University
Ignition Demo Project
Knowledge Base Articles
Forum
IA Support
SDK Examples
Before jumping into the technical aspects of programming for the gateway, client, and designer, there are a few core concepts that should be addressed at a high level. This chapter will introduce certain design paradigms that are common across all of Ignition, and may be different than other systems that you've programmed for in the past.
Most of the topics covered in this chapter will be discussed more completely later, but we felt it was important to provide a conceptual overview as soon as possible.
Ignition uses the Apache Log4J framework to record events, errors and debug messages. Log4J is a simple framework that makes it easy to log information, and store those logs in different formats.
Log4J is a logging system, meaning that information is stored to it, recording what happened in the system. The information is stored to a particular Logger, which has a name, with a certain Severity rating. Appenders in the system receive the messages, and do something with them- such as writing them to a text file. Appenders can be configured to only log messages of a certain severity, and at any
time we can set the level of severity that a given logger will accept by using that logger's name. For example, trace severity, which is the lowest, is usually very verbose, and not logged by default. If we are trying to troubleshoot a particular part of the system, and know which logger it uses, we can go and turn on trace logging for just that one, in order to see all of its information.
You can obtain a "Logger", which is a named entity used to log messages, by calling the static LogManager.getLogger functions. There are two primary methods used to get loggers- by class, and by name. The class method simply uses the full path of the class, whereas the name method allows you to use any name you'd like. For example:
Logger log = LogManager.getLogger(getClass()); log.info("System started.");
or
Logger log = LogManager.getLogger("MyModule.CoreSystem.Status")
Note that in Log4J, names are seperated by ".". With the second example above, for instance, if we had other loggers with similar names under CoreSystem, we could set them all to log debug messages as well by setting that level directly on MyModule.CoreSystem.
While it is easy to get loggers based on class name, it is important to remember that these names will have little meaning to customers and integrators trying to use your module. On the other hand, using completely custom names sometimes makes it difficult to later track down where things are occurring in code. Still, it is usually advisable to use custom names that have meaning to both you and the end user, in order that someone who is trying to troubleshoot a problem can find the logger without help.
Logging information is an art, and one that is difficult to perfect. There are a few elements that play into the concept of what "good logging" is:
Log messages involving an exception should always include a custom message, in addition to the exception object. If only the exception object is provided, some appenders will not store the stack trace, making troubleshooting very difficult.
The use of severities is ultimately up to you, but here is a general guideline:
There are several ways to store configuration data (as opposed to process data) in Ignition, depending on what the data represents, and how it is used. The majority of data will be stored in the Ignition Internal Database, an embedded database that is managed by the platform, though in special cases it is necessary to store data directly to disk.
Nearly all configuration data is stored in the internal database. This database is automatically replicated through the redundancy system, and provides a host of benefits over traditional file based storage.
Furthermore, there are several helpful abstractions build on this system that make it very easy to perform common tasks like store and retrieve settings, project resources, etc. Ultimately, it is very unlikely that you will ever interact directly with the internal database though a SQL connection.
Ignition includes a simple, but highly functional, ORM (object-relation-management) system, making it very easy to store and retrieve data. This system manages the creation and maintenance of tables, while still providing a high level of advanced accessibility. The PersistentRecord system is most often used directly for storing module configuration data and other information that is global to the server, that is, not part of a project.
Project data is handled through the project management system (ProjectManager in the gateway context), which in turn uses the PersistentRecord system mentioned above. The project management system makes it trivial for a module to store and retrieve any type of data required for operation, as long as it makes sense that the data should be local to a project. It's important to remember that a system may have multiple projects running at once, and a project might be cloned and run multiple times. The project management system also provides facilities for resource change history, multi-staged resource lifecycles (ie, "staged" vs. "published" resources), and the option to protect resources from modification once deployed (the "runtime lock" feature).
It is occasionally necessary to store data directly to the gateway's hard drive. This is possible, and files and folders can be created under the gateway directory, but it's important to realize that the data in these files will not be included in project exports, database backups, or transferred to redundant nodes. Many times, however, it is these traits exactly that make storing directly to disk attractive.
Currently there are only a few pieces of Ignition that store data in this way: the redundancy settings, the alert state cache, and several drivers' tag data caches. In general, you will want to store data under GatewayContext.getHome(), which is the home data directory for Ignition. However, if the data is temporary and only pertains to the current running session (in other words, should be cleared on gateway restart), you can store to GatewayContext.getTempDir().
The Extension Point system is a single, unified interface for extending various parts of the Ignition platform. Using extension points, modules can provide new implementations of various abstract concepts. The Extension Point system is closely tied to the persistence and web interface system, reducing the amount of work required to expose and store configuration data.
The following systems expose themselves as extension points, meaning that modules can provide new implementations of them:
Alarm Notification Audit Profiles Authentication Profiles
OPC Server Connections SQLTag Providers
To get started building an extension point implementation, see the Extending Ignition with Extension Points chapter.
Ignition supports redundancy, in which two gateways share configuration, and one is "active" at a given time. Obviously, it is crucial that module developers consider early on in the development process how this affects their module. There are two main aspects that must be examined: configuration, and runtime.
Generally, only modules that operate in the gateway scope need to be concerned with redundancy. Client and Designer modules will usually not need to know the state of the gateway they're connected to.
The specific aspects of dealing with redundancy will be highlighted as they arise in other sections. This is only intended to lay out the scope of what you, the developer, must keep in mind when designing a module.
For redundancy to work correctly, it is crucial that the two gateway nodes have the same information in their configuration databases. Configuration is synchronized from the master to the backup node as quickly as possible, in the form of incremental updates. If an error occurs or a mis-match is detected, a full gateway backup is sent from the master.
Anything that a module stores in the internal database must be sent across to the backup node. Conveniently, when using the PersistentRecord system, this is handled automatically behind the scenes. Other changes can be duplicated to the backup through the RedundancyManager in the gateway. Using that system, any operation that would change the internal database is executed as a runnable, and on success, is sent across and executed on the backup.
The runtime considerations for redundancy come in two forms: runtime operation, and runtime state. The first, runtime operation, is how the module acts according to the current redundancy state. There are several aspects of redundancy that must be addressed, such as the meaning between "cold, warm and active", and how historical data is treated. Your module can subscribe to updates about the current
redundant state, and respond accordingly.
The second category of concern is runtime state. This is the "current operating state" of your module, and is the information that the module would need to begin running at the same level on the backup node, if failover should occur. In many cases, modules can just start up again and recreate this, but if not, it is possible to register handlers to send and receive this runtime state data across the redundant network. For example, in the alerting system in Ignition, alert messages are sent through the runtime state system, so that on failover the current states, including acknowledgements, is accurate. In this case, if the data was just recreated, it would not have the acknowledgement information, and would likely result in new notifications being sent.
Localization is the process by which an application can be adapted to use a different language, and to present information in a way that is consistent with what users in different countries are accustomed to (date formatting, for instance). Ignition supports localization, and it is fairly easy for module developers to adapt their code to support it as well, though it is easier if they understand how the system works from the outset.
At the core of localization is the idea of externalizing string data. That is, any time you would have a string of text in English, instead of using the string directly, you store it externally, and reference it through the localization system. In Ignition, these strings are stored in key/value properties files, called resource bundles. Beyond externalizing strings, in presenting data it is important to take care that numbers and dates are converted to strings using a locale aware mechanism instead of direct toStrings.
Nearly all operations involving localized string data in Ignition go through the statically accessed BundleUtil class. Modules can register resource bundles through a number of convenient functions of this class, and can retrieve the value using the resource key (sometimes also referred to as the "bundle key").
For example, it is common that a gateway module will have one main resource bundle defining most of its strings. If the file were located directly next to the gateway hook class, and were named "module_gateway.properties", in the startup function of the module it could be registered as follows:
BundleUtil.get().addBundle("modgw", this.getClass(), "module_gateway")
This registers the bundle under the name "modgw". Anywhere in our gateway module that we wanted to display text defined in the file, for example a resource key "State.Running" that corresponds to "State is running", we could do:
BundleUtil.get().getString("modgw.State.Running");
There are a variety of overloads for loading bundles and getting strings. See the BundleUtil JavaDocs for more information.
Some systems support other localization mechanisms. For example, in developing gateway web pages, placing a properties file next to a Java class with the same name will be enough to register that bundle with the system. Mechanisms such as these will be described in the documentation as they come up.
Many parts of the system that appear to use strings actually require a resource key instead. That is, when implementing a function defined by a platform interface that returns a string, take special care to identify whether the value returned should be the actual value, or a resource key that can be used to retrieve the value. This should be noted in the documentation of the function, but most functions and arguments use naming conventions such as "getTextKey" and "function(descKey)" to indicate this.
Resources are loaded based on the user's current locale, falling back to the best possible alternative when the locale isn't present. The Java documentation for the ResourceBundle class explains the process. To provide translations for different locales, you can simply place the translated files (properly named with the locale id) next to the base bundles.
In general, the Ignition platform follows the recommendations of the Oracle Code Conventions for the Java Programming Language. Most of the interfaces and classes in the Ignition platform are simply named using the standard casing. Some items, however, have names that are products of their history, which may make them a bit confusing. The following list tries to identify inconsistent or legacy naming schemes that module writers are likely to encounter:
Module services are a way for modules to provide APIs to other modules, or to implement well-known services in new ways. Put differently, the ModuleSerivceManager (accessed through GatewayContext) is a directory of Objects, referenced by their class type. Any module can register a new ModuleService, and other modules can subscribe to these types of services, and retrieve the registered instance when its available. This allows child modules to get the running instance of a class provided by a parent module.
To create a module service, a class or interface must extend the empty ModuleService marker interface. No other definition is required. This class or interface, however, will need to be located in a project that can be referenced by child modules (such as an "api" or "common" project), as they will be referring to it explicitly to retrieve the implementation.
The cycle for a module service is:
For example, let's say that we want to create an "Advanced Calculation Engine", that lets sub-modules
register and use new types of calculations:
public interface AdvancedCalculationEngine extends ModuleService { List<String> getCalculationTypes(); CalculationResults performCalculation(String type, InputData data); void registerCalculationType() }
In the gateway hook of our module, which happens to implement our interface, we would register it with the context on setup, and notify the manager of the state on startup and shutdown:
public class AdvancedCalculationEngineGatewayHook extends AbstractGatewayModuleHook implements AdvancedCalculationEngine { protected GatewayContext context; public void setup(GatewayContext context) { //Store the context locally so we can get it when we need to. this.context = context; context.getModuleServicesManager().registerService(AdvancedCalculationEngine.class, this); } public void startup(LicenseState activationState) { context.getModuleServiceManager().notifyServiceReady(this); } public void shutdown() { //Stop the service and de-register it on shutdown context.getModuleServicesManager().notifyServiceShutdown(this); context.getModuleServicesManager().unregisterService(AlarmNotificationContext.class); } //Implementation of the service ... }
In our sub-module, which depends on the module that provides the service (so it knows about the Interface, in this example, the "AdvancedCalculationEngine"), we subscribe to the service by providing a ModuleServiceConsumer, and obtain the implementation when the consumer is notified that it is ready. To continue our example, the gateway hook of our CalculationEngineConsumer implements the ModuleServiceConsumer interface.
public class CalculationEngineConsumer extends AbstractGatewayModuleHook implements ModuleServiceConsumer { GatewayContext context; //When we get the engine from the service, we'll keep it here. AdvancedCalculationEngine engine; public void setup(GatewayContext context) { this.context = context; context.getModuleServicesManager().subscribe(AdvancedCalculationEngine.class, this); } public void serviceReady(Class<?> serviceClass) { if (serviceClass == AdvancedCalculationEngine.class) { engine = context.getModuleServicesManager().getService(AdvancedCalculationEngine.class); } } public void serviceShutdown(Class<?> serviceClass) { if (serviceClass == AlarmNotificationContext.class) { engine = null; } } }
Since it is possible to subscribe to multiple service classes, it is important to check that the service being notified on is the actual class that is desired. While this example doesn't go further, the idea is that the module could now do things like register new calculations, or perform calculations through the service.
The module service system can be used as necessary by modules, but it is also used for several important parts of Ignition. Currently, it is used to register new OPC-UA device drivers (the DriverManager service interface, defined in driver-api), and to access the AlarmNotificationContext, which is part of the Alarm Notification Module and its API.
1 Comment
Anonymous
Type at line 16 CalculationEngineConsumer.serviceShutdown():
Shouldn't it be as follows?
Best regards,
Almer Bolatov