Software/Zaber Console/Source Code

From ZaberWiki
Jump to navigation Jump to search

We provide the source code for Zaber Console, along with some simpler example projects as a learning tool for developers writing Microsoft.NET software to drive Zaber devices. This page links to design documentation, tips and tricks, and how-to pages. Before working with the source code, try installing Zaber Console and experimenting with it. Writing scripts in Zaber Console is an easy place to start before going to the effort of installing all the development tools.

Support level

Zaber will provide limited support to customers attempting to build or customize the Zaber Console source code.

PLEASE NOTE that the APIs presented by the .NET libraries provided with Zaber Console - namely the Zaber and ZaberWpfToolbox libraries - are subject to change without notice and only major changes will be documented in release notes. If you need a stable SDK on which to build your application, we recommend upgrading infrequently and with care.

Getting started with Zaber source code

Download or clone the latest source code from the Zaber GitLab account.

Development Tools

To view, edit and build the Zaber source files, you will require some additional development tools. Here are the basic development tools we use:

The above is all you need to compile and run Zaber Console. Additional things you can install if needed:

  • If you want to be able to compile the Zaber Console installer:
    1. Open Visual Studio and select "Tools: Extensions and Updates..." from the toolbar.
    2. Click on "Online" from the menu from the left, then enter "Visual Studio Installer" in the search box in the top-right.
    3. Install the Visual Studio Installer Projects extension from the search results.
  • If you want to be able to regenerate the .CHM help files:

Building and running the Zaber source files

These instructions describe how to build and run the projects from source code so that you can start making changes.

Downloading the source files

  1. Download or clone the source code and unzip it to a working folder on your computer. The following folders are of interest:
    • ZaberConsoleGUI - The application code
    • ZaberCSDemo - simple C# Demo projects
    • ScriptRunner - lets the user run script files from the command line
    • Zaber - the Zaber communication library (only used by Zaber Console)
    • ZaberWpfToolbox - custom WPF components used in Zaber applications
    • plugins - source for plugins that are not compiled into Zaber Console directly
    • lib - third-party libraries

There will also be accompanying test projects for some of the above.

Building the source files

  1. Install Visual Studio on your system (see the development tools section above).
  2. Double-click on the solution file src/ZaberCSDemo/ZaberCSDemo.sln to open it. You may see an error about your version of Visual Studio not supporting the ZaberConsoleSetup project. This will not stop you from compiling the code; it just means you will not be able to build the installer. If you want to avoid this error, you can either delete the setup project from the solution or follow these instructions:
    • Open Visual Studio and select "Tools: Extensions and Updates..." from the toolbar.
    • Click on "Online" from the menu from the left, then enter "Visual Studio Installer" in the search box in the top-right.
    • Install the Visual Studio Installer Projects extension from the search results.
  3. Make sure the Solution Explorer pane is visible. If not, click View/Solution Explorer.
  4. The Solution Explorer pane shows all the projects contained in the Solution. Make sure "ZaberConsoleGUI" is the startup project (it should be shown in bold). If not, right click on "ZaberConsoleGUI" and select "Set as Startup Project".
  5. Press F5 or click the "Start Debugging" icon (a green arrow in the title bar) to build and execute the Zaber Console project. There may be a delay the first time as the NuGet Package Manager downloads dependencies.
  6. Repeat the last 2 steps for any other project you wish to build and execute.

Running the executables

If you wish, you can create shortcuts to the executable files so you can run them without starting Visual Studio. Keep in mind that whenever you build a project, the corresponding executable gets overwritten. If the path shown below doesn't exist, you may need to build the project first by selecting Build Solution from the Build menu.

Project Executable location
Zaber Console src\ZaberConsoleGUI\bin\Release\ZaberConsole.exe
Script Runner (command line) src\ScriptRunner\ScriptRunner\bin\Release\ScriptRunner.exe

If you make changes to the Zaber libraries and re-build them, you can find the new Zaber.dll file in the folder src\Zaber\bin\Release\Zaber.dll and the new ZaberWpfToolbox.dll in src\ZaberWpfToolbox\bin\Release\ZaberWpfToolbox.dll.

Note the above file locations assume you are building in the release configuration. If you are using the debug configuration, replace Release with Debug in the above paths.

Strong name key

The executable and some libraries in the official Zaber Console installation are signed with a strong name. If you're compiling from the public source code, strong naming is disabled by default. This should not present any problems unless you want to distribute your own signed build of Zaber Console to your users. You will need to obtain or generate a strong naming key and a code signing certificate. Examine the Enable-Code-Signing and Disable-Code-Signing targets in the ZaberCSDemo/build.fsx file; the Enable-Code-Signing target will alter the project files to enable strong naming and code signing where needed for our distribution. Your plugin project will not need similar changes, but if you add new library projects they will need a code signing section.

Creating a new client project

Follow these steps to create a project that uses the Zaber library to drive a chain of Zaber devices. These steps assume you are using Visual Studio 2015, but the procedure should be similar for any version of the Visual Studio environment.

Zaber Console Plugin

This is the simplest type of client project. It's just a control library that has at least one class marked with the Zaber.PlugIns.PlugIn attribute. We recommend looking at the SimpleControlsPlugin as an example.

The simplest way to set up your own plugin is to add a new project to the ZaberCSDemo solution. The currently recommended way to create plugins is to use WPF and the MVVM pattern. The following instructions will illustrate.

  1. Open the ZaberCSDemo.sln file.
  2. Scroll to the top of the Solution Explorer window, right click on the solution, and choose Add: New Project....
  3. Type a name for your plugin project, and create a folder to save it in.
  4. Choose the Class Library template. Click OK.
  5. Right-click on your project in the Solution Explorer and select "Properties", then in the Application section set the Target Framework setting to be the same as the Zaber project - currently .NET Framework 4. This will avoid some possible warning messages when loading your plugin into Zaber Console.
  6. Add references from your project to the following assemblies. To add a reference, right-click on your project in the Solution Explorer pane and select "Add / Reference...". Then click the Assembies group and choose the ones to add. Click OK.
    • PresentationCore
    • PresentationFramework
    • System.ComponentModel.Composition
    • System.Xaml
  7. Add a reference from your project to the Zaber library project and optionally the ZaberWpfToolbox library. Using ZaberWpfToolbox is recommended unless you have your own preferred WPF library. To add a reference, right-click on your project in the Solution Explorer pane and select "Add / Reference...". Then click the Projects tab and choose the Zaber and ZaberWpfToolbox projects. Click OK.
  8. Add a reference to log4net, which is a dependency of the Zaber library. For this one go "Add / Reference" then select the Browse group, click the Browse button at lower right, then navigate to the lib folder and pick log4net.dll.
  9. The project template will have created an empty class for you called Class1.cs. Rename this to something more useful, like MyPlugin.cs.
  10. Open MyClass.cs by double-clicking on it.
  11. Mark the control as a plugin.
    • At the top of the file, add two using declarations: using Zaber; and using Zaber.PlugIns;
    • Just before the class declaration, add this attribute: [PlugIn(Name = "My Plugin)]
  12. Build the user interface for your control.
    • Right-click on your project, select Add / New Item and then select "User Control (WPF)". Enter a name for it, such as MyPluginView.xaml. This file will define your user interface. There are lots of online tutorials on WPF and XAML, for example: http://www.wpf-tutorial.com/xaml/what-is-xaml/
    • Write the code for your plugin and the markup for your user interface in parallel. After we associate them in a step below, the user interface (the view) will receive your plugin class (the viewmodel) as its DataContext, or the place it pulls data from by default.
    • For example, if you look at the XAML view for your MyPluginView.xaml file, you should see the opening and closing elements for a Grid. Add a label between them to display the name of the currently selected device. The grid section of the file should now look like this:
      <Grid>
      <Label Content="{Binding DeviceName}" />
      </Grid>
    • The "Binding DeviceName" part means the content displayed by the label should come from a public property called DeviceName on the DataContext object, which is your MyPlugin class. Let's add that next.
    • Go back to MyPlugin.cs. In order to ensure the label display is always in sync with the data, we need to make this class implement the INotifyPropertyChanged interface. Note that Zaber Console expects WPF-compatible plugin classes to implement INotifyPropertyChanged, either directly or by inheritance. If they do not, Zaber Console will assume the plugin has no graphical user interface. ZaberWpfToolbox provides an easy way to do this by inheritance:
    • First, at the top of the file add a using statement for ZaberWpfToolbox.
    • Next, make your class inherit from the ObservableObject implementation in ZaberWpfToolbox. Change the class declaration line to:
      public class MyPlugin: ObservableObject
    • Then, inside the class add a private member and a public observable property for the label to pull content from. Here Set (note capitalization) is a method implemented in ZaberWpfToolbox.ObservableObject that handles the INotifyPropertyChanged implementation for you. Any time you change the value of this DeviceName property, the WPF layout engine will receive a notification of the change and will update the label in your user interface.
      private string _deviceName = "<unknown>";
      public string DeviceName
      {
      get { return _deviceName; }
      set
      {
      Set(ref _deviceName, value, nameof(DeviceName));
      }
      }
  13. Now you can add some plugin properties that will be set by Zaber Console. The most common is to add a Conversation property. This is your interface with the Zaber devices. We'll set up so that when the value is changed, which happens when you click on a different device in the Zaber Console Device List, it will update our label with the name of the device selected. The PlugInProperty attribute tells Zaber's Plugin Manager to update this property when its value should change. In knows by the Conversation property type that this property is meant to hold a reference to the currently selected device conversation. The above implementation of the setter will update the DeviceName property in turn, and the implementation of the DeviceName setter will cause the display to be updated.
    private Conversation _conversation;
    [PlugInProperty]
    public Conversation Conversation
    {
    get { return _conversation; }
    set
    {
    _conversation = value;
    DeviceName = _conversation?.Device.Description ?? "<unknown>";
    }
    }
  14. Finally, you need to create a Resource Dictionary that associates your view type with your viewmodel type by use of a WPF DataTemplate. This is how your user interface will get automatically displayed when Zaber Console instantiates your plugin class.
    • Right-click on your project, select "Add / New Item", pick XML file, and enter a name for it, such as "StaticResource.xaml". The main part of the name doesn't matter, but the extension should be ".xaml".
    • Open StaticResources.xaml in the editor and delete any existing content. Make it look like this, with any appropriate name changes to match your classes:
      <ResourceDictionary x:Class="MyPlugin.StaticResources"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="clr-namespace:MyPlugin">
      <DataTemplate DataType="{x:Type local:MyPlugin}">
      <local:MyPluginView />
      </DataTemplate>
      </ResourceDictionary>
    • This DataTemplate means that whenever an instance of MyPlugin is exposed in the user interface, an instance of MyPluginView should be created and drawn, with its DataContext property initialized to refer to the MyPlugin instance. The WPF layout engine handles this automatically, but you need to ensure that Zaber Console picks up this Resource Dictionary and merges it into the main application-level one:
    • Add a .cs class file corresponding to this ResourceDictionary: StaticResources.cs.
    • make the content of StaticResources.cs look as below. This extends the ResourceDictionary you defined in the .xaml file above, and adds an attribute that enables Zaber Console to identify this as your plugins' main Resource Dictionary:
      using System.ComponentModel.Composition;
      using System.Windows;
      using Zaber.PlugIns;
      namespace MyPlugin
      {
      [Export(PlugInAttribute.PLUGIN_RESOURCE_DICTIONARY, typeof(ResourceDictionary))]
      public partial class StaticResources : ResourceDictionary
      {
      public StaticResources()
      {
      InitializeComponent();
      }
      }
      }
    • ZaberConsole will look for the Export attribute when the PLUGIN_RESOURCE_DICTIONARY name to find your plugin's top-level Resource Dictionary. Normally there should only be one exported per plugin, but in its .xaml file it can merge in other local dictionaries.
  15. Decide whether you need initialization code in your plug in. You can do some initialization in your viewmodel's constructor. If you need to register for events like opening the port, then you should add a ZaberPortFacade property, mark it with the [PlugInProperty] attribute, and register for any events in the property setter. You can also detect when your plugin is initialized or destroyed and when its user interface is visible or hidden by adding public methods marked with the [PlugInMethod] attribute.
  16. Screenshot of the example plugin described in the text
    The easiest way to test your plugin is to go to Zaber Console's advanced tab, and point the plugin folder at an empty directory and copy your plugin's bin/Release/MyPlugin.dll to that folder. If you're using several plugins, you may prefer to add a post-build step that copies your plugins to Zaber Console's bundled plugin folder; see the post-build step of the SimpleControlsPlugin for an example. Note that if Zaber Console's plugin folder points at a location that has many unrelated DLLs in it, such as your plugin's bin/Release folder, it can slow down program startup and can cause errors during plugin loading.
    • If all went well, you should see MyPlugin in the list of available tabs in Zaber Console, and if you show it you should see the label displaying "<unknown>" or the name of the device you select.

Distributing your plugin

You have several options for sharing your plug in with other users.

  1. You can get all users to download it to their own plugin folders. When you release a new version, they have to exit Zaber Console and copy the new version to their folder.
  2. If your users are on the same local network, they can use a network share as a plugin folder. If you put a version number at the end of your plugin's file name, then you can copy new versions into the folder while users have the old version running. When Zaber Console starts, it loads the highest version number. For example: ZaberPortDemo-1.1.dll is higher than ZaberPortDemo-1.0.dll. If will warn users if it finds multiple versions of the same plugin, but will only activate one of them.

When you are deciding what to name your project and control, the names appear in two places. The plugin will show up in the list on the Advanced tab as "ProjectName.ControlName". The plugin's name in the main list of tabs defaults to "ControlName". However, you can override the default with a parameter in the [PlugIn] attribute. Here's an example that overrides the name:

   [PlugIn(Name="Device Demo")]
   public partial class DevicePlugIn : UserControl
   {
       ...
   }

Legacy plugins

Pre-2.0 versions of Zaber Console expected the classes bearing the PlugIn attribute to also be Windows Forms User Controls. This approach is deprecated but still supported; Zaber Console will automatically embed Windows Forms plugins in a compatibility layer called a WindowsFormsHost. However, the compatibility isn't perfect; specifically it is no longer possible for your Windows Forms plugin to obtain a reference to the Window that contains it. Some plugins did this to detect when Zaber Console was closing in order to clean up their resources. The recommended way to do this now is to create a public cleanup method and tag it with the attribute [PlugInMethod(PlugInMethodAttribute.EventType.PluginDeactivated)]. The Zaber Console Plugin Manager will invoke this method for you when the program is shutting down.

Plugin Manager interfacing

Zaber Console's internal plugin management system provides the data and events your plugin needs in order to do anything useful. This Plugin Manager identifies your plugins by looking for classes marked with the [PlugIn] attribute. Once it instantiates one of them, it looks for methods and properties tagged with additional attributes in order to determine what your plugin wishes to have communicated to it. Below are examples of all possible connections; you can use any combination of these. They must be public properties and methods. Their names do not matter but their types do.

// Low-level interface to the port that all device communication is routed through.
[PlugInProperty]
public IZaberPort Port { get; set; }


// Higher-level interface to the communication port; provides additional 
// functionality.
[PlugInProperty]
public ZaberPortFacade PortFacade { get; set; }


// The device(s) currently selected in the Device List part of Zaber Console's
// user interface. This can be an axis, a controller, an integrated device
// (both axis and controller) or an arbitrary group of devices.
[PlugInProperty]
public ZaberDevice Device { get; set; }


// The conversational representation of the currently selected device(s).
// this provides extra communication functionality. Again this can represent
// multiple disparate devices.
[PlugInProperty]
public Conversation Conversation { get; set; }


// The unit of measure selected for the current device in the Device List.
[PlugInProperty]
public UnitOfMeasure MasterUnitOfMeasure { get; set; }


// An interface the plugin can use to write text to the Message Log.
// (To write to the Application Log, use a log4net ILog instance.)
[PlugInProperty]
public TextWriter Output { get; set; }


// Get a reference to the Plugin Manager itself.
[PlugInProperty]
public IPlugInManager PlugInManager { get; set; }


// This method will be invoked once when Zaber Console instantiates the plugin.
// This might be at program startup or it might be later, when the user
// selects the plugin to be displayed.
[PlugInMethod(PlugInMethodAttribute.EventType.PluginActivated)]
public void OnActivated()
{
}


// This method will be invoked once when Zaber Console is deactivating the
// plugin. This might be when the program is closing or it might be when
// the user has chosen to hide the plugin, which will eventually lead to it
// being garbage collected.
[PlugInMethod(PlugInMethodAttribute.EventType.PluginDeactivated)]
public void OnDeactivated()
{
}


// This method is called when the plugin's user interface has become visible.
// This might be right after the plugin is instantiated, or it might be as a
// result of the user switching tabs in Zaber Console. There is no guarantee
// against multiple invocations. It will never occur before the PluginActivated
// event.
[PlugInMethod(PlugInMethodAttribute.EventType.PluginShown)]
public void OnShown()
{
}


// This method is called when the plugin's user interface becomes invisible,
// either because the program is closing or because the user has switched to
// another tab. There is no guarantee against multiple invocations. It will
// never occur after PluginDeactivated.
[PlugInMethod(PlugInMethodAttribute.EventType.PluginHidden)]
public void OnHidden()
{
}

Stand-alone client project

If you don't want to run as part of Zaber Console, you have to take care of configuration and opening the serial port yourself. There are two example projects that show ways of doing that: SpringDemo and SpringFreeDemo. The source code for both of them is under the src/ZaberCSDemo folder. The help tab on each form describes some benefits of each technique.