Click here to Skip to main content
15,867,330 members
Articles / Programming Languages / Visual Basic

Embedded Controls in HMIs

Rate me:
Please Sign up or sign in to vote.
5.00/5 (10 votes)
10 Nov 2017Public Domain35 min read 93.9K   33   23
Creating Embedded Controls for WonderWare InTouch and WinCC

Background

The company where I'm currently contracted sells reheat steel furnaces. The Level 2 system that's sold with the furnace includes a sophisticated mathematical model capable of calculating the internal heat and internal temperatures of the steel within the furnace. Although the PLC has a Level 1 HMI with all its screens, the Level 2 for this system typically has a separate PC dedicated to running just the Level 2 for calculating those temperatures and the most efficient use of fuel for achieving the desired output temperatures with minimal fuel and minimal scaling.

It's not unusual for customers to request the two HMIs to run on the same computer terminal, and we often create a button on the Level 1 HMI capable of launching the Level 2 HMI. In rare occasions, some parts of the Level 2 HMI (such as the simulated isotherm image) are requested to be embedded into the Level 1 screens; but recently, we've seen an increase in the number of customers that are insisting that the Level 2 HMI be thoroughly embedded within the Level 1 HMI. We've been creating custom one-of implementations within the customer's Level 1 SCADA system, borrowing or creating custom one-of controls for this purpose, but after finishing two of these fully mixed systems, it seemed like a good time to investigate a new design philosophy for our HMI in general.

This is an on-going effort for us. The first step is to examine what's possible. Since the two most common SCADA systems for which our customers request this type of implementation has been WinCC and WonderWare's InTouch, our research (and this article) will focus primarily on these two systems, but there are likely issues discussed here that will apply to designs of this type in any SCADA system. Likewise, any suggestions or feedback found here are likely to contribute to our final design.

Definitions

HMI
A Human Machine Interface or HMI is the graphical interface for a plant to control some industrial equipment. The rest of the world would call this a Graphical User Interface or Gooey (GUI), but Human Machine Interface just sounds more -- down-to-business.
PLC
A Programmable Logic Controller or PLC is a specialized, hardened computer that reads industrial sensors and controls industrial equipment.
Level 1, 2, and such
Automation Levels are based on the ANSI/ISA Batch Standards (S88). Level 0 generally refers to MITL (Man In The Loop) controls. This is where some guy hand-cranks valves and pushes a start button on a pump, watches the glass level fill up, then pushes the stop button. Level 1 is where that guy's supervisor gets tired of paying someone to stand around all day watching the level on a tank, and buys a PLC to monitor an electronic level gauge, and programs the PLC to automatically send start and stop signals to the pump. Level 2 is where that supervisor's manager realizes that the water in the tank costs more between 6 AM and 5 PM, and decides it makes sense to fill the tank from half to full after 5 PM, but only fill it to half from a quarter between 6 AM and 5 PM.
SCADA
A Supervisory Control and Data Acquisition system or SCADA system is a type of application development environment intended to make it easier for non-programmers to create an HMI.
WonderWare, InTouch
WonderWare's InTouch is a SCADA system. For more information on this system, check out Invensys' WonderWare InTouch Product Page.
WinCC
WinCC is a SCADA system. For more information on this system, check out Seimen's Simatic WinCC Product Page. Use caution here when looking for information on WinCC, because Seimens also has another, unrelated SCADA system with similar capabilities called WinCC Flexible. (If you Google information on WinCC, I suggest appending -Flex -Flexible filters at the end of your query.)
Setpoint
A setpoint is easiest to understand in terms of a home's thermostat. You don't typically turn your heater on and off; rather, you set the temperature to a specific setpoint, such as 72 degrees F, and the thermostat turns the heater on when it's too cold and turns it off when it's warm enough.
PID
A Proportional-Integral-Derivative controller or PID works similar to the cruise control in some modern automobiles. It is a sophisticated output calculation (how much gas to feed the engine) that is based on your setpoint (the current mph or kph you desire), the rate of acceleration or deceleration, and how long it's taking to speed up or slow down.
Tags
Tags are special variables that are common to PLCs and SCADAs. External tags are control points that correspond to some physical piece of equipment such as a pressure indicator or a valve's percentage open. Some, such as the pressure indicator, are read-only. Some are writable only under certain circumstances; the valve's percentage open, for example, is only writable when the valve is in manual. If the PLC is controlling the valve via a PID controller, then a setpoint is the writable value, but the percentage open is controlled by the PID calculated output. Internal, or memory tags mimic external tags, but are not tied to a specific physical piece of equipment. Rather, memory tags are much more like a variable for the SCADA system, with their values set by the SCADA systems programming logic.
Microsoft's Interop Forms Toolkit
"Microsoft's Interop Forms Toolkit is a free Visual Studio add-in that simplifies the process of displaying .NET forms and controls in a Visual Basic 6 application." [Microsoft, 2010] This tool is primarily designed to allow customers to extend the lifetime of existing Visual Basic 6 applications and allow them to incorporate new VB.NET controls and forms into existing applications of that type. However, it has been successfully applied to other applications which make use of the same technologies.
SOA
Service-Oriented Architecture or SOA is a design philosophy for providing data on one computer to be consumed by one or several computers for purposes that may not be known at the time the data service was created. The easiest way to understand SOA services is in terms of Web pages. The HTML language returned from a HTTP request was initially only used for displaying web pages, but increasingly is used to provide raw data that can be displayed in various ways by a wide array of devices. Pod Casts, RSS feeds and Geo-spatial data from services like Google Maps are examples of SOA Data Services.
VMS
Virtual Memory System or VMS was the Cadillac of computer operating systems. However, it consumed too much gas and was quickly displaced by Windows NT when computers were switched to all electric. For more information on VMS, check out the Hewlett-Packard, OpenVMS Product Page.
OPC
Object-linking and embedding (OLE) for Process Control or OPC was originally created as a Windows device driver for PLCs. The current name is a misnomer in that more modern OPC Standards Specifications do not rely on Object-Linking and Embedding for implementation. In fact, there is evidence of some movement within the current standards that offer to open the drivers to non-Windows operating systems. For more information on OPC, check out the OPC Foundation's web site.

Design Considerations

The first step was to layout what our ideal implementation would look like. The best solution for us was still the separate PC implementation for the HMI. SCADA systems, from our perspective, don't represent a long-term investment, but rather another method of hosting our HMI. In most cases, these systems represent a rather alien hosting environment. So alien, in fact, that we had been rewriting the majority of our HMI from the beginning, and we wanted to be able to reuse as much as possible to reduce cost and schedule. It benefits both our company and our customers if we can spend our time and money on innovation, rather than implementation.

Consumable Pieces

I suspect that even for veteran programmers, development begins by dragging controls onto a form, where those controls are double-clicked to add standard event logic. Custom Controls, if they're created at all, are only created during refactoring. The difficulty in translating the code behind a form to run inside of a SCADA has prompted us to reconsider our design philosophy. Shifting to custom controls would allow us to host the controls in any system that supports embedded controls. In such a scenario, the Windows Form would be just another hosting environment. I can't say this is a compelling design reason for developing embedded controls unless you intend to run the same code on many different hosts, which I suspect is not typical. The most likely justification I can imagine for the majority of readers is to supplement the native capabilities of an existing SCADA system. Whatever your reasons, I hope I can provide some insights here.

Data Access and Screen Updates

The next consideration was in how to perform screen updates, both timing and data, to our embedded controls. We were hoping to allow the hosting environment to orchestrate updates and intra-control communications: we were hoping to avoid adding timers to each embedded control and to avoid using thousands of internal tags to provide data to our controls.

Ideally, the data collection should be capable of running as a system service so that it could be launched automatically without adding complexity to the end-users. However, the data collection architecture should remain flexible. Since I've worked at this company, even our standard Windows Forms HMI has been adapted to use SQL Server, Oracle, OPC, and Sockets for communications between the Level 2 and the HMI. If possible, the design of our data collection system should enable us to swap out the mechanism of communication.

Hosting controls within a SCADA system poses a unique issue with sockets communications. It's easy to forget that once the HMI is distributed to each client, that each client will run it's own instance of that code. In other words, if you create a socket connection for your control, and that control is displayed by three clients, this will create three socket connections on the other end. Additionally, if the controls can not share data collection, then each control must support its own socket connection. In short, 10 controls on 3 clients means 30 socket connections.

This isn't a technical problem, but it is rather bothersome. Especially since we would certainly be sending the same information multiple times to different controls running on the same client. A much more efficient use of network and computing resources would be to have a data access layer (DAL) in the background managing all the data for all of the controls, and have the controls query the DAL for information needed.

Hosting Limitations

Finally, we need to code to the most restrictive limitations. We are already quite familiar with Windows Forms, but WinCC and WonderWare's capabilities are still being fleshed out. We still haven't determined all of those limitations yet, so I anticipate several future edits to this article. We did make some significant discoveries that will hopefully be of benefit.

Defining What's Possible

One of my favorite stories is how design of the Space Shuttle, one of the world's most sophisticated technological achievements, is based on the size of a horse's behind. Long story short, the rocket boosters have to travel by rail to their destination, and railroad tracks are spaced the same distance as the ancient Roman chariots - the width of two horses' behinds. The shuttle, then, has to account for the fact that it's booster rockets are a specific size.

Likewise, we had envisioned creating a large monolithic data object that would manage data, would be shared by all of our objects, and would be updated by a running service. Only you can't access data directly across application boundaries, especially a system service. So that was an immediate casualty of architecture. We needed to find out if any mechanism existed for collecting data that could be shared between multiple controls.

We knew from the vendor documentation and from other engineers that .NET controls were fully supported by WinCC and that with InterOp, they could be embedded into WonderWare. This left a lot of room for filling in details of exactly what could be supported. Since a common object could not be passed between separate applications, how would this work if passed between embedded controls?

WinCC's Support for .NET Controls

What we discovered was frustrating, but definitely worth sharing. From the vendor's documentation, we did not expect much difficulty from embedded controls in WinCC. We anticipated some possible issues sharing the Data Access Layer object between controls on the same picture or across pictures in a compound window. We even anticipated some interference from the InterOp commands and attributes required to support WonderWare. As it turns out, the vendor's claim that they support .NET controls is slightly overstated. So far, we have confirmed that public read-write properties are fully supported (read-only and write-only properties are not supported), but that public methods are not supported.

Furthermore, declaring a public event enables the programmer to see the event from within the WinCC development environment and even write scripting code. However, raising the event to WinCC will cause an OutOfMemory exception. (I presume this is because the WinCC hosting environment does not have access to the Garbage Collector, which allocates memory for .NET controls and applications. I'm still awaiting a response from Siemens on confirmation of this.)

We can make a control that contains dozens of other controls, and any of the contained controls can raise events that are handled by the container, but the container cannot raise an event nor allow an event to bubble up to WinCC. The implication is that any embedded controls in WinCC must be self-contained and that the WinCC hosting environment cannot control or effectively manage the embedded control as we hoped. For example, dropping in monolithic controls, such as an entire HMI would work. Separating the HMI into individual controls and hosting them within WinCC will be more difficult.

Service Pack 2 for WinCC v7 has been released for download and is supposed to address the lack of support for embedded .NET controls, but I have not had the chance to test this new release. When I get the chance to verify the changes, I'll update this section to reflect those results. In the mean time, if you know of errors in my comments above, please leave a comment below to share your own experiences.

WonderWare's Support for .NET Controls

Oddly, although WonderWare doesn't claim support for .NET controls within their product, we found the InterOp attributes annoying, but fully functional. My WonderWare programming partner in this has not yet attempted to run multiple controls from a common library, so I can't verify that sharing of a common Data Access Layer object still works as expected. So far, all properties (we have not tried read-only or write-only), all methods, and all events have performed as expected. (If you find any limitations, let me know and I'll edit this section for inclusion.)

Bringing it Together

We were surprised and disappointed with the limitations of WinCC, but we were able to think of some workarounds using only properties. Properties can receive parameters, and property getter and setter methods can be assigned to or read from to invoke a method. For events, it's possible to create a cyclic script in WinCC that can read public properties to trigger actions. It might even be possible to link an internal tag to a property and use that to trigger events. However, the code would be less responsive and difficult to understand.

In particular, we were envisioning using the events to communicate status changes from one control to other hosted controls. For example, selecting a piece of steel in a furnace overview control would raise an event with the piece ID. The PieceID property of the Isotherm Control would then be set by WinCC, and the Isotherm Control would graphically display the temperature profile for that piece. But waiting for the next cycle of a script would create an unacceptable lag across controls. As a result, the boundaries of our controls will need to be chosen with care and will likely result in larger boundaries than we wished.

To be honest, we had never considered monolithic controls and are intrigued to find our Italian partner company has successfully embedded the entire HMI as a control in previous jobs. Since we have not yet examined the HMI with a critical eye for segmenting it into stand-alone controls, we are likely to at least consider this option.

Frankly, I'm much more excited by the opportunity to refactor an entire application and bring some Object Oriented Architecture into a process engineering application, regardless of how the HMI is divided. We're going to look at some of that in the following sections as we explore the boundaries of WonderWare and WinCC embedded controls.

Simple Isotherm Control

InterOp Toolkit Differences

For those who are following along, if you're working with WonderWare, I suggest you download and install the Microsoft InterOp Forms Toolkit (see definition above for the link). If you intend to target WinCC exclusively, you may skip this step. This toolkit makes available the VB6 Interop UserControl and the VB6 InteropForm Library projects, and our walkthrough here will be done using the VB6 Interop UserControl. If you are not using the toolkit, the Windows Control Library would be the non-InterOp equivalent. When comparing your code to this example, be aware that any code displayed here will include additional attributes that are inserted automatically. Additionally, the toolkit inserts two helper classes and at least two helpful methods for registering and unregistering the control.

Finally, the toolkit also creates several helpful public events which, if you're running WinCC should probably be removed. Raising these events within your control will stop execution at that point, raising an OutOfMemory exception. Also, while these events remain public, they will be visible within WinCC's development environment, encouraging future programmers to write script for them.

Choice of Language

Note that embedded controls can be written in any language regardless of the support for that language within the hosting environment: the compiled control is embedded as an object, not as raw language constructs. I prefer the C# language over VB because I learned programming in C and because of all the languages I know, a large group of them -- Java, JavaScript, C#, C and C++ -- share a common structure which VB does not. However, although our C# programmers can all program VB, the reverse is not true. So we're using the VB language. I should also point out that the toolkit above is for VB only, which may influence what language you choose. (If you need assistance translating any portion of this article into C#, please speak up in the comments and I'll be happy to translate.)

Getting Started

We are going to create a simple control that we use to display our simulated Isotherm image. Because of its simplicity and lack of interaction, this is where we started in our efforts (that, and the fact that we were having trouble with this particular piece of code). However, because this control lacks any clicky-things, it does not lend itself to experimenting with events. As a result, we quickly switched to more contrived examples, but the isotherm image control provides a better working example for this discussion.

Create a New Project

To start out, create a new project; as mentioned above, I'm using the VB6 Interop UserControl, and naming it InteropIsothermControl. Expand everything and, if you're using the Interop, locate and comment out (or delete) the two events Click and DblClick.

VB.NET
#Region "VB6 Events"
         'This section shows some examples of exposing a UserControl's events to VB6.  
        'Typically, you just
        '1) Declare the event as you want it to be shown in VB6
        '2) Raise the event in the appropriate UserControl event.

        'Public Shadows Event Click() 'Event must be marked as Shadows since 
				 '.NET UserControls have the same name.
        'Public Event DblClick()

        'Private Sub InteropUserControl_Click(ByVal sender As Object, 
        'ByVal e As System.EventArgs) Handles MyBase.Click
        '    RaiseEvent Click()
        'End Sub

        'Private Sub InteropUserControl_DoubleClick(ByVal sender As Object, 
        'ByVal e As System.EventArgs) Handles Me.DoubleClick
        '    RaiseEvent DblClick()
        'End Sub

#End Region

Add the Namespace and Member Variables

Next, go to the top of the file and add a namespace declaration. (Not shown here, the End Namespace should be added at the bottom of the file.) At the top of the class, we need to declare our bitmap. We don't want to instantiate it yet, since we won't know what size the bitmap will be. We also want to be able to obtain the array of temperatures we'll use for our bitmap, and that needs to come from some data supplier. To make it easier to program this data supplier with various implementations, we're making use of an Interface Object. Right now, though, we need only declare the Interface. We'll use reflection on the array returned by our data source to provide the dimensions of our bitmap, but that gets done later, when we're able to select which bitmap we're drawing (that is, when we can provide a PieceID string).

VB.NET
Namespace MyCorp

    <ComClass(InteropIsothermControl.ClassId, _
	InteropIsothermControl.InterfaceId, InteropIsothermControl.EventsId)> _
    Public Class InteropIsothermControl
        Public Interface IGetIsothermTemperatures
            Function GetIsothermTemperatures(ByVal PieceID As String) As Double(,)
        End Interface

        Private myBitmap As System.Drawing.Bitmap = Nothing

The minute you move the class definition into a namespace, it will lose access to all the Designer-Generated member properties and methods in the Partial Class definition. In your solution explorer, click the button that shows all files. Then open the InteropIsothermControl.Designer.vb file. Just above the Partial Class declaration, add the namespace declaration with exactly the same name as before. (Remember again to end the namespace at the bottom of the file.)

Interface Pattern

This code pattern is called, oddly enough, an Interface Pattern. The idea is to decouple the class that displays the isotherm from the method used to collect the data. The Interface keyword is used in VB.NET to bypass the limitation of single inheritance, but I have no need to inherit from this interface. In fact, I don't even want to implement the interface in this class, I merely want this class to have an object that does inherit from this interface. However, if you implement this here using an abstract class, you will run into problems when you decide to expand from one control to a library of controls.

Implementing this using the Interface keyword, we can define a separate Interface object type for each control. Remember that we're hoping to be able to implement a Data Access Layer object that can take care of supplying all of our data collection needs; one that can be shared among all of our controls. If we use an Interface, we can have one class implement as many interfaces as we like, but it can only inherit from one parent. Understanding this, we need to include a member variable of this Interface type (to be initialized inside of our constructor).

VB.NET
Public Class InteropIsothermControl
    Public Interface IGetIsothermTemperatures
        Function GetIsothermTemperatures(ByVal PieceID As String) As Double(,)
    End Interface

    Private myBitmap As System.Drawing.Bitmap = Nothing
    Private IsothermDataProvider As IGetIsothermTemperatures = Nothing

Implementing this using the Interface keyword is similar to how Windows Communication Foundation (WCF) implements SOA. WCF is not available in C++, and as long as our company continues to support VMS clients--. Well, that option won't be available to us for quite some time. However, WCF is not the only way to implement SOA services. In fact, WCF service providers can be implemented manually in C++. The true beauty of the Interface Pattern, though, is not needing to know any details about the data service provider other than the fact that it provides a function called GetIsothermTemperatures. I pass in a string containing the PieceID, and I receive a two-dimensional array of temperatures from which I can build my isotherm bitmap.

The next task is to add our constructor code to initialize our member variables. We don't have a piece ID to pass into the interface yet, so we still don't know the size of our bitmap. Instead, we'll instantiate the bitmap inside of our paint method. This is a small bitmap, so allocating the bitmap during the paint event handler won't be a problem. Since the bitmap won't change sizes between pieces, I could hard-code the size in the declaration and then color it during the paint method, but then we'd have to edit it between jobs when it does change. Since I've never seen this bitmap image grow larger than a 7x11 image, I'm making the conscious decision to take the hit. If you're intent on using this code for your own purposes, you will need to make your own justifications and change the code appropriately.

Inside the Constructor: The Factory Method

If you're using the Interop Toolkit, you already have a constructor hidden in the VB6 Methods region. It's added there so that you can call OnCreateControl to raise the Load Event for VB6-type hosts. (To my knowledge, WonderWare does not require this event. It does not cause any problems with WinCC, however, so I left it.) If you're not using the toolkit, you'll need to create the constructor. In VB.NET, this is the public subroutine called New. Intellisense should jump in when you create one, but make sure the constructor contains the InitializeComponents call. If Visual Studio doesn't add this function call to a class that inherits from a control, it's a good indicator that you already have a default constructor defined somewhere else.

In any case, we need to assign the actual member variable. The key to understanding the Interface Pattern is recognizing that the member variable IsothermDataProvider is of type IGetIsothermTemperatures, but is being assigned to a type of DataAccessLayer object. We can do this because the Implements keyword in VB.NET is a special type of inheritance. In other words, the DataAccessLayer object is a child implementation of IGetIsothermTemperatures. So long as I use only the parent declaration within my Isotherm Control, I can replace the entire DataAccessLayer.vb file with any other implementation and the Isotherm Control will work without any changes. This will allow us to create a SqlDataAccessLayer object, an OracleDataAccessLayer object, a TcpDataAccessLayer object, and in 2025 when the RFC is scheduled for approval, we can even write an AITelepathicProtocolDataAccessLayer object. A quick recompile with our new module, and our Isotherm Control will work without any code changes.

Sorry, I must have been dreaming. Now that the crust is wiped away from my eyes, I see an immediate problem here. I'm about to assign my IGetIsothermTemperatures member variable, the IsothermDataProvider, to a DataAccessLayer object. We need a way here to assign that member variable to a child implementation without referencing that child implementation. This neat little trick is accomplished through the Factory Pattern (or more accurately, the Factory Method). This can be done with any public function that returns a type of IGetIsothermTemperatures object.

The Factory Pattern is intended to be used to obtain one of several sibling objects where the specific child returned is based on run-time conditions. When implementing the Data Access Layer, however, it's always a design-time decision with a static assignment. If you prefer to assign this directly within the constructor, it will not result in the immediate and spontaneous combustion of your keyboard. However, it would be nice if we could somehow invert the assignment so that it took place in the same file we know we intend to replace anyway, and save us the hassle of editing the files that contain the controls, and it turns out that we can declare a Module directly in the DataAccessLayer.vb class file.

We're going to call this function GetIsothermDataProvider and it will return an IGetIsothermTemperatures object, but the function won't be defined until we create the file that implements a concrete IGetIsothermTemperatures Interface class. At the bottom of that file, we'll add this function definition in a new FactoryProvider module. (Get used to the little squiggly line you'll see here. It's gonna be a while, but we will fix it.) For now, our constructor looks something like this.

VB.NET
Public Sub New()

    ' This call is required by the Windows Form Designer.
    InitializeComponent()

    ' Add any initialization after the InitializeComponent() call.
    IsothermDataProvider = GetIsothermDataProvider()

    'Raise Load event
    Me.OnCreateControl()
End Sub

Painting the Isotherm

With the architectural details out of the way, we can get down to the actual drawing of the bitmap. Select "(<Control Name> Events)" in the left drop-down list, then browse the right drop-down list until you find Paint. This should fill in a skeleton OnPaint Event Handler. First we need to fetch the array that contains our data. Dimension a variable here of a two-dimensional array of doubles, and assign it to the value returned from our data supplier. Since I know that the bitmap won't change between pieces, I'm going to create an if-statement here to see if my member bitmap variable is Nothing, which will be true only on the first pass. Once we know the size of both dimensions of our two-dimensional array of temperatures, we can instantiate a bitmap object with those dimensions.

Our isotherm image is not intended to display heat, but rather to accentuate cold spots in the heating profile of a piece of steel in the furnace. We have seven different color bins which we divide evenly between the highest and lowest temperatures in the array. All temperatures are then calculated as a linear interpolation between the two nearest colors. There's nothing in this algorithm of scientific or educational value, so I'm not going to bother to show the algorithm. For our purposes, we can significantly reduce the code by simply dividing the temperatures into fixed color bins and assign the temperatures a color from a Select-Case statement. For the curious reader, an algorithm based on Black-Body radiation might make a more interesting algorithm, both scientifically and programmatically, and you can start your research with Wikipedia's article on Color temperature. I've used a select statement here that roughly approximates black-body radiation scaled down by a factor of six.

As we convert our temperatures to colors, we're going to color our bitmap pixel-by-pixel. The SetPixel method is a slow method because it must first lock the bitmap array of pixels. For a bitmap of our size, this is insignificant, but if you're creating a larger bitmap, you'll want to freeze the bitmap's array first, shade all your pixels, then unfreeze the bitmap. You can see where I tried this method in my code, but when I host this control, it crashes there claiming that the bitmap is already frozen, so I simply removed these statements. If you have a larger image to build, I suggest you research this further. If you know what I'm doing wrong, please share so I can pass it along.

Finally, since a 7x11 (or smaller) graphic isn't of much value, we'll make use of GDI+ to perform interpolation and anti-aliasing when expanding the image to fill our entire control area. The interpolation of pixels is important when shrinking an image because you want to find a color which is a weighted representation of all the colors being squashed into a single pixel. When you're stretching an image, it's critical. When the image stretches, you have to make a best guess about what color the new in-between pixel would be. GDI+ offers several options here, and which option is best depends on your particular circumstances. Once you have the image displaying, it's easy to experiment with all the GDI+ interpolation methods. Unfortunately, some of the most photo-friendly aren't available as in-built options, but there are many articles right here on CodeProject that detail more advanced methods for photographic images. In our case, high-quality bi-linear interpolation provides the best results and it's scientifically justifiable. Perhaps it's because I'm using ARGB and don't know how to turn this off, but my image is being alpha-blended with the background image. Choose your background with this in mind.

If you've ever worked with GDI+ routines to perform bitmap interpolation, you're probably aware of the way it offsets images by a half-pixel. This becomes most pronounced when you use nearest neighbor interpolation (which copies the nearest pixel). This offset is most pronounced when you are working with extremely small bitmap areas. To correct this behaviour, you need to offset your image by a half-pixel. Luckily, there's a property to do this for you. The reason for this offset could fill an article of its own, and I don't fully understand it myself. I do know that this property that shifts it back for extreme zooming is a well-hidden jewel.

VB.NET
	'Please enter any new code here, below the Interop code
        Private Function BlackBodyRadiance(ByVal Temperature As Double) As Color
            Select Case Temperature
                Case Is < 200
                    BlackBodyRadiance = Color.DarkGray
                Case Is < 525
                    BlackBodyRadiance = Color.OrangeRed
                Case Is < 1123
                    BlackBodyRadiance = Color.DarkOrange
                Case Is < 1425
                    BlackBodyRadiance = Color.Orange
                Case Is < 1725
                    BlackBodyRadiance = Color.Wheat
                Case Is < 2025
                    BlackBodyRadiance = Color.PapayaWhip
                Case Is < 2325
                    BlackBodyRadiance = Color.AliceBlue
                Case Is < 2625
                    BlackBodyRadiance = Color.Lavender
                Case Else
                    BlackBodyRadiance = Color.LightSteelBlue
            End Select
        End Function

        Private Sub InteropIsothermControl_Paint(ByVal sender As Object, _
		ByVal e As System.Windows.Forms.PaintEventArgs) Handles Me.Paint
            'Retrieve our array of temperatures from our data access layer.
            Dim IsothermColors As Double(,) = _
		IsothermDataProvider.GetIsothermTemperatures("TestPiece")

            'Instantiate a bitmap, but only if it's not already instantiated.
            If myBitmap Is Nothing Then
                myBitmap = New Bitmap(IsothermColors.GetUpperBound(0) + 1, _
		IsothermColors.GetUpperBound(1) + 1)
            End If

            Debug.Assert(myBitmap.Width = IsothermColors.GetUpperBound(0) + 1, _
		"You're isotherm bitmap changed sizes.")
            Debug.Assert(myBitmap.Height = IsothermColors.GetUpperBound(1) + 1, _
		"You're isotherm bitmap changed sizes.")

            'Pin our bitmap in memory to increase speed.
            'Dim myBitmapData As System.Drawing.Imaging.BitmapData = _
		myBitmap.LockBits(New Rectangle(New Point(0, 0), myBitmap.Size), _
		Imaging.ImageLockMode.WriteOnly, myBitmap.PixelFormat)
            For x As Integer = 0 To myBitmap.Width - 1
                For y As Integer = 0 To myBitmap.Height - 1
                    myBitmap.SetPixel(x, y, BlackBodyRadiance(IsothermColors(x, y)))
                Next
            Next
            'Remember to unpin the bitmap array so the GC can do it's magic.
            'myBitmap.UnlockBits(myBitmapData)

            'You should play with the interpolation mode, smoothing mode and pixel 
            'offset just for fun.
            e.Graphics.InterpolationMode = _
		System.Drawing.Drawing2D.InterpolationMode.HighQualityBilinear
            e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias
            e.Graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.Half
            'Allow GDI+ to scale your image for you.
            e.Graphics.DrawImage_
		(myBitmap, 0, 0, Me.ClientSize.Width, Me.ClientSize.Height)
        End Sub
    End Class

End Namespace

As you can see in the code, we're passing in a static string for the Piece ID. Typically, this would come from a private member variable that's set via a public property. The public property would assign the private variable and call a refresh. I've taken liberties here to simplify the design so we can focus on the topic of this discussion.

The Data Access Layer

Now that we have the control finished, we need to create a new class to provide data to our controls. From your solution explorer, right-click the solution and Add New Item. Choose Class and change the class name to something meaningful, like DataAccessLayer. At the top of the new file, add your namespace. At the bottom of the file, end that namespace. Below the class declaration, add Implements InteropIsothermControl.IGetIsothermTemperatures. Intellisense will insert a skeleton function here for the GetIsothermTemperatures() function. In our case, we're ignoring the PieceID string, and we'll return a static, 3x3 array. We can rewrite this piece later to use sockets, OPC, SQL queries or some other method to acquire this data from the Level 2 furnace model that continuously simulates the specific piece as it travels through the furnace.

I should point out here that this should work even if the DataAccessLayer class is not in the same namespace. Regardless of the namespace, we also want to implement the GetDataAccessLayerObject() function in this file. After your End Class, declare the Module FactoryMethod. As soon as you implement the interface in the DataAccessLayer class, that class will be recognized as a type of IGetIsothermTemperatures. In your function, you'll declare a new one and pass it back.

VB.NET
Namespace MyCorp
    Public Class DataAccessLayer
        Implements InteropIsothermControl.IGetIsothermTemperatures

        Public Function GetIsothermTemperatures(ByVal PieceID As String) As Double(,) _
	Implements MyCorp.InteropIsothermControl.IGetIsothermTemperatures._
	GetIsothermTemperatures
            Dim TemperatureArray As Double(,) = {{159, 240, 136}, _
		{240, 1150, 229}, {133, 239, 160}}
            Return TemperatureArray
        End Function
    End Class

    Module FactoryMethod
        Public Function GetIsothermDataProvider() As _
		InteropIsothermControl.IGetIsothermTemperatures
            Return New DataAccessLayer
        End Function
    End Module
End Namespace

Singleton Pattern

Because there is only one control in this library and because the data is not being collected through a limited resource (such as sockets), this should actually work as is. However, if you add another control to this library and try to share data between the two controls, you'll quickly realize that each new control that gets a DataAccessLayer object (of whatever type), they each have their own copy. To prevent this, we'll need to use the Singleton Pattern.

Who remembers DefInstance? Ok, quiet down! I know, I know, believe me I know. I'm embarrassed to admit it, but we still have them in our code. Our office uses a rather brutal mechanism for Configuration Management -- we ship the entire development environment, Visual Studio and source code, with each job. As a result, we aren't so quick to buy Visual Studio for the developers. If someone gets called to the field (or more often, remotes into the site), everything they need is already there, pre-installed. Brutal, but effective, and I can't say it isn't cheaper, especially since the development tools are all paid for by the customer. (In my previous job, we would have loved these guys! We could expense a complete set of development tools under a hardware purchase!)

The thing is, we went through a serious boom in the steel industry at about the same time that .NET was rolling out, and there just wasn't time for a thoughtful re-interpretation of the code. We discovered then what many folks are painfully learning with each new release of Visual Studio -- backward incompatibilities suck if you can't buy old copies of the development environment. Microsoft's policy is to pull surplus product from the shelves once a new version ships. We were forced to upgrade code from VB6 to whatever the latest version of VB.NET was available, and we had to complete the entire conversion during a normal job cycle. Liking it was optional. As a result, we still have DefInstances in our code from the automated VB.NET upgrade tool.

Amusing though this may be, the reason I bring up the DefInstance is because it provides a good example of a Singleton Pattern. No matter how many times you request a copy of the DefInstance, it gives you a reference to the one that's already instantiated. The only time you get a new one is if there isn't one. To make this magic work, we need a shared private member variable, we'll call it m_Singleton, and a shared, read-only property which we'll call -- sorry but I can't resist -- DefInstance. These are added to the DataAccessLayer class. In the GetDataAccessLayerObject() function, instead of returning a new object, we'll return the DefInstance of the DataAccessLayer class. The re-write looks like this.

VB.NET
Namespace MyCorp
    Public Class DataAccessLayer
        Implements InteropIsothermControl.IGetIsothermTemperatures

        Private Shared m_Singleton As DataAccessLayer = Nothing

        Public Shared ReadOnly Property DefInstance()
            Get
                If m_Singleton Is Nothing Then
                    m_Singleton = New DataAccessLayer
                End If
                Return m_Singleton
            End Get
        End Property

        Public Function GetIsothermTemperatures(ByVal PieceID As String) As Double(,) _
	Implements MyCorp.InteropIsothermControl.IGetIsothermTemperatures._
		GetIsothermTemperatures
            Dim TemperatureArray As Double(,) = {{159, 240, 136}, _
		{240, 1150, 229}, {133, 239, 160}}
            Return TemperatureArray
        End Function
    End Class

    Module FactoryProvider
        Public Function GetIsothermDataProvider() _
	As InteropIsothermControl.IGetIsothermTemperatures
            Return DataAccessLayer.DefInstance
        End Function
    End Module
End Namespace

Now, a purist would argue that this implementation is incomplete because I haven't made the default (no arguments) constructor private. As a result, the next schmuck that comes along can instantiate a dozen more of these and really mess up our code. Because that's a real possibility, you should add two things to this code: a private default constructor, and a comment there explaining why removing it or changing it to public is a really, really bad idea. Note that intellisense will not add the call to InitializeComponents because this class does not inherit from control or component.

Wrapping Up

Host in Visual Studio First

The first thing you should do is add this control to a standard Windows form. If anything's wrong, like the LockBits for me, you'll receive more debugging information from Visual Studio than you will from either WinCC or WonderWare. To do this, create a new Windows Application. With the graphical designer open, right-click on the Toolbox and select Choose Items. Browse to the location of the DLL you get when you built the control. A new control will be added to your Toolbox, and you can drop one onto a form. If you need to step through the control code, you can open the control in one instance of Visual Studio and attach to the devenv.exe of the Visual Studio where you're hosting the control. Hopefully, you'll never need to do that, but if the control dies just from dropping it onto a form, it's a neat trick to know.

I hope I'll get the chance to write at least one follow-up article on this topic. With only one control in the library, you can't verify that the data access layer class does enable shared data between controls. Before we could finish, my partner was called out to the field. As a result, this method has only been tested in WinCC, where it does work as expected. (This is why this article is lacking more information on WonderWare.) Hopefully, I'll not only have some more detailed information on this, but will be able to add at least one additional control to allow the reader to follow along.

Hosting in WinCC

Server

Once you've got it working in Visual Studio, you should compile the release version before inserting this control into a WinCC picture. When you do insert this into a WinCC picture, you should use Smart Objects' .NET Control, rather than trying to add the control library to the list of .NET Controls. When you add a control library, you'll only get the first control in the library. If you browse using the Smart Object, the last step will show you the list of all the controls in the library, and you can choose.

Clients

I do not have access to a computer running the Client WinCC software at the moment, but the document Working with WinCC does include information on copying custom embedded controls to a client computer. I believe the primary issue is ensuring that the location of the file containing the compiled controls is the same on all computers.

Hosting in WonderWare's InTouch

I do know that the control produced from this effort can be hosted in WonderWare's InTouch. However, I'm not sure whether this control must be registered via regsvr32, regasm, adding this control to the Global Assembly Cache (GAC) or anything else. If you have information on what's involved in hosting this on either the server or the client for WonderWare's InTouch, please contact me with details so I can add that information here. In particular, make sure you include specific instructions that I can post here, and specify whether the installation pertains to the Server or the Client installation of custom controls.

Avoid Remote Desktop

I will leave you with one last odd experience that I've seen more people complaining about -- GDI+ is aware of when it's running in remote mode. As a result, in complete ignorance of user preferences, Microsoft ostensibly for improved performance, took it upon themselves to disable all blending modes other than nearest neighbor. Apparently the networking guys at Microsoft aren't even able to make fiber optics networking fast enough to render a blended 3x3 bitmap over a remote desktop connection. If it's critical, use VNC. (We considered building our bitmap rotated, then rotating it into the correct position, but were happy to discover that VNC actually obeys user preferences in this regard.)

Good luck!

You can read the continuation of this article here.

History

  • Updated link for Working with WinCC. 10 November 2017
  • Correct "VPN" remote desktop to "VNC". 14 February 2011 
  • Removed corporate references. 8 October 2010 
  • Changed callouts to use new sidebars. 1 October 2010
  • Service Pack 2 of WinCC v7 was released, addressing shortcomings of .NET support. 22 September 2010
  • Added visual interest through call-outs. Corrected several typos. 14 September 2010
  • Resolved Factory Method call. Corrected grammatical errors. Improved clarity in several paragraphs. 11 September 2010
  • First released 9 September 2010

License

This article, along with any associated source code and files, is licensed under A Public Domain dedication


Written By
Software Developer (Senior) TenovaCore
United States United States
Rod currently works in The Burgh using .Net technologies to create Human-Machine Interfaces for steel-making furnaces. He has 7 years in the steel industry, and 12 years of experience working in the nuclear industry at Savannah River Site in Aiken, SC. He enjoys riding his Honda Goldwing through the winding Pennsylvania farmlands, and taking his Jeep offroad.

Comments and Discussions

 
GeneralException Pin
Laubschi27-Oct-10 1:45
Laubschi27-Oct-10 1:45 
GeneralRe: Exception Pin
knockNrod27-Oct-10 3:02
knockNrod27-Oct-10 3:02 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.