CSWorks: web-based industrial automation

Of CSWorks and software development

BACnet IP performance - Lab 05

clock July 19, 2011 22:31 by author Sergey Sorokin

If you are curious how well our new BACnet IP implementation can perform, you may find this post interesting. I used  CSWorks 2.0.4115.0 LiveData Service installed on a Core2 Quad Q6600 @2.40 GHz machine with 4GB RAM running 64-bit Windows 7. As a testing client, I used four instances of a simple LiveData client application that subscribes to updates from 5000 BACnet analog inputs, requesting for updates every second. Test applications talks to CSWorks LiveData Service directly over WCF (exctly the same way WCF LiveData Agent demo does). All hardware is connected via 100mb Ethernet.

My LiveData Service config file referenced four BACnet IP datasources, from "BacnetIpDemo01" to "BacnetIpDemo04" with ids from 260001 to 260004, you can see correspondent fragment of the config file on the screenshot below.
Test application expected every analog input data item to change on every update request. If a data item is not changed, the test application increases item's "skip" count. In the ideal world, skip count for all data items would be zero, since every BACnet device changes every item once a second. But due to the discretization process that occurs twice (once between LiveData Service and the BACnet device, another time between the client application and the server) it is hard to avoid these misses.

Performance monitors shows an average rate of ~18000 updates per second. If there was no device-to-server discretization and UDP packet loss, the rate would be exactly 20000 updates per second. LiveData Service was consuming 5-7% CPU.

Click to enlarge

CSWorks 2.0 talks BACnet

clock May 31, 2011 22:02 by author Sergey Sorokin

Starting from version 2.0, CSWorks installation package includes BACnet IP data source provider. That means: now you can build web-based solutions that can communicate with BACnet devices - controllers, modules, thermostats - any kind of hardware that supports BACnet. Here is a quick demonstration.


We will need:
- room thermostat that supports BACnet MSTP;
- BACnet IP to MSTP adapter;
- local network;
- desktop computer (server);
- notebook computer (client).

We have to use BACnet IP to MSTP adapter, because CSWorks supports only IP version of the BACnet protocol, and our thermostat provides only BACnet MSTP (good old RS-485) connectivity. Make sure that:
- both computers and BACnet adapter are connected to your local network;
- thermostat is connected to the adapter via RS-485.

BACnet thermostat in the local network diagram


The following software will be used:
- CSWorks 2.0 on the server; full CSWorks version is required, CSWorks Light will not let clients from other computers access the application;
- Microsoft VisualStudio 2010 on the server;
- web browser on the notebook.

Build demo application

On the server computer, open VisualStudio project at C:\Program Files\CSWorks\Demo\Src\BacnetThermostatIntegrationDemo\ and have a look at MainPage.xaml. BACnet connection points are defined in the very beginning:

<!-- AnalogValue(0): mode (Heat, Cool, Idle, Afterhours, Unoccupied Idle, Unoccupied Heat, Unoccupied Cool) -->
<data:DoubleDataItem x:Key="mode" Id="..." DataSource="RoomThermostat01" TemplateName="analogValue" Parameters="tagIndex=0;"/>
<!-- AnalogValue(6): heating setpoint -->
<data:DoubleDataItem x:Key="heatingSP" Id="..." DataSource="RoomThermostat01" TemplateName="analogValue" Parameters="tagIndex=6;"/>
<!-- AnalogValue(7): cooling setpoint -->
<data:DoubleDataItem x:Key="coolingSP" Id="..." DataSource="RoomThermostat01" TemplateName="analogValue" Parameters="tagIndex=7;"/>
<!-- AnalogValue(20): room temperature -->
<data:DoubleDataItem x:Key="roomTemp" Id="..." DataSource="RoomThermostat01" TemplateName="analogValue" Parameters="tagIndex=20;"/>
<!-- AnalogValue(87): fan speed (off, 1, 2, 3, auto, on) -->
<data:DoubleDataItem x:Key="fanSpeed" Id="..." DataSource="RoomThermostat01" TemplateName="analogValue" Parameters="tagIndex=87;"/>

and used later in the UI definition part:

<TextBlock Text="{Binding Value, Source={StaticResource roomTemp}, ..." .../>
<TextBlock Text="{Binding Value, Source={StaticResource mode}, ..." .../>
<TextBlock Text="{Binding Value, Source={StaticResource heatingSP}, ..." .../>
<TextBlock Text="{Binding Value, Source={StaticResource coolingSP}, ..." .../>
<TextBlock Text="{Binding Value, Source={StaticResource fanSpeed}, ..." .../>

Please note that these BACnet connection points are specific to the particular thermostat used in this demo, your connections points will be different - see BACnet integration guide for your thermostat. Build Release|x86 (for 32-bit installations) or Release|x64 (for 64-bit installations) configuration. Build script will place the compiled application file (CSWorks.Client.BacnetThermostatIntegrationDemo.xap) and hosting HTML file (BacnetThermostatIntegrationDemo.html) to the correspondent locations under C:\Program Files\CSWorks\Demo\Web\. Your client demo application is ready.

Add LiveData source

CSWorks LiveData Service must be aware of the new data source - BACnet device. Our client application will connect to a data source called "RoomThermostat01" (see MainPage.xaml above), so we have to add a new data source with this name. Add the following piece to the LiveData Service configuration file (see C:\Program Files\CSWorks\Framework\Server\CSWorks.Server.LiveDataService.exe.config):

  <bacnetIpLiveDataSources type="CSWorks.Server.DataSource.Bacnet.BacnetIpLiveDataSource, CSWorks.Server.BacnetIpProvider">
    <bacnetIpLiveDataSource name="RoomThermostat01" sampleBufferLength="16" deviceId="99102" ipPort="47808" updateRate="250">
        <template name="analogValue" type="Single" readPath="AnalogValue(($tagIndex)).PresentValue" canWrite="true" />

This demo works only with AV (analog value) BACnet connection points, so there is only one template is defined for this data source. Please make sure that deviceId attribute corresponds to the device identifier of the thermostat, otherwise CSWorks will not be able to locate it in the BACnet network. When done, restart CSWorks LiveData Service.

Configure LiveData web service

LiveData web service must be aware of the "RoomThermostat01" data source as well. Add an antry for "RoomThermostat01" to the LiveData Web Service configuration file (C:\Program Files\CSWorks\Demo\Web\LiveDataWebService\web.config):

    <liveDataPartition name="partition1" primaryLiveDataServer="liveDataServer_1_primary" secondaryLiveDataServer="">
        <dataSource name="RoomThermostat01"/>

Run the application

On the client notebook, open a browser and navigate to the CSWorks application page:


After a few seconds of initialization, you will see the demo reading values from the thermostat (click on the image to enlarge it):

BACnet integration with thermostat

Congratulations! Your thermostat is web-enabled now.

CSWorks 2.0.4115.0 released

clock May 31, 2011 01:00 by author Sergey Sorokin

What's new:

  • SQL LiveData provider: improved boolean data type support
  • Server: analogDeadband property for LiveData sources
  • Server: revised thread concurrency model in LiveData and Alarm servers (better data source error handling performance)
  • OPC LiveData provider: OPC group size limit
  • Server: BACnet IP support
  • Client: LiveData Manager - improved connectivity error handling
  • Client: BACnet Provider demo
  • Client: BACnet thermostat integration demo


CSWorks 1.7.5000.0 released

clock March 14, 2011 17:56 by author Sergey Sorokin

What's new:

  • HVAC symbol library


A minor release. CSWorks installation package now includes a symbol library for building automation industry. Click on the image below to see our online demo.

HVAC interactive demo: AHU


CSWorks 1.7.4050.0 released

clock February 11, 2011 14:28 by author Sergey Sorokin

What's new:

  • Windows Phone 7 client

The following video shows CSWorks Windows Phone client in action:

Building Silverlight applications: remove unused cultures

clock December 7, 2010 13:46 by author Sergey Sorokin

When building Silverlight projects, msbuild puts a lot of internationalized resource content to the intermediate folder (obj) and output folder (ClientBin or bin): look at all those ar, bg, ca folders. In 99% of cases, these files are just waste of build time and disk space. In 1% of cases - when you are building an application that actually supports all those cultures - those files will end up in your XAPs and your app will use them.

It's always a good idea to have a good internationalization plan and explicitly support only a number of cultures. An additional reason to do that is saving build time and disk space by eliminated unnecessary culture files. You can remove unused cultures from "Program Files\Microsoft SDKs\Silverlight\v4.0\Libraries\Client\" (or better move them to "UnusedCultures" folder). This will save you a lot of space on the build machine and improve your building time.

Three things to keep in mind:

1. Do not forget to put correspondent cultures back when you add a new supported culture to your project.

2. Do not forget to re-arrange your culture libraries after every Silverlight SDK refresh.

3. I doubt this approach is recommended by Microsoft, so use it at your own risk :)



CSWorks 1.4.4000.0 released

clock November 18, 2010 11:57 by author Sergey Sorokin

What's new:

  • 2-way remote alarm notification: email and SMS

The following video shows CSWorks SMS and email alarm notification in action:

  • a user triggers an alarm by closing all intake valves;
  • CSWorks Alarm Notification Service detects the active alarm and sends notification email (using SMTP server) and notification SMS (using GSM modem);
  • pipeline operator gets notificatons by email and SMS;
  • the operator sends acknowledgement SMS to the alarm notification server;
  • the alarm gets acknowledged;
  • the alarm goes inactive.


About resource hierarchy in web.config files

clock November 12, 2010 19:55 by author Sergey Sorokin

CSWorks stores resource hierarchy in web.config files, so:

  • LiveData Web Service can find LiveData Service instance that collects data from some specific data source;
  • Alarm Web Service can find Alarm Service that handles some specific alarm group;
  • History Reader Web Service can find database that holds historical data for a specific data point.

Let’s take History Reader Web Service for example. The following piece of web.config tells web service that data history for data point “7cc32718-0da0-42b3-ae55-0acbb3593671" (also known as "Tank1") is stored in the database “partition1”:


    <historyPartition name="partition1" primaryDbTarget="…" secondaryDbTarget="…">


        <area id="…" name="By Equipment/Location">


            <area id="…" name="Tanks">


                <area id="…" name="West Side">


                    <dataPoint id="{7cc32718-0da0-42b3-ae55-0acbb3593671}" description="Tank1"/>

One potential problem is that some solutions can record thousands of historical data points, and all of them must be referenced in a single web.config file, and by default, ASP.NET limits the size of web.config file by 256kb. Consider the following setup.

Run a script that generates 10,000 historical datapoint references (you can download it here) and insert them into <historyPartition name="partition1"…> node of the web.config of History Reader Web Service. Web.config now has size around 1mb (see screenshot below). Run any client application that uses this web service (CSWorks TrendDemo for example) and get a web service error that doesn’t leave any trace: no exceptions in ASP.Net worker process, no error in the Event Log.

Now tell ASP.NET that it should accept bigger web.config files. Create DWORD registry value HKLM\Software\Microsoft\InetStp\Configuration\MaxWebConfigFileSizeInKB and set its value to, say 1024kb (see screenshot below).

Now restart the client application and start accessing those 10,000 historical data points. The screenshot below shows:

  • registry editor with MaxWebConfigFileSizeInKB value;
  • web.config file size;
  • a fragment of web.config that references historical data points that belong to area “#0199”;
  • a fragment of CSWorks Trend Control that allows to choose any historical data point from area “#0199” and add it to the chart.

Click to enlarge

There are few other potential problems with large amount of data in web.config files. First, Silverlight Tree Control has problems with displaying more than a few thousand nodes simultaneously. Second, CSWorks History Reader Web Service handles data point hierarchy more efficiently if every area doesn’t contain more than a few hundred data points.

The recommendation is obvious: when planning historical data point areas hierarchy, do not put more than a few hundred data points in a single area, avoid “plain” hierarchies with thousands of data points in the same area. The script used in this demo (you can download it here) generates 200 areas each containing 50 historical data points.

This discussion also applies to LiveData and Alarm web services, although the problem of large web.config files is not that relevant for live data and alarming: only extremely large CSWorks solutions use more than thousand data sources and more than thousand alarm groups.

CSWorks user authorization using third-party access management system - Lab 04

clock October 22, 2010 13:38 by author Sergey Sorokin


In CSWorks 1.4.3900.0 we have added a possibility to restrict user access to specific data sources, alarm groups and historical data points. Inquiring users will find compelling answers awaiting them in CSWorks documentation that explains new security model in detail. In this article, I will focus on CSWorks integration with a third-party access rights management system. For the purpose of demonstration, I will be using SecureAccess 4.2 by PortSight - .NET security component for user management and application access rights management. This product meets all of the requirements to be used as an underlying access management system:

  • it can be accessed from .NET applications
  • it allows custom privileges like LiveData Read, LiveData Write etc.
  • it allows storing of resource identifiers: strings (data sources) and guids (alarm groups, historical data points)
  • it allows retrieval of all resource identifiers of a specific type (data sources, alarm groups, historical data points) available to a specific user for specific action (read, write, acknowledge)

I will walk through the process of creating a custom authorization provider for CSWorks and using it with Secure Demo application that comes as part of the CSWorks setup.

Setting up SecureAccess

Here are the steps:

  • install SecureAccess on a server computer;
  • provide it with database server access and create a new database using SecureAccess Catalog Manager, call this database "SecureAccess";
  • create the following application hierarchy: Applications->AlarmSecurity->Alarm Groups->Pipeline alarms;
  • create a role "PipelineAdmins", create a user "JohnDoe" and add him to the role;

The following screenshot of the SecureAccess administration panel shows what has been done so far:


Now make sure that Description field of the application part "PipelineAlarms" contains the id of the alarm group "All Pipes" used by CSWorks Alarm Service and defined in Alarms.xml:

<alarmGroup id="{4709B095-BBB6-4e48-97B8-AF15C5F19DD6}" description="All Pipes">

The following screenshot displays "PipelineAlarms" details:


At the level of "AlarmSecurity" application, create custom permission types ReadAlarms and AckAlarms and make sure they are available in "PipelineAlarms" application part:

Permission Types

Now the most important thing: on "PipelineAlarms" application part level, give ReadAlarms and AckAlarms permissions "PipelineAdmins" role. Permission matrix for "PipelineAlarms" will look as follows:

Permission Matrix

Now SecureAccess is ready to perform authorization for users who want to access CSWorks alarms in "All Pipes" alarm group.

Developing authorization provider

Using Microsoft Visual Studio, create a new .NET library CSWorks.Server.SecureAccessAuthorizationProvider.dll that references CSWorks.Server.WebSecurity.dll (it comes as part of the CSWorks setup) and the following SecureAccess libraries:

  • ARDataServices.dll
  • ARObjects.dll
  • ARWebSecurity.dll
  • SecureAccess.dll

Create a class SecureAccessAuthorizationProvider that implements IAuthorizationProvider interface defined in CSWorks.Server.WebSecurity:

using System;
using System.Collections.Generic;
using System.Linq;
using PortSight.SecureAccess.ARObjects;
using CSWorks.Server.Diagnostics;

namespace CSWorks.Server.WebSecurity
  public class SecureAccessAuthorizationProvider: IAuthorizationProvider
    public void Open(string connectionString, string userName)
      // Nothing to do

    public void Close()
      // Nothing to do

    public IEnumerable GetPrivilegeGuids(string userName, Privilege privilege)
      List ids = new List();
      if (privilege == Privilege.AlarmRead || privilege == Privilege.AlarmAck)
        IEnumerable alarmGroupAppParts = ARHelper.GetARObjects(ARObjectTypesEnum.ARApplicationPart).OfType().Where(ap => ap.ObjectAlias.StartsWith("AlarmSecurity.AlarmGroups.", StringComparison.InvariantCultureIgnoreCase));
        string secureAccessPrivilegeName = (privilege == Privilege.AlarmRead ? "ReadAlarms" : "AckAlarms");

        // Build a list of all group ids this user is authorized to READ or ACK
        foreach (ARApplicationPart ap in alarmGroupAppParts)
          if (ARHelper.IsAuthorized(userName, ap.ObjectAlias, secureAccessPrivilegeName))
            // Take group id from the app part description
            ids.Add(new Guid(ap.ObjectDescription));
        // Report error: privilege not supported

      return ids;

    public IEnumerable GetPrivilegeStrings(string userName, Privilege privilege)
      throw new NotImplementedException();

The idea is very straightforward: when given a user name and alarm privilege, GetPrivilegeGuids() method should return all alarm group ids this user has correspondent access to. GetARObjects() returns all application parts under "AlarmGroups", and IsAuthorized() method filters out those alarm groups that are not accessible.

Using authorization provider with your application

Copy CSWorks.Server.SecureAccessAuthorizationProvider.dll together with all SecureAccess dependencies to the bin folder of SecurityDemo application (CSWorks\Demo\Web\bin) so secure CSWorks web services can access it. Add the following lines to the web.config of SecureAlarmWebService located at CSWorks\Demo\Web\SecureAlarmWebService (make sure you have specified proper SQL Server name or address, SQL Server user name and password, and a valid SecureAccess license key):

    <sectionGroup name="authorizationProviderConfig">
      <section name="authorizationProviders" type="CSWorks.Server.WebSecurity.AuthorizationProviderConfigurationSection, CSWorks.Server.WebSecurity" />
    <add key="SecureAccessConnectionString" value="data source=sqlserver;initial catalog=SecureAccess;user id=sqluser;password=sqlpassword;packet size=4096" />
    <add key="SecureAccessDefaultCulture" value="en-US" />
    <add key="SecureAccessParentFrameName" value="" />
    <add key="SecureAccessLicenseKey" value="XXXX-XXXX-XXXX-XXXX-XXXX" />
    <add key="SecureAccessVirtualPath" value="" />
    <add key="SecureAccessTopRecords" value="1000" />
    <add key="SecureAccessCacheExpiration" value="30" />
    <add key="SecureAccessApplicationAlias" value="SecureAccess" />
    <add key="SecureAccessLogonFormRedirectsTo" value="" />
    <authorizationProviders activeAuthorizationProvider="secureAccessAuthorizationProvider">
      <authorizationProvider name="secureAccessAuthorizationProvider" type="CSWorks.Server.WebSecurity.SecureAccessAuthorizationProvider, CSWorks.Server.SecureAccessAuthorizationProvider" connectionString="" allowCaching="true"/>

SecureAlarmWebService is ready to use the provider you have just crafted. Now modify SecurityDemo.aspx.cs source file of the SecurityDemo sample application by replacing "demooperator@acme.com" with "johndoe" in the AllowAlarmPipes_Click() method as follows:

namespace CSWorks.Server.SecurityDemoWebApplication.WebApplication
  public partial class SecurityDemo : System.Web.UI.Page

    protected void AllowAlarmPipes_Click(object sender, EventArgs e)
      //System.Web.Security.FormsAuthentication.SetAuthCookie("demooperator@acme.com", true);
      System.Web.Security.FormsAuthentication.SetAuthCookie("johndoe", true);


From now on, when a user clicks the "Allow 'All Pipes' alarm group only (no acks)" he/she is impersonated as "John Doe" and gets read/ack access to all alarms in the group "All Pipes". All other alarm groups are not available to this user.

Running the demo

Build updated SecurityDemo application, run it and click the "Allow 'All Pipes' alarm group only (no acks)" button. Make sure that "JohnDoe" user name appears in the message on top of the page and you have read/ack access only to those alarms that belong to "All Pipes" alarm group. You can do that by running "Pipes and tanks" demo application and closing all valves that feed the mixing tank: this eventually will trigger an alarm called "Both lines are empty" in "All Pipes" group, and you will be able to see it and acknowledge it from the Security Demo.

The discussed example deals with CSWorks alarming only, but you can easily do the same for LiveData sources (use data source name for SecureAccess application part description, and implement provider's GetPrivilegeStrings() method) and historical data points (use data point id for SecureAccess application part description, and add some code to GetPrivilegeGuids() method).

CSWorks 1.4.3900.0 released

clock October 20, 2010 21:53 by author Sergey Sorokin

What's new:

  • Security: item-level authorization, XmlAuthorizationProvider, using User.Identity
  • Setup: better component granularity, friendly ASP.NET/IIS dialogs
  • Security Demo: now uses LiveData, Alarm and Historical Data