Key Design Concepts
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.
Quick Introduction to Log4J
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.
Getting a Logger
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:
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 Severities and Messages
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 should contain helpful and identifiable data. For example, if you have a logger in a driver and write "Device connected", it will be of little use- multiple instances of a driver might be running at once, and the message does not indicate which instance it is referring to.
- Log messages set to Info and above should not "flood the log"- that is, report very frequently, making it impossible to see other logs, or filling up the allocated buffer quickly. There are several strategies for avoiding this, such as logging once, and then logging subsequent messages on Debug for a period of time, or only logging the first event, and then making the status easily visible in the gateway or designer.
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:
- Error - An error in the system, usually based on some sort of exception, that is unusual, and usually should be reviewed by somebody.
- Warning - Like error, an unusual event that should likely be reviewed. May indicate that something is not quite correct, but is not necessarily preventing correct operation.
- Info- Standard messages indicating a change of state, or anything else that might be beneficial for the user to know.
- Debug - Information used for troubleshooting, perhaps logged repeatedly or more frequently, provides technical information that might only make sense to a trained user or you, the developer.
- Trace - Very fine grained information that might be very verbose. Generally only logged for a short period of time in order to gather information for troubleshooting. This level is usually very technical, such as packet contents and result codes.
Storing Configuration Data
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.
The Internal Database
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.
The PersistentRecord ORM system
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.
The ProjectResource system
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).
Disk Based Storage
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.
Current Extension Point Types
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.
Programming for Redundancy
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.
BundleUtil - The heart of externalization in Ignition
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:
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:
There are a variety of overloads for loading bundles and getting strings. See the BundleUtil JavaDocs for more information.
Other Localization Mechanisms
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.
Localization and Platform Structures
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.
Translating Your Resources
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.
Style and Naming Conventions
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:
- factorysql or factorypmi packages, or the abbreviations "fsql" and "fpmi" in identifiers.
- These products were the predecessors to the SQL Bridge and Vision modules respectively, and these names still show up in code fairly regularly.
- "SR*" naming convention.
- During main development, Ignition was referred to as "ScadaRail". This led to many classes being named with the initial abbreviation SR, a practice that has been abandoned, but not completely reversed.
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.
Implementing and Registering a Module Service
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:
- Register the implementation
- Notify the system that the service is ready
- Modules use the service
- Notify the system that the service is not ready, on shutdown.
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:
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:
Consume the Module 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.
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.
Uses in Ignition
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.
- No labels